#!/usr/bin/env python3
"""여섯 모델 A/B 판정 결과 갤러리 (2026-08-29).

`judge_model_bakeoff.py` 가 남긴 calls 를 읽어 후보 두 장·샷 지문·
모델별 판정을 한 쪽에 놓는다. ★기록에서만 읽는다 — 유료 호출 0.

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

import html
import json
import pathlib
import re
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]

ORDER = ["Gemini 3.1 Pro", "Grok 4.6", "GPT-5.6 Sol", "Claude Opus 5",
         "Kimi K3", "GLM-5.3 Flash"]
LIVE = {"Gemini 3.1 Pro", "Grok 4.6"}      # 지금 프로덕션 판정 두 자리


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


def _shot_text(episode_id: str, tag: str) -> str:
    """샷 텍스트 SOT — `scene_still.shot_description`.

    ★접속 정보는 설정에서만 온다(`_db`) — 비밀번호를 도구에 안 적는다.
    """
    m = re.match(r"S(\d+)sh(\d+)$", tag)
    if not m:
        return ""
    got = _db.one_col(
        "SELECT replace(coalesce(shot_description,''), E'\\n',' ') "
        "FROM scene_still WHERE episode_id = :ep "
        "AND scene_index = :si AND shot_index = :shi",
        {"ep": episode_id, "si": int(m.group(1)), "shi": int(m.group(2))})
    return str(got[0]) if got else ""


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

    from app.core.config import settings

    src = outdir / "calls_merged.json"
    if not src.is_file():
        src = outdir / "calls.json"
    if not src.is_file():
        raise SystemExit(f"★판정 기록을 못 찾았다: {outdir}")
    rows = [r for r in json.loads(src.read_text()) if r.get("winner")]
    rows.sort(key=lambda r: ORDER.index(r["name"])
              if r["name"] in ORDER else 99)

    rdir = (pathlib.Path(settings.projects_dir) / project_id / "images"
            / episode_id / "scene" / "recipe")
    rec = json.loads((rdir / "records.json").read_text())[tag]
    sel = str(rec.get("selected") or "")
    cm = rec.get("cross_model_order") or {}
    prompt = ((rec.get("roll_prompts") or {}).get("A") or "")
    keys = [ln.strip() for ln in prompt.splitlines()
            if re.search(r"FRAMING SCALE|one hanging fluorescent|"
                         r"A single fluorescent|upside down", ln)]

    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:1500px;
 font:15px/1.65 -apple-system,'Apple SD Gothic Neo','Noto Sans KR',sans-serif}
h1{font-size:25px;margin:0 0 6px} h2{font-size:19px;margin:32px 0 8px;
 border-bottom:1px solid #333;padding-bottom:6px}
.n{color:#8a8a8a;font-size:13px}
.txt{color:#e8e8e8;margin:10px 0;padding:11px 13px;background:#1b1b1b;
 border-left:3px solid #4a7;border-radius:4px}
.txt code{color:#ffd88a;font-size:13px}
table{border-collapse:collapse;margin:12px 0;font-size:14px;width:100%}
td,th{border:1px solid #333;padding:7px 10px;text-align:left;
 vertical-align:top}
th{background:#1b1b1b} .win{color:#6fcf6f;font-weight:700}
.hv{color:#ff8f6f} .zero{color:#666}
.live{display:inline-block;font-size:11px;padding:1px 7px;border-radius:99px;
 border:1px solid #2c5;color:#6fcf6f;margin-left:6px}
.imgs{display:flex;gap:16px;flex-wrap:wrap;margin:14px 0}
.imgs figure{margin:0;max-width:560px} .imgs img{width:100%;border-radius:6px;
 border:2px solid #333;display:block}
.imgs .picked img{border-color:#6fcf6f}
.imgs figcaption{font-size:13px;color:#999;margin-top:6px}
.imgs .picked figcaption{color:#6fcf6f;font-weight:700}
.warn{background:#2a1c14;border-left:3px solid #b86;padding:11px 13px;
 border-radius:4px;margin:12px 0}
</style>"""]

    P.append("<h1>같은 A/B 를 여섯 모델에게 물었다</h1>")
    P.append(f'<p class="n">{esc(tag)} · 정순 1회씩 · 창구는 OpenRouter 하나로 '
             f'통일 · 프로덕션 선정 <b>{esc(sel)}</b> '
             f'(route={esc(cm.get("route"))}, 슬롯별 {esc(cm.get("slot_winner"))})'
             "</p>")

    st = _shot_text(episode_id, tag)
    if st:
        P.append(f'<div class="txt"><b>샷 텍스트</b><br>{esc(st)}</div>')
    if keys:
        P.append('<div class="txt"><b>프롬프트가 못박은 것</b><br>'
                 + "<br>".join(f"<code>{esc(k)}</code>" for k in keys)
                 + "</div>")

    P.append("<h2>후보 두 장</h2><div class='imgs'>")
    for lab in ("A", "B"):
        f = rdir / f"{tag}_{lab.lower()}.png"
        if not f.is_file():
            continue
        rel = f.resolve().relative_to(ROOT)
        pick = " picked" if lab == sel else ""
        P.append(f'<figure class="{pick.strip()}">'
                 f'<a href="/{esc(rel)}" target="_blank">'
                 f'<img src="/{esc(rel)}"></a><figcaption>후보 {lab}'
                 + (" ← 최종 선정" if pick else "") + "</figcaption></figure>")
    P.append("</div>")

    n = len(rows)
    same = sum(1 for r in rows if r["winner"] == sel)
    P.append("<h2>누가 무엇을 골랐나</h2>")
    P.append(f'<p class="n">성공 {n}판 중 프로덕션과 같은 것을 고른 것 '
             f'<b>{same}</b>. ★모델당 <b>한 판</b>이다 — 백분율로 쓰지 않는다.'
             "</p>")
    P.append("<table><tr><th>모델</th><th>고른 것</th><th>점수 A / B</th>"
             "<th>하드위반</th><th>시간</th><th>추론토큰</th></tr>")
    for r in rows:
        s = r.get("scores") or {}
        hv = []
        for lab, lst in (r.get("hard_by_label") or {}).items():
            for x in lst:
                hv.append(f"<b>후보 {esc(lab)}</b> — {esc(x)}")
        badge = '<span class="live">지금 도는 자리</span>' \
            if r["name"] in LIVE else ""
        P.append(
            f'<tr><td>{esc(r["name"])}{badge}<br>'
            f'<span class="n">{esc(r["model"])}</span></td>'
            f'<td class="win">{esc(r["winner"])}</td>'
            f'<td>{esc(s.get("A"))} / {esc(s.get("B"))}</td>'
            + (f'<td class="hv">{"<br>".join(hv)}</td>' if hv
               else '<td class="zero">한 건도 안 냄</td>')
            + f'<td>{r["duration_s"]:.1f}초</td>'
            f'<td>{esc((r.get("usage") or {}).get("reasoning_tokens"))}</td>'
            "</tr>")
    P.append("</table>")

    # ── 프로덕션 조립부로 실제 돈 판 (있으면) ──────────────────────
    smoke_f = outdir / "production_smoke.json"
    if smoke_f.is_file():
        rows_s = json.loads(smoke_f.read_text())
        P.append("<h2>바꾼 뒤 — 프로덕션 조립부로 실제 판정</h2>")
        P.append('<p class="n">위 표는 창구를 OpenRouter 로 통일해 잰 것이라 '
                 "「프로덕션 alias 로도 된다」의 증거가 아니다. 그래서 "
                 "<code>make_gemini_judge_fn</code> 을 그대로 불러 태웠다.</p>")
        for r in rows_s:
            if r.get("tag") != tag:
                continue
            P.append("<table><tr><th>슬롯</th><th>본 순서</th><th>고른 것</th>"
                     "<th>하드위반</th></tr>")
            for s in r.get("slots") or []:
                hv = []
                for lab, lst in (s.get("hard_by_label") or {}).items():
                    for x in lst:
                        hv.append(f"<b>후보 {esc(lab)}</b> — {esc(x)}")
                P.append(f'<tr><td>{esc(s.get("model"))}</td>'
                         f'<td>{esc(s.get("order"))}</td>'
                         f'<td class="win">{esc(s.get("winner"))}</td>'
                         + (f'<td class="hv">{"<br>".join(hv)}</td>' if hv
                            else '<td class="zero">한 건도 안 냄</td>')
                         + "</tr>")
            P.append(f'<tr><td colspan="2"><b>합산</b></td>'
                     f'<td class="win">{esc(r.get("winner"))}</td>'
                     f'<td>route={esc(r.get("route"))} · '
                     f'{r.get("duration_s")}초 · 물리 쌍 '
                     f'<code>{esc(r.get("physical"))}</code></td></tr>')
            P.append("</table>")
            for n in r.get("_notes") or []:
                P.append(f'<p class="n">★{esc(n)}</p>')

    P.append("<h2>읽을 때 조심할 것</h2>")
    P.append('<div class="warn">'
             "· <b>표본은 샷 하나·모델당 한 판이다.</b> 누가 더 낫다고 "
             "말하기엔 모자란다 — 여기서 읽을 것은 「같은 그림을 놓고 "
             "무엇을 결함으로 올리는지가 갈린다」는 사실이다.<br>"
             "· <b>시간은 프로덕션 시간이 아니다.</b> 여기서는 GPT·Gemini·"
             "Opus 도 OpenRouter 를 거쳤다. 프로덕션은 각자 네이티브 키다.<br>"
             "· reasoning 파라미터를 안 보냈다 — 각 모델 기본값으로 돌았다.<br>"
             "· Gemini 와 GLM 은 1차 시도에서 응답 본문을 못 읽어 다시 던졌다"
             "(둘 다 2차 성공). 일시 오류였다."
             "</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)
    ip = subprocess.run(["ipconfig", "getifaddr", "en0"],
                        capture_output=True, text=True).stdout.strip()
    print(f"만듦: {outdir/'index.html'}")
    print(f"주소: http://{ip}:8940/{rel}")
    return 0


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