"""카메라 문법 팩(flow v3 · staging v20) 오프라인 파일럿 — 표적 씬 재저작 대조.

프로덕션 CP 는 **읽기 전용**이다. 어떤 체크포인트/DB 행도 쓰지 않는다.
산출은 artifact/<날짜>_카메라문법_재저작_대조/ 에만 남긴다.

흐름:
  1. CP 로드(shot_validator·shot_selection·scene_save·scene_director·
     entity_merge·visual_world_rules·scene_camera_flow(구)·shot_staging(구))
  2. 표적 씬별 scene_camera_flow 재저작 — 스텝 _process_scene 과 동일한
     프롬프트 구성, 팩은 로더가 최신(v3)을 자동 선택
  3. 새 flow 를 입력으로 run_shot_staging(표적 씬만 필터) — 팩 v20 자동
  4. 전후 대조 JSON + HTML 갤러리 (표적 샷 강조, 구 최종 스틸 썸네일 동반)

사용:
  .venv/bin/python camera_grammar_pilot.py \
      --project <pid> --episode <eid> --scenes 1,4,5,9,37,39,56,64,65 \
      --targets S1sh11,S4sh9,S5sh6,S9sh10,S9sh13,S9sh21,S65sh10,S64sh1,S56sh1,S39sh4,S37sh6
"""
from __future__ import annotations

import argparse
import copy
import html
import json
import re
import sys
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple

sys.path.insert(0, str(Path(__file__).resolve().parent))

ROOT = Path(__file__).resolve().parent.parent


def _load_cp(projects_dir: str, pid: str, eid: str, step: str) -> Dict[str, Any]:
    p = Path(projects_dir) / pid / "checkpoints" / "episodes" / eid / step / "manifest.json"
    if not p.exists():
        raise SystemExit(f"CP 없음: {p}")
    return json.loads(p.read_text("utf-8"))


def reauthor_flow_scene(
    si: int,
    seg: Dict[str, Any],
    selected_shots: List[dict],
    unselected_shots: List[dict],
    entity_names: List[str],
    system_prompt: str,
    user_template: str,
    schema: Dict[str, Any],
    project_config: Optional[Dict[str, Any]],
) -> Dict[str, Any]:
    """scene_camera_flow_step._process_scene 과 동일 구성의 오프라인 재저작."""
    from app.modules.llm.llm_client import call_structured

    sel_lines = [
        f"  Shot {sh.get('shot_index', '?')}: {sh.get('description', '')}"
        for sh in selected_shots
    ]
    unsel_lines = [
        f"  Shot {sh.get('shot_index', '?')}: {sh.get('description', '')}"
        for sh in unselected_shots
    ]
    user_prompt = user_template.format(
        scene_index=si,
        scene_heading=seg.get("heading", ""),
        scene_text=seg.get("text", ""),
        selected_shots_block="\n".join(sel_lines) if sel_lines else "  (없음)",
        unselected_shots_block="\n".join(unsel_lines) if unsel_lines else "  (없음)",
        entities_block=", ".join(entity_names) if entity_names else "(엔티티 정보 없음)",
    )
    call_schema = copy.deepcopy(schema)
    selected_indices = [sh["shot_index"] for sh in selected_shots]
    if selected_indices:
        call_schema["properties"]["shot_assignments"]["items"]["properties"][
            "shot_index"] = {"type": "integer", "enum": selected_indices}
    result = call_structured(
        step="scene_camera_flow",
        system_prompt=system_prompt,
        user_prompt=user_prompt,
        response_schema=call_schema,
        project_config=project_config,
        schema_name=f"camera_pilot_flow_{si}",
    )
    result["scene_index"] = si
    return result


def _flow_stage_text(sc: Optional[Dict[str, Any]], shi: int) -> str:
    """씬 flow 결과에서 이 샷이 배정된 단계의 원문을 요약."""
    if not sc:
        return "(없음)"
    stages = {s.get("stage_index"): s for s in sc.get("flow_stages") or []}
    for a in sc.get("shot_assignments") or []:
        if a.get("shot_index") == shi:
            st = stages.get(a.get("stage_index")) or {}
            dev = a.get("deviation_note") or ""
            return (
                f"[stage{a.get('stage_index')} {st.get('stage_label', '')} · "
                f"{st.get('camera_motion', '')} · pos={a.get('flow_position', '')}]\n"
                f"position: {st.get('camera_position', '')}\n"
                f"focus: {st.get('visual_focus', '')}"
                + (f"\ndeviation: {dev}" if dev else "")
            )
    return "(배정 없음)"


def _metrics(stages: List[Dict[str, Any]]) -> Dict[str, Any]:
    pos = [str(s.get("camera_position") or "") for s in stages]
    total = max(1, len(pos))

    def n(pat: str) -> int:
        return sum(1 for t in pos if re.search(pat, t, re.I))

    return {
        "stages": len(pos),
        "eye_level": n(r"eye level"),
        "non_eye_height": n(r"low angle|high angle|overhead|elevated|above|waist|chest|knee|ground"),
        "frontal": n(r"in front of|directly opposite|facing"),
        "oblique": n(r"behind|over.the.shoulder|beside|side|lateral|profile|oblique|diagonal|angled"),
        "_total": total,
    }


def main() -> None:
    ap = argparse.ArgumentParser()
    ap.add_argument("--project", required=True)
    ap.add_argument("--episode", required=True)
    ap.add_argument("--scenes", required=True, help="쉼표 씬 인덱스")
    ap.add_argument("--targets", default="", help="강조 표시할 SxshY 쉼표 목록")
    ap.add_argument("--out", default="")
    args = ap.parse_args()

    from app.core.config import settings
    from app.core.creator_corrections import project_corrections_block
    from app.core.database import SessionLocal
    from app.modules.pipeline.shot_staging import run_shot_staging
    from app.modules.pipeline.still_recipe import build_camera_frame_clause
    from app.modules.prompt_loader import (
        get_effective_source,
        load_prompt,
        load_schema,
    )
    from app.services.step_execution_service import _load_project_config

    pid, eid = args.project, args.episode
    scene_set = {int(x) for x in args.scenes.split(",") if x.strip()}
    targets = {t.strip() for t in args.targets.split(",") if t.strip()}

    # 팩 확인 — 로더가 실제로 무엇을 고르는지 기록 (측정 도구≠프로덕션 방지)
    def _file_ver(module: str) -> str:
        src = get_effective_source(module, "system")
        return str(((src.get("candidates") or {}).get("file") or {})
                   .get("version"))

    flow_src = {"version": _file_ver("scene_camera_flow")}
    staging_src = {"version": _file_ver("shot_staging")}
    print(f"flow pack   = {flow_src.get('version')}")
    print(f"staging pack = {staging_src.get('version')}")

    projects_dir = settings.projects_dir
    validator = _load_cp(projects_dir, pid, eid, "shot_validator")["data"]
    selection = _load_cp(projects_dir, pid, eid, "shot_selection")["data"]
    scene_save = _load_cp(projects_dir, pid, eid, "scene_save")["data"]
    director = _load_cp(projects_dir, pid, eid, "scene_director")["data"]
    merge = _load_cp(projects_dir, pid, eid, "entity_merge")["data"]
    vwr = _load_cp(projects_dir, pid, eid, "visual_world_rules")["data"]
    old_flow = _load_cp(projects_dir, pid, eid, "scene_camera_flow")["data"]
    old_staging = _load_cp(projects_dir, pid, eid, "shot_staging")["data"]

    db = SessionLocal()
    try:
        project_config = _load_project_config(db, pid)
    finally:
        db.close()

    seg_by_scene = {s.get("scene_index"): s for s in scene_save.get("segments", [])}
    selected_map = {
        s["scene_index"]: set(s.get("selected_shot_indices", []))
        for s in selection.get("scenes", [])
    }
    id_to_name: Dict[str, str] = {}
    for etype in ("characters", "locations", "props"):
        for e in merge.get(etype, []):
            if e.get("short_id"):
                id_to_name[e["short_id"]] = e.get("name", "")
    entities_by_scene: Dict[int, List[str]] = {}
    for sc in director.get("scenes", []):
        si = sc.get("scene_index")
        names = [id_to_name.get(v, "") for v in (sc.get("visible_entity_ids") or [])]
        entities_by_scene[si] = [n for n in names if n]

    system_prompt = load_prompt("scene_camera_flow", "system")
    user_template = load_prompt("scene_camera_flow", "user")
    schema = load_schema("scene_camera_flow", "schema")

    # ── 1) flow 재저작 (표적 씬) ────────────────────────────────────
    new_flow_scenes: List[Dict[str, Any]] = []
    for sc in validator.get("scenes", []):
        si = sc.get("scene_index")
        if si not in scene_set:
            continue
        sel = selected_map.get(si, set())
        shots = sc.get("shots", [])
        selected_shots = [sh for sh in shots if sh.get("shot_index") in sel]
        if not selected_shots:
            print(f"S{si}: 선택 샷 없음 — 건너뜀")
            continue
        unselected_shots = [sh for sh in shots if sh.get("shot_index") not in sel]
        print(f"S{si}: flow 재저작 ({len(selected_shots)} shots)...")
        new_flow_scenes.append(reauthor_flow_scene(
            si, seg_by_scene.get(si, {}), selected_shots, unselected_shots,
            entities_by_scene.get(si, []), system_prompt, user_template,
            schema, project_config,
        ))

    # ── 2) staging 재저작 (새 flow 입력, 표적 씬만 필터) ────────────
    validator_f = {
        **validator,
        "scenes": [s for s in validator.get("scenes", [])
                   if s.get("scene_index") in scene_set],
    }
    selection_f = {
        **selection,
        "scenes": [s for s in selection.get("scenes", [])
                   if s.get("scene_index") in scene_set],
    }
    print("staging 재저작...")
    new_staging = run_shot_staging(
        shot_extract_data=validator_f,
        shot_selection_data=selection_f,
        scene_save_data=scene_save,
        entity_merge_data=merge,
        vwr_data=vwr,
        camera_flow_data={"scenes": new_flow_scenes},
        creator_corrections_block=project_corrections_block(pid),
    )

    # ── 3) 대조 조립 ────────────────────────────────────────────────
    old_flow_by_scene = {s.get("scene_index"): s for s in old_flow.get("scenes", [])}
    new_flow_by_scene = {s.get("scene_index"): s for s in new_flow_scenes}
    old_st_by_key = {
        (s.get("scene_index"), s.get("shot_index")): s
        for s in old_staging.get("shots", [])
    }
    new_st_by_key = {
        (s.get("scene_index"), s.get("shot_index")): s
        for s in new_staging.get("shots", [])
    }

    rows: List[Dict[str, Any]] = []
    for (si, shi), new_st in sorted(new_st_by_key.items()):
        old_st = old_st_by_key.get((si, shi)) or {}
        tag = f"S{si}sh{shi}"
        rows.append({
            "tag": tag,
            "target": tag in targets,
            "flow_old": _flow_stage_text(old_flow_by_scene.get(si), shi),
            "flow_new": _flow_stage_text(new_flow_by_scene.get(si), shi),
            "cam_old": old_st.get("camera_direction", ""),
            "cam_new": new_st.get("camera_direction", ""),
            "clause_old": build_camera_frame_clause(old_st),
            "clause_new": build_camera_frame_clause(new_st),
            "framing_old": old_st.get("framing_scale", ""),
            "framing_new": new_st.get("framing_scale", ""),
            "sel_png": f"projects/{pid}/images/{eid}/scene/recipe/{tag}_sel.png",
        })

    m_old = _metrics([
        st for sc in old_flow.get("scenes", [])
        if sc.get("scene_index") in scene_set
        for st in sc.get("flow_stages") or []
    ])
    m_new = _metrics([st for sc in new_flow_scenes for st in sc.get("flow_stages") or []])

    out_dir = Path(args.out) if args.out else (
        ROOT / "artifact" / "20260812_카메라문법_재저작_대조")
    out_dir.mkdir(parents=True, exist_ok=True)
    (out_dir / "data.json").write_text(json.dumps({
        "flow_pack": flow_src.get("version"),
        "staging_pack": staging_src.get("version"),
        "metrics_old": m_old, "metrics_new": m_new,
        "rows": rows,
        "new_flow_scenes": new_flow_scenes,
        "new_staging_shots": new_staging.get("shots", []),
    }, ensure_ascii=False, indent=1), "utf-8")

    # ── 4) HTML ─────────────────────────────────────────────────────
    def esc(t: str) -> str:
        return html.escape(str(t or "")).replace("\n", "<br>")

    cards = []
    for r in sorted(rows, key=lambda x: (not x["target"], x["tag"])):
        border = "#e0245e" if r["target"] else "#334"
        badge = ("<span style='background:#e0245e;color:#fff;padding:1px 8px;"
                 "border-radius:9px;font-size:12px'>지적 샷</span>" if r["target"] else "")
        cards.append(f"""
<div style="border:1.5px solid {border};border-radius:10px;margin:14px 0;padding:12px;background:#151a24">
  <h3 style="margin:2px 0 8px">{r['tag']} {badge}
    <span style="color:#8aa;font-size:13px">framing: {esc(r['framing_old'])} → {esc(r['framing_new'])}</span></h3>
  <div style="display:flex;gap:12px;flex-wrap:wrap">
    <div style="flex:0 0 260px"><img src="../../{r['sel_png']}" style="max-width:100%;border-radius:6px"
         onerror="this.style.display='none'"><div style="color:#789;font-size:12px">현행 최종 스틸(구 지시 산출)</div></div>
    <div style="flex:1;min-width:340px">
      <table style="width:100%;border-collapse:collapse;font-size:13px">
        <tr><th style="width:50%;text-align:left;color:#f77;padding:4px;border-bottom:1px solid #334">구(현행)</th>
            <th style="text-align:left;color:#7f7;padding:4px;border-bottom:1px solid #334">신(팩 v3+v20)</th></tr>
        <tr><td style="vertical-align:top;padding:6px;border-right:1px solid #223"><b>flow</b><br>{esc(r['flow_old'])}</td>
            <td style="vertical-align:top;padding:6px"><b>flow</b><br>{esc(r['flow_new'])}</td></tr>
        <tr><td style="vertical-align:top;padding:6px;border-right:1px solid #223;border-top:1px solid #223"><b>staging camera_direction</b><br>{esc(r['cam_old'])}</td>
            <td style="vertical-align:top;padding:6px;border-top:1px solid #223"><b>staging camera_direction</b><br>{esc(r['cam_new'])}</td></tr>
        <tr><td style="vertical-align:top;padding:6px;border-right:1px solid #223;border-top:1px solid #223;color:#aac"><b>스틸 CAMERA 절(조립)</b><br>{esc(r['clause_old'])}</td>
            <td style="vertical-align:top;padding:6px;border-top:1px solid #223;color:#aac"><b>스틸 CAMERA 절(조립)</b><br>{esc(r['clause_new'])}</td></tr>
      </table>
    </div>
  </div>
</div>""")

    def pct(m: Dict[str, Any], k: str) -> str:
        return f"{m[k]}/{m['stages']} ({m[k] * 100 // max(1, m['stages'])}%)"

    html_doc = f"""<meta charset="utf-8">
<title>카메라 문법 재저작 대조 (flow v3 · staging v20)</title>
<body style="background:#0d1117;color:#dde;font-family:'Apple SD Gothic Neo',sans-serif;max-width:1280px;margin:0 auto;padding:18px">
<h1 style="font-size:20px">카메라 문법 재저작 대조 — 표적 {len(scene_set)}씬 / 지적 {len(targets)}샷</h1>
<p style="color:#9ab">팩: flow={esc(flow_src.get('version'))} · staging={esc(staging_src.get('version'))} ·
프로덕션 CP 무변경(오프라인 재저작). 지적 샷이 위, 같은 씬 나머지 샷이 아래.</p>
<table style="border-collapse:collapse;font-size:13px;margin:8px 0">
<tr><th style="padding:3px 10px;border:1px solid #334">flow 단계 지표</th>
    <th style="padding:3px 10px;border:1px solid #334;color:#f77">구</th>
    <th style="padding:3px 10px;border:1px solid #334;color:#7f7">신</th></tr>
<tr><td style="padding:3px 10px;border:1px solid #334">eye level 명시</td>
    <td style="padding:3px 10px;border:1px solid #334">{pct(m_old, 'eye_level')}</td>
    <td style="padding:3px 10px;border:1px solid #334">{pct(m_new, 'eye_level')}</td></tr>
<tr><td style="padding:3px 10px;border:1px solid #334">비-눈높이 높이 어휘</td>
    <td style="padding:3px 10px;border:1px solid #334">{pct(m_old, 'non_eye_height')}</td>
    <td style="padding:3px 10px;border:1px solid #334">{pct(m_new, 'non_eye_height')}</td></tr>
<tr><td style="padding:3px 10px;border:1px solid #334">정면 배치 어휘</td>
    <td style="padding:3px 10px;border:1px solid #334">{pct(m_old, 'frontal')}</td>
    <td style="padding:3px 10px;border:1px solid #334">{pct(m_new, 'frontal')}</td></tr>
<tr><td style="padding:3px 10px;border:1px solid #334">비껴난 축(측면·OTS·후방 등)</td>
    <td style="padding:3px 10px;border:1px solid #334">{pct(m_old, 'oblique')}</td>
    <td style="padding:3px 10px;border:1px solid #334">{pct(m_new, 'oblique')}</td></tr>
</table>
{''.join(cards)}
</body>"""
    (out_dir / "index.html").write_text(html_doc, "utf-8")
    print(f"완료: {out_dir}/index.html  (rows={len(rows)})")


if __name__ == "__main__":
    main()
