"""카메라 권한 갤러리 — 상류가 정한 앵글이 최종까지 남는가를 육안으로 본다.

묻는 것은 하나다: **shot_staging 이 쓴 camera_direction 이 최종 그림의 카메라와 맞는가.**

샷마다 다섯 단계를 나란히 놓는다.

    a     롤 후보 A — CAMERA & FRAME 을 따른 기본 구도
    b     롤 후보 B — `broll_composition_variation.md` 가 "CAMERA & FRAME is a
          start, not a lock" 이라며 앵글·높이·거리를 바꾸라고 시킨 후보
    sel   판정이 고른 승자 (a 냐 b 냐가 여기서 갈린다)
    fix   critique 수리본
    cine  `cine_transform.md` 가 "the source still does not fix where the camera
          stands" 라며 다시 푼 최종본 — 이것이 scene primary 가 된다

★왜 텍스트로 못 재나: `scene_still.camera_json` 은 전부 `{}` 이고
  `image_asset.source_image_id` 도 비어 있다. 계보가 DB 에 없어서 「무엇이
  무엇으로 바뀌었나」를 질의로는 못 잇는다. 파일 이름이 유일한 계보다.

★판정은 육안이다 — 낱말 대조로는 카메라가 어디 섰는지 못 가른다.

사용법:
    python tools/prompt_measure/build_camera_authority_gallery.py <project_id> <episode_id>
"""
import html
import json
import subprocess
import sys
from pathlib import Path

ROOT = Path("/Users/manta/Documents/Projects/TheRoad-I1")
STAGES = [
    ("a", "롤 후보 A", "CAMERA &amp; FRAME 을 따른 기본 구도"),
    ("b", "롤 후보 B", "「not a lock」 — 앵글·높이·거리를 바꾸라고 시킨 것"),
    ("sel", "판정 승자", "a 냐 b 냐가 여기서 갈린다"),
    ("fix", "critique 수리본", "관찰 지적을 반영"),
    ("cine", "cine 최종", "「카메라 위치를 고정하지 않는다」 — 이것이 primary"),
]


def _sha(p: Path) -> str:
    """★크기가 같다고 같은 파일이 아니다 — 승자 판별은 바이트로 한다."""
    import hashlib
    return hashlib.sha256(p.read_bytes()).hexdigest()


def lan_ip() -> str:
    try:
        return subprocess.run(["ipconfig", "getifaddr", "en0"],
                              capture_output=True, text=True, timeout=5).stdout.strip()
    except Exception:
        return "127.0.0.1"


def load_staging(proj: str, epi: str) -> dict:
    """shot_staging 체크포인트에서 샷별 camera_direction 을 꺼낸다."""
    cp = (ROOT / "projects" / proj / "checkpoints" / "episodes" / epi
          / "shot_staging" / "manifest.json")
    if not cp.exists():
        return {}
    shots = json.loads(cp.read_text(encoding="utf-8")).get("data", {}).get("shots") or []
    out = {}
    for s in shots:
        key = f"S{s.get('scene_index')}sh{s.get('shot_index')}"
        out[key] = {
            "camera_direction": s.get("camera_direction") or "",
            "lighting_mood": s.get("lighting_mood") or "",
            "framing_scale": s.get("framing_scale") or "",
            "perspective": s.get("perspective") or "",
        }
    return out


def main() -> int:
    if len(sys.argv) < 3:
        print(__doc__)
        return 2
    proj, epi = sys.argv[1], sys.argv[2]

    recipe = ROOT / "projects" / proj / "images" / epi / "scene" / "recipe"
    if not recipe.is_dir():
        print(f"[없음] {recipe}")
        return 1

    # 샷 이름을 파일에서 모은다 — 파일 이름이 유일한 계보다
    shots = sorted({p.name.split("_")[0] for p in recipe.glob("S*_*.png")
                    if not p.name.endswith(".stale.png") and ".stale_" not in p.name})
    staging = load_staging(proj, epi)

    out_dir = ROOT / "artifact" / "20260825_camera_authority"
    out_dir.mkdir(parents=True, exist_ok=True)
    rel = f"../../projects/{proj}/images/{epi}/scene/recipe"

    rows = []
    counted = {"샷": 0, "b가 이김": 0, "a가 이김": 0, "판별불가": 0}
    for sh in shots:
        counted["샷"] += 1
        cells = []
        sizes = {}
        for suf, label, note in STAGES:
            f = recipe / f"{sh}_{suf}.png"
            if f.exists():
                sizes[suf] = _sha(f)
                cells.append(
                    f'<figure><img loading="lazy" src="{rel}/{sh}_{suf}.png" alt="{sh} {suf}">'
                    f'<figcaption><b>{label}</b><br><span class="note">{note}</span></figcaption></figure>')
            else:
                cells.append(f'<figure class="miss"><div class="ph">없음</div>'
                             f'<figcaption><b>{label}</b></figcaption></figure>')

        # 승자 판별 — sel 은 이긴 것의 복사본이라 바이트가 같다
        win = "판별불가"
        if "sel" in sizes:
            if sizes.get("b") == sizes["sel"]:
                win = "B (자유 변주)"
                counted["b가 이김"] += 1
            elif sizes.get("a") == sizes["sel"]:
                win = "A (기본 구도)"
                counted["a가 이김"] += 1
            elif sizes.get("fix") == sizes["sel"]:
                # fix 가 sel 을 덮었다 — 이 샷은 a/b 중 무엇이 이겼는지 파일로는 못 가른다
                win = "fix 가 덮음 (A/B 판별 불가)"
                counted["판별불가"] += 1
            else:
                counted["판별불가"] += 1

        st = staging.get(sh, {})
        cd = html.escape(st.get("camera_direction", "") or "(shot_staging 기록 없음)")
        meta = " · ".join(x for x in [
            f"framing_scale: {html.escape(st['framing_scale'])}" if st.get("framing_scale") else "",
            f"perspective: {html.escape(st['perspective'])}" if st.get("perspective") else "",
        ] if x)

        rows.append(f"""
  <section class="shot">
    <h2>{sh} <span class="win">승자 = {html.escape(win)}</span></h2>
    <div class="dir">
      <div class="dir-h">shot_staging 이 쓴 camera_direction — <b>이것이 지켜졌는지 보십시오</b></div>
      <p>{cd}</p>
      {f'<div class="meta">{meta}</div>' if meta else ''}
    </div>
    <div class="strip">{''.join(cells)}</div>
  </section>""")

    doc = f"""<meta charset="utf-8">
<title>카메라 권한 — 상류가 정한 앵글이 최종까지 남는가</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
  :root {{ color-scheme: light dark; }}
  body {{ font: 15px/1.65 -apple-system, "Apple SD Gothic Neo", sans-serif;
         margin: 0; padding: 24px; background: #0f1115; color: #e6e8ee; }}
  h1 {{ font-size: 22px; margin: 0 0 4px; }}
  .lede {{ color: #a8b0c0; max-width: 76ch; margin: 0 0 20px; }}
  .lede b {{ color: #ffd479; }}
  .tally {{ display: flex; gap: 18px; flex-wrap: wrap; margin: 0 0 28px;
            padding: 12px 16px; background: #171a21; border-radius: 10px; }}
  .tally div span {{ color: #8f97a8; font-size: 13px; }}
  .tally div b {{ display: block; font-size: 20px; }}
  .shot {{ margin: 0 0 40px; padding: 0 0 28px; border-bottom: 1px solid #222732; }}
  h2 {{ font-size: 17px; margin: 0 0 10px; }}
  .win {{ font-size: 13px; font-weight: 400; color: #7fd1a0; margin-left: 8px; }}
  .dir {{ background: #171a21; border-left: 3px solid #ffd479;
          padding: 12px 16px; border-radius: 0 8px 8px 0; margin: 0 0 14px; }}
  .dir-h {{ font-size: 12px; color: #8f97a8; margin-bottom: 6px; }}
  .dir p {{ margin: 0; }}
  .meta {{ margin-top: 8px; font-size: 12px; color: #8f97a8; }}
  .strip {{ display: grid; grid-template-columns: repeat(5, 1fr); gap: 10px; }}
  @media (max-width: 1100px) {{ .strip {{ grid-template-columns: repeat(2, 1fr); }} }}
  figure {{ margin: 0; }}
  figure img {{ width: 100%; border-radius: 6px; display: block; background: #000; }}
  figcaption {{ font-size: 12px; color: #c3c9d6; padding: 6px 2px 0; }}
  .note {{ color: #8f97a8; }}
  .miss .ph {{ aspect-ratio: 16/9; display: grid; place-items: center;
               background: #171a21; border-radius: 6px; color: #565e6e; }}
</style>
<h1>카메라 권한 — 상류가 정한 앵글이 최종까지 남는가</h1>
<p class="lede">
  <code>shot_staging</code> 이 씬 원문·인물 관계·시선을 다 보고 <code>camera_direction</code> 을 씁니다.
  그런데 그 뒤 두 곳이 <b>명문으로 그 권위를 풉니다</b> —
  B-roll 후보는 「CAMERA &amp; FRAME is a start, <b>not a lock</b>」,
  최종 cine 는 「the source still <b>does not fix where the camera stands</b>」.
  왼쪽 지시문을 읽고, 다섯 장이 그것을 지켰는지 보십시오.
</p>
<div class="tally">
  <div><span>샷</span><b>{counted['샷']}</b></div>
  <div><span>B(자유 변주)가 이김</span><b>{counted['b가 이김']}</b></div>
  <div><span>A(기본 구도)가 이김</span><b>{counted['a가 이김']}</b></div>
</div>
{''.join(rows)}
"""
    (out_dir / "index.html").write_text(doc, encoding="utf-8")
    ip = lan_ip()
    print(f"샷 {counted['샷']} · B승 {counted['b가 이김']} · A승 {counted['a가 이김']}")
    print(f"→ {out_dir}/index.html")
    print(f"→ http://{ip}:8940/artifact/20260825_camera_authority/")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
