#!/usr/bin/env python3
"""②선정 판정 A/B 갤러리 — **누가 무엇을 왜 골랐나** (2026-08-29).

PR #43 이후 판정은 **Gemini 정순 + Grok 역순 2콜**이고, 갈리면
`combine_select_verdicts` 가 정규화 점수 합 + 하드위반 합집합으로 정한다.
그 계약이 실제로 어떻게 작동했는지는 **후보 두 장을 나란히 놓고** 봐야
읽힌다.

## 이 갤러리가 보여 주는 것

  · 샷 텍스트 (무엇을 그리라고 했나)
  · 후보 A / B 원본, 그리고 최종 `_sel` · `_cine`
  · **두 모델이 각각 무엇을 골랐나** + 점수 + **하드위반**
  · route (agree / combined / 단독 fallback)

★기록에서만 읽는다 — 유료 호출 0.

usage:  build_judge_ab_gallery.py <project_id> <episode_id> [출력디렉토리]
"""
from __future__ import annotations

import html
import json
import pathlib
import subprocess
import sys

sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[2]))
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent))
import _db  # noqa: E402
ROOT = pathlib.Path(__file__).resolve().parents[3]


def esc(x) -> str:
    return html.escape(str(x if x is not None else ""))


def _shot_texts(episode_id: str) -> dict:
    """샷 텍스트 SOT — `scene_still.shot_description`.

    ★접속 정보는 설정에서만 온다(`_db`) — 비밀번호를 도구에 안 적는다.
    """
    out = {}
    for si, shi, txt in _db.rows(
            "SELECT scene_index, shot_index, "
            "replace(coalesce(shot_description,''), E'\\n',' ') "
            "FROM scene_still WHERE episode_id = :ep",
            {"ep": episode_id}):
        out[f"S{si}sh{shi}"] = txt
    return out


def main() -> int:
    argv = [a for a in sys.argv[1:] if not a.startswith("--")]
    if len(argv) < 2:
        print(__doc__)
        return 2
    project_id, episode_id = argv[0], argv[1]
    outdir = pathlib.Path(argv[2]) if len(argv) > 2 else (
        ROOT / "artifact" / "20260829_judge_ab_gallery")

    from app.core.config import settings

    rdir = (pathlib.Path(settings.projects_dir) / project_id / "images"
            / episode_id / "scene" / "recipe")
    recs = json.loads((rdir / "records.json").read_text())
    texts = _shot_texts(episode_id)

    rows = [(k, r) for k, r in recs.items()
            if isinstance(r, dict) and r.get("cross_model_order")]
    rows.sort()
    if not rows:
        print("★내 조회로는 cross_model_order 를 가진 샷을 못 찾았다 — "
              "「없다」로 읽지 마라. 이 주행이 PR #43 이전일 수 있다")
        return 1

    P: list[str] = ['<meta charset="utf-8">',
                    "<title>판정 A/B — 누가 무엇을 골랐나</title>", """<style>
:root{color-scheme:dark}
body{background:#111;color:#ddd;margin:0;padding:28px 32px;max-width:1600px;
 font:15px/1.65 -apple-system,'Apple SD Gothic Neo','Noto Sans KR',sans-serif}
h1{font-size:24px;margin:0 0 4px} .n{color:#8a8a8a;font-size:13px}
h2{font-size:19px;margin:34px 0 6px;border-bottom:1px solid #333;padding-bottom:6px}
.shot{border:1px solid #2a2a2a;border-radius:9px;padding:16px;margin:18px 0;background:#161616}
.txt{color:#e8e8e8;font-size:15px;margin:4px 0 12px;padding:10px 12px;
 background:#1c1c1c;border-left:3px solid #4a7;border-radius:4px}
table{border-collapse:collapse;margin:10px 0;font-size:14px}
td,th{border:1px solid #333;padding:6px 10px;text-align:left}
th{background:#1b1b1b}
.win{color:#6fcf6f;font-weight:700} .lose{color:#888}
.hv{color:#ff8f6f;font-weight:700} .zero{color:#666}
.imgs{display:flex;gap:12px;flex-wrap:wrap;margin-top:12px;align-items:flex-start}
.imgs figure{margin:0;max-width:330px}
.imgs img{width:100%;border-radius:6px;border:2px solid #333;display:block}
.imgs .picked img{border-color:#6fcf6f}
.imgs figcaption{font-size:12px;color:#999;margin-top:5px}
.imgs .picked figcaption{color:#6fcf6f;font-weight:700}
.route{display:inline-block;font-size:12px;padding:2px 9px;border-radius:99px;
 border:1px solid #444;background:#1e1e1e;margin-left:8px}
.agree{color:#6fcf6f;border-color:#2c5} .comb{color:#e8c26a;border-color:#a83}
</style>"""]

    n_agree = sum(1 for _, r in rows
                  if r["cross_model_order"]["route"] == "cross_slot_agree")
    P.append("<h1>②선정 판정 — 후보 A/B 와 <b>누가 무엇을 골랐나</b></h1>")
    P.append(f'<p class="n">에피소드 {esc(episode_id)} · 샷 {len(rows)}개 · '
             f'두 심판이 합의 {n_agree} · 갈림 {len(rows)-n_agree} · '
             f'기록에서만 읽음(유료 호출 0)</p>')

    # ── 모델별 하드위반 총계 — 이 판의 핵심 관찰 ──────────────────
    tot: dict = {}
    for _, r in rows:
        for s in r["cross_model_order"]["slots"]:
            who = "Gemini" if "gemini" in s["model"] else "Grok"
            n = s.get("normalized") or {}
            tot[who] = tot.get(who, 0) + sum(
                len(rr.get("hard_violations") or [])
                for rr in (n.get("readings") or []))
    P.append("<h2>모델별 하드위반 총계</h2>")
    P.append("<table><tr><th>모델</th><th>하드위반</th><th></th></tr>")
    for who, v in sorted(tot.items()):
        note = ("★한 건도 안 냈다 — 그러면 「위반 합집합」이 다른 모델 "
                "것만으로 정해진다") if v == 0 else ""
        P.append(f'<tr><td>{esc(who)}</td>'
                 f'<td class="{"zero" if v==0 else "hv"}">{v}건</td>'
                 f"<td>{esc(note)}</td></tr>")
    P.append("</table>")

    P.append("<h2>샷별</h2>")
    for tag, r in rows:
        c = r["cross_model_order"]
        sel = str(r.get("selected") or "")
        route = c["route"]
        cls = "agree" if route == "cross_slot_agree" else "comb"
        P.append('<div class="shot">')
        P.append(f'<h3 style="margin:0">{esc(tag)}'
                 f'<span class="route {cls}">{esc(route)}</span>'
                 f'<span class="n"> · 최종 선정 <b>{esc(sel)}</b></span></h3>')
        if texts.get(tag):
            P.append(f'<div class="txt">{esc(texts[tag])}</div>')

        # 두 심판이 각각 무엇을 봤나
        P.append("<table><tr><th>심판</th><th>본 순서</th><th>고른 것</th>"
                 "<th>점수</th><th>하드위반</th></tr>")
        for s in c["slots"]:
            who = "Gemini 3.1 Pro" if "gemini" in s["model"] else "Grok 4.6"
            n = s.get("normalized") or {}
            if not s.get("ok"):
                P.append(f'<tr><td>{esc(who)}</td><td>{esc(s["order"])}</td>'
                         '<td colspan="3">응답 실패 — 이 슬롯은 빠졌다</td></tr>')
                continue
            w = str(n.get("winner") or "")
            sc = " · ".join(
                f'{esc(v.get("label"))} {esc(v.get("score"))}'
                for v in (n.get("verdicts") or []))
            hv = []
            for rr in (n.get("readings") or []):
                lst = rr.get("hard_violations") or []
                if lst:
                    hv.append(f'{esc(rr.get("label"))}: '
                              + "; ".join(esc(x) for x in lst))
            hv_html = ("<br>".join(hv) if hv
                       else '<span class="zero">없음</span>')
            P.append(f'<tr><td>{esc(who)}</td><td>{esc(s["order"])}</td>'
                     f'<td class="{"win" if w==sel else "lose"}">{esc(w)}</td>'
                     f"<td>{sc}</td><td>{hv_html}</td></tr>")
        # 합산 결과 (갈린 경우만 의미가 있다)
        if route == "cross_slot_combined":
            fin = " · ".join(f'{esc(v.get("label"))} {esc(v.get("score"))}'
                             for v in (r.get("verdicts") or []))
            P.append(f'<tr><td colspan="3"><b>합산</b></td>'
                     f'<td colspan="2">{fin}</td></tr>')
        P.append("</table>")

        # 이미지 — 후보 둘, 최종, 변환
        P.append('<div class="imgs">')
        for lab in ("a", "b"):
            f = rdir / f"{tag}_{lab}.png"
            if not f.is_file():
                continue
            rel = f.resolve().relative_to(ROOT)
            picked = " picked" if lab.upper() == sel else ""
            cap = f"후보 {lab.upper()}" + (" ← 선정" if picked else "")
            P.append(f'<figure class="{picked.strip()}">'
                     f'<a href="/{esc(rel)}" target="_blank">'
                     f'<img src="/{esc(rel)}" loading="lazy"></a>'
                     f"<figcaption>{esc(cap)}</figcaption></figure>")
        for lab, cap in (("cine", "최종 (cine 변환)"),):
            f = rdir / f"{tag}_{lab}.png"
            if not f.is_file():
                continue
            rel = f.resolve().relative_to(ROOT)
            P.append(f'<figure><a href="/{esc(rel)}" target="_blank">'
                     f'<img src="/{esc(rel)}" loading="lazy"></a>'
                     f"<figcaption>{esc(cap)}</figcaption></figure>")
        P.append("</div></div>")

    outdir.mkdir(parents=True, exist_ok=True)
    (outdir / "index.html").write_text("\n".join(P), encoding="utf-8")
    rel = (outdir / "index.html").resolve().relative_to(ROOT)
    print(f"만듦: {outdir/'index.html'}")
    print(f"주소: http://192.168.231.91:8940/{rel}")
    return 0


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