"""Opus+Sol 이중 판정 254샷 갤러리 — 무엇을 왜 골랐는지 한 화면에서 본다.

샷마다 후보 롤 전부 → 두 심판이 각자 고른 것 → 합산 결과 → (심각 판정이면)
i2i 수정본 → 확정본을 나란히 놓고, 두 심판의 판독문을 그대로 붙인다.
요약하지 않는 것은 심판이 무엇을 보고 그렇게 말했는지가 곧 검증 대상이기
때문이다. 두 심판이 갈린 샷은 따로 걸러 볼 수 있게 한다.
"""
from __future__ import annotations

import html
import json
import re
from pathlib import Path

ROOT = Path(__file__).resolve().parent.parent
PILOT = ROOT / "artifact/20260806_opus_pilot"
WEB_RECIPE = ("/projects/e716bafb-24bb-42b7-aea0-fdb383844ee8/images"
              "/d6a9aa85-b75e-400c-980c-4ee7e876a15b/scene/recipe")
WEB_PILOT = "/artifact/20260806_opus_pilot/images"
OUT = PILOT / "gallery.html"
SEV = {"critical": "치명", "major": "중대", "minor": "경미"}
NAME = {"claude-opus": "Opus 5", "gpt": "GPT-5.6 Sol"}


def esc(x) -> str:
    return html.escape(str(x or ""))


def key(s: str):
    m = re.findall(r"\d+", s)
    return (int(m[0]), int(m[1])) if len(m) >= 2 else (9999, 9999)


def card(stem: str, v: dict) -> str:
    sel = (v.get("selected_roll") or "").upper()
    prior = (v.get("prior_selected_roll") or "").upper()
    s = v.get("select") or {}
    changed = bool(sel and prior and sel != prior)
    split = s.get("agreed") is False
    repaired = bool(v.get("final_is_repaired"))

    tags = []
    if changed:
        tags.append(f'<span class="t chg">선정 {esc(prior)}→{esc(sel)}</span>')
    elif sel:
        tags.append(f'<span class="t">선정 {esc(sel)} 유지</span>')
    if split:
        pw = s.get("per_model_winner") or {}
        who = " / ".join(f"{NAME.get(m, m)}→{w}" for m, w in pw.items())
        tags.append(f'<span class="t split">두 심판 갈림 · {esc(who)}</span>')
    if repaired:
        tags.append('<span class="t fix">i2i 수정</span>')
    if v.get("prior_had_fix"):
        tags.append('<span class="t old">어제 i2i 있었음</span>')

    # 합산 점수 한 줄
    adj, vio = s.get("adjusted") or {}, s.get("violation_counts") or {}
    score = " · ".join(
        f'<b class="{"w" if l == sel else ""}">{esc(l)} {adj[l]:+.2f}</b>'
        f'<span class="vi">위반 {vio.get(l, 0)}</span>'
        for l in sorted(adj))

    shots = []
    for lb in ("a", "b", "c"):
        if not (ROOT / WEB_RECIPE.lstrip("/") / f"{stem}_{lb}.png").exists():
            continue
        on = sel.lower() == lb
        shots.append(
            f'<figure class="cand{" sel" if on else ""}">'
            f'<img loading="lazy" src="{WEB_RECIPE}/{stem}_{lb}.png">'
            f'<figcaption>{lb.upper()}{" ★확정" if on else ""}</figcaption></figure>')
    if repaired:
        # 수정본 파일명은 두 갈래다 — 부분 수정(i2i)이면 `_opusfix`, Opus 가
        # 통째로 다시 그리라고 한 샷이면 `_opusregen`. json 값을 그대로 쓴다.
        fx = Path(v.get("fixed") or "").name
        shots.append(f'<figure class="fixi"><img loading="lazy" '
                     f'src="{WEB_PILOT}/{fx}">'
                     f'<figcaption>{"다시 생성" if "regen" in fx else "i2i 수정"}'
                     f'</figcaption></figure>')
    shots.append(f'<figure class="final"><img loading="lazy" '
                 f'src="{WEB_PILOT}/{stem}_final.png">'
                 f'<figcaption>확정</figcaption></figure>')

    # 심판별 판독
    body = ""
    for m, r in (s.get("per_model") or {}).items():
        rows = ""
        for rd in r.get("readings") or []:
            lb = (rd.get("label") or "").upper()
            hv = rd.get("hard_violations") or []
            rows += (
                f'<div class="rd"><b class="{"w" if lb == (r.get("winner") or "").upper() else ""}">'
                f'{esc(lb)} · {esc(rd.get("score"))}점</b>'
                + (f'<span class="hv">{esc(" / ".join(hv))}</span>' if hv else "")
                + f'<p><i>방향</i> {esc(rd.get("direction"))}</p>'
                f'<p><i>공간</i> {esc(rd.get("built_space"))}</p>'
                f'<p><i>요소</i> {esc(rd.get("entities"))}</p></div>')
        body += (f'<h3>{esc(NAME.get(m, m))} — 선택 {esc(r.get("winner"))}</h3>'
                 f'{rows}<p class="why">{esc(r.get("why"))}</p>')

    for i in (v.get("critique") or {}).get("issues") or []:
        sv = i.get("severity", "")
        body += (f'<p class="iss s-{esc(sv)}"><b>{SEV.get(sv, esc(sv))}</b> '
                 f'{esc(i.get("issue_ko"))}</p>')

    for d in ("forward", "reverse"):
        blk = (v.get("rejudge") or {}).get(d)
        if not blk:
            continue
        rows = "".join(
            f'<div class="rd"><b>{esc(x.get("label"))} · {esc(x.get("score"))}점</b>'
            f'<p>{esc(x.get("verdict_ko"))}</p></div>'
            for x in blk.get("verdicts") or [])
        body += (f'<h3>수정 후 재비교 · {"정방향" if d == "forward" else "좌우 바꿈"}'
                 f' — 승 {esc(blk.get("winner"))}</h3>{rows}')

    cls = " ".join(filter(None, ["card", "f-chg" if changed else "",
                                 "f-fix" if repaired else "",
                                 "f-split" if split else ""]))
    n_iss = len((v.get("critique") or {}).get("issues") or [])
    return f"""<section class="{cls}" id="{esc(stem)}">
<h2>{esc(stem)} {''.join(tags)}</h2>
<div class="sc">합산 {score}</div>
<div class="row">{''.join(shots)}</div>
<details><summary>두 심판 판독 · 지적 {n_iss}건{' · 재비교' if v.get('rejudge') else ''}</summary>
{body}</details></section>"""


def main() -> None:
    res = json.loads((PILOT / "full_dual.json").read_text("utf-8"))
    res = res.get("results", res)
    items = sorted(((k, v) for k, v in res.items() if isinstance(v, dict)),
                   key=lambda kv: key(kv[0]))
    n_chg = sum(1 for _, v in items if v.get("selected_roll")
                and v.get("prior_selected_roll")
                and v["selected_roll"] != v["prior_selected_roll"])
    n_fix = sum(1 for _, v in items if v.get("final_is_repaired"))
    n_spl = sum(1 for _, v in items if (v.get("select") or {}).get("agreed") is False)

    OUT.write_text(f"""<meta charset="utf-8">
<title>Opus+Sol 이중 판정 — 254샷 선정·수정·확정</title>
<style>
:root{{color-scheme:dark}}
body{{background:#111;color:#ddd;font:14px/1.65 -apple-system,"Apple SD Gothic Neo",sans-serif;margin:0;padding:16px}}
h1{{font-size:20px;margin:0 0 4px}}
.sum{{color:#9ab;margin-bottom:14px}}
.bar{{position:sticky;top:0;background:#111f;padding:9px 0;z-index:9;border-bottom:1px solid #333;margin-bottom:14px}}
button{{background:#222;color:#ccc;border:1px solid #444;border-radius:6px;padding:6px 12px;margin-right:6px;cursor:pointer;font-size:13px}}
button.on{{background:#2a4a6a;color:#fff;border-color:#4a7aaa}}
.card{{border:1px solid #2a2a2a;border-radius:10px;padding:12px;margin-bottom:16px;background:#171717}}
.card.f-fix{{border-color:#7a5a2a}} .card.f-split{{border-color:#6a3a6a}}
h2{{font-size:15px;margin:0 0 6px;display:flex;gap:6px;align-items:center;flex-wrap:wrap}}
h3{{font-size:13px;color:#9ab;margin:14px 0 6px;border-top:1px solid #2a2a2a;padding-top:10px}}
.t{{font-size:11px;font-weight:400;background:#2a2a2a;color:#aaa;border-radius:4px;padding:2px 7px}}
.t.chg{{background:#2a4a6a;color:#bde}} .t.fix{{background:#6a4a1a;color:#fda}}
.t.split{{background:#4a2a5a;color:#e9f}} .t.old{{background:#333;color:#888}}
.sc{{font-size:12px;color:#889;margin-bottom:8px}}
.sc b{{color:#bbb;font-weight:600;margin-left:10px}} .sc b.w{{color:#6da}}
.vi{{color:#755;margin-left:5px;font-size:11px}}
.row{{display:flex;gap:8px;overflow-x:auto;padding-bottom:4px}}
figure{{margin:0;flex:0 0 300px}}
figure img{{width:100%;border-radius:6px;display:block;background:#000}}
figcaption{{font-size:11px;color:#888;margin-top:3px;text-align:center}}
.cand.sel img{{outline:2px solid #4a9;outline-offset:1px}}
.cand.sel figcaption{{color:#6da}}
.fixi img{{outline:2px solid #b83}} .fixi figcaption{{color:#fc8}}
.final img{{outline:2px solid #58f}} .final figcaption{{color:#9bf}}
details{{margin-top:10px}} summary{{cursor:pointer;color:#89a;font-size:12px}}
.rd{{background:#1e1e1e;border-radius:6px;padding:8px 10px;margin:6px 0;font-size:12.5px}}
.rd b{{color:#ccc}} .rd b.w{{color:#6da}}
.rd p{{margin:3px 0;color:#bbb}} .rd i{{color:#789;font-style:normal;margin-right:5px}}
.hv{{color:#e88;font-size:11px;margin-left:8px}}
.why{{color:#aab;font-size:12.5px;margin:6px 0 0;padding-left:10px;border-left:2px solid #345}}
.iss{{margin:5px 0;font-size:12.5px;color:#bbb}}
.s-critical b{{color:#f77}} .s-major b{{color:#fb7}} .s-minor b{{color:#8a8}}
</style>
<h1>Opus 5 + GPT-5.6 Sol 이중 판정 — 254샷</h1>
<div class="sum">원본 후보(A/B/C)를 두 심판이 각자 읽고 점수를 합산해 확정 ·
심각하다고 본 샷만 i2i 로 수정한 뒤 <b>다시 비교</b> ·
확정 <b>{len(items)}</b> / 이전과 다른 선택 <b>{n_chg}</b> /
두 심판 갈림 <b>{n_spl}</b> / i2i 수정 <b>{n_fix}</b></div>
<div class="bar">
<button class="on" data-f="">전체 {len(items)}</button>
<button data-f="f-chg">이전과 다른 선택 {n_chg}</button>
<button data-f="f-split">두 심판 갈림 {n_spl}</button>
<button data-f="f-fix">i2i 수정 {n_fix}</button>
</div>
{''.join(card(k, v) for k, v in items)}
<script>
document.querySelectorAll('.bar button').forEach(b=>b.onclick=()=>{{
  document.querySelectorAll('.bar button').forEach(x=>x.classList.remove('on'));
  b.classList.add('on');
  const f=b.dataset.f;
  document.querySelectorAll('.card').forEach(c=>
    c.style.display=(!f||c.classList.contains(f))?'':'none');
}});
</script>""", "utf-8")
    print(f"{OUT}\n  샷 {len(items)} / 다른 선택 {n_chg} / 갈림 {n_spl} / 수정 {n_fix}")


if __name__ == "__main__":
    main()
