"""지목 27샷 G+Q 재검증 갤러리 (2026-08-10 사용자 지시).

각 샷에 대해 네 판정 소스를 나란히 놓는다:
  · 기존 프로덕션 — Opus(+Sol 조건부 이중) 판정: records.json 의
    selected/verdicts/dual(있으면 per_model_winner — margin-skip 으로
    둘째가 생략된 샷은 dual 이 없다=Opus 단독)
  · 8/9 재선정 실측 — Gemini / Qwen (실험 계약 SELECT_SYS, 같은 후보):
    artifact/20260809_final_eval_gallery/results.json
  · 이번 G+Q 새 루틴(프로덕션 계약 v7·관찰→취합·재판정):
    artifact/20260810_gq_flagged_recheck/summary.json + 산출 파일

그리고 수정·최종 차이:
  · 기존 최종( recipe {stem}_sel.png ) vs 이번 최종( recheck {stem}_sel.png )
  · 이번 수정본( recheck {stem}_fix.png ) — 재판정이 원본을 유지한 샷은
    "개악 판정 — 미채택" 배지

이미지는 8940 루트 서버 기준 절대 경로(/projects/…, /artifact/…)로 링크.
사용:  .venv/bin/python build_gq_recheck_gallery.py
"""
from __future__ import annotations

import html as _html
import json
from pathlib import Path

ROOT = Path(__file__).resolve().parent.parent
PROJ = "e716bafb-24bb-42b7-aea0-fdb383844ee8"
EPI = "d6a9aa85-b75e-400c-980c-4ee7e876a15b"
RECIPE = ROOT / f"projects/{PROJ}/images/{EPI}/scene/recipe"
FINAL_EVAL = ROOT / "artifact/20260809_final_eval_gallery/results.json"
RECHECK = ROOT / "artifact/20260810_gq_flagged_recheck"
OUT_HTML = RECHECK / "index.html"

REGEN_0809 = {"S1sh5", "S1sh7", "S96sh1", "S96sh4", "S98sh2", "S98sh5",
              "S98sh6"}
DB_UNSELECTED = {"S93sh10", "S18sh5"}  # 2026-08-10 확인: 현재 대표 아님

# 지목 사유 — 2026-08-10 사용자 원문 요약 (표시용 데이터)
FLAG_REASONS = {
    "S112sh8": "수정본이 팔이 여러 개",
    "S104sh7": "평가가 잘못됨 — B가 훨씬 좋음",
    "S98sh6": "수정 후 머리 스타일 달라짐 — 이전샷 참조 여부 조사 필요",
    "S97sh5": "수정 후 눈의 황금빛이 사라짐",
    "S97sh1": "등판 관통 장면이 이상 + 수정 후 피가 없어짐",
    "S93sh10": "장면 표현 이상 — 사람이 붙은 것 같음",
    "S93sh8": "장면 표현 이상 — 사람이 붙은 것 같음, 분석 필요",
    "S93sh5": "인물 표현 부족, 수정 후 흰머리 — 변형 모습 혼입 여부?",
    "S88sh3": "고정 자세가 차렷 일렬이 아님 — 마네킹 일렬 같음",
    "S85sh6": "수정 후 순찰정이 더 이상함",
    "S79sh1": "밝은 조명 지시인데 영화 촬영용 조명이 중앙에 등장",
    "S76sh5": "수정 후 걸쇠 등이 사라짐 — 수정 전이 더 좋음",
    "S74sh8": "수정 후 옷이 완전히 달라짐",
    "S73sh4": "수정 후 갑자기 둘 다 비니를 씀",
    "S67sh9": "수정 후 후레쉬가 사라짐",
    "S62sh4": "지폐 관련",
    "S61sh12": "수정 전이 더 좋음 — 여러 사람이 잘 보임",
    "S57sh3": "수정 후 비석 글자가 퍼즐처럼 흩어짐",
    "S42sh4": "휴대폰 화면에 주유기가 투명하게 비침 + 휴대폰 공중 부양"
              " — 상세 리뷰 필요",
    "S18sh1": "자동차 내부 복잡 구도 — 실험 연구 필요",
    "S18sh5": "자동차 내부 복잡 구도 — 실험 연구 필요",
    "S15sh5": "자동차 내부 복잡 구도 — 실험 연구 필요",
    "S15sh3": "자동차 내부 복잡 구도 — 실험 연구 필요",
    "S13sh4": "자동차 내부 복잡 구도 — 실험 연구 필요",
    "S13sh3": "자동차 내부 복잡 구도 — 실험 연구 필요",
    "S1sh7": "들어가는 장면인지 나가는 장면인지 매우 이상",
    "S2sh5": "전체가 슬랩스틱 코미디처럼 어색",
}
TAGS = list(FLAG_REASONS)


def e(s) -> str:
    return _html.escape(str(s if s is not None else ""))


def url(p: Path) -> str:
    return "/" + str(p.relative_to(ROOT))


def img(p: Path, label: str, badge: str = "") -> str:
    if not p.exists():
        return f'<div class="miss">{e(label)}: 파일 없음</div>'
    b = f'<span class="badge">{e(badge)}</span>' if badge else ""
    return (f'<figure><a href="{url(p)}" target="_blank">'
            f'<img loading="lazy" src="{url(p)}"></a>'
            f'<figcaption>{e(label)}{b}</figcaption></figure>')


def verdict_block(title: str, winner, body: str, cls: str = "") -> str:
    return (f'<div class="verdict {cls}"><h4>{e(title)} — '
            f'<b>{e(winner or "?")}</b></h4>'
            f'<div class="body">{e(body)}</div></div>')


def prod_verdict_text(rec: dict) -> str:
    parts = []
    for v in rec.get("verdicts") or []:
        parts.append(f"[{v.get('label')}:{v.get('score')}] "
                     f"{v.get('verdict_ko', '')}")
    return "\n".join(parts)


def main() -> None:
    records = json.loads((RECIPE / "records.json").read_text("utf-8"))
    fe = json.loads(FINAL_EVAL.read_text("utf-8"))
    summary = {s["stem"]: s for s in json.loads(
        (RECHECK / "summary.json").read_text("utf-8"))}

    rows = []
    for tag in TAGS:
        rec = records.get(tag) or {}
        s = summary.get(tag) or {}
        f_g = (fe.get(tag) or {}).get("gemini") or {}
        f_q = (fe.get(tag) or {}).get("qwen") or {}
        gq = s.get("gq") if isinstance(s.get("gq"), dict) else {}
        badges = []
        if tag in REGEN_0809:
            badges.append("★8/9 재생성본 — 지목 당시 이미지와 다름")
        if tag in DB_UNSELECTED:
            badges.append("현재 DB 선정 해제 샷")
        if "오류" in s:
            badges.append(f"이번 실행 오류: {s['오류']}")

        dual = rec.get("dual") or {}
        prod_title = ("기존 프로덕션 " + (
            f"Opus+Sol {dual.get('per_model_winner')}"
            if dual else "Opus 단독(margin-skip)"))
        # 이번 실행: 재판정 문자열 "B (수정본승=True)" — summary 계약
        rj = str(s.get("재판정", "—"))
        fix_won = "수정본승=True" in rj
        new_final_note = ("수정본 채택" if fix_won else
                          "원본 유지(수정 개악 판정)" if s.get("수정함")
                          else "수정 없음")

        cand_imgs = "".join(
            img(RECHECK / f"{tag}_{lab}.png", f"후보 {lab.upper()}",
                badge=" ".join(
                    x for x in [
                        "기존선정" if rec.get("selected") == lab.upper()
                        else "",
                        "이번선정" if s.get("카나리_선정") == lab.upper()
                        else "",
                    ] if x))
            for lab in ("a", "b", "c")
            if (RECHECK / f"{tag}_{lab}.png").exists())

        fix_p = RECHECK / f"{tag}_fix.png"
        rows.append(f"""
<section class="shot" id="{tag}">
<h2>{tag} <small>{e(FLAG_REASONS[tag])}</small></h2>
{"".join(f'<div class="warn">{e(b)}</div>' for b in badges)}
<div class="grid">
<div class="col"><h3>후보 롤</h3>{cand_imgs}</div>
<div class="col"><h3>최종 비교</h3>
{img(RECIPE / f"{tag}_sel.png", "기존 최종 (프로덕션)")}
{img(RECHECK / f"{tag}_sel.png", "이번 최종 (G+Q 새 루틴)")}
{img(fix_p, "이번 수정본",
     badge="" if fix_won else "개악 판정 — 미채택") if fix_p.exists() else ""}
</div>
<div class="col wide"><h3>판정 4소스</h3>
{verdict_block(prod_title, rec.get("selected"), prod_verdict_text(rec))}
{verdict_block("8/9 Gemini 재선정(실험 계약)", f_g.get("winner"),
               f_g.get("why", ""))}
{verdict_block("8/9 Qwen 재선정(실험 계약)", f_q.get("winner"),
               f_q.get("why", ""))}
{verdict_block("이번 G+Q(프로덕션 계약 v7)", s.get("카나리_선정"),
               f"route={gq.get('route')} gap={gq.get('gap')} · "
               f"점수={s.get('판정_점수')} · Qwen 관찰 {s.get('qwen_관찰')}건 "
               f"→ 결함 채택 {s.get('결함')}건 · 재판정={rj} → "
               f"{new_final_note}", cls="new")}
</div>
</div>
</section>""")

    n_flip = sum(1 for t in TAGS
                 if summary.get(t, {}).get("카나리_선정")
                 and summary[t]["카나리_선정"] != (records.get(t) or {}).get(
                     "selected"))
    head = f"""<meta charset="utf-8">
<title>지목 27샷 G+Q 재검증 (2026-08-10)</title>
<style>
body{{font-family:sans-serif;margin:16px;background:#111;color:#ddd}}
h2{{border-top:2px solid #444;padding-top:12px}}
h2 small{{color:#e6b34c;font-size:.65em;margin-left:10px}}
.grid{{display:flex;gap:14px;flex-wrap:wrap}}
.col{{flex:1;min-width:280px}} .col.wide{{flex:2;min-width:420px}}
figure{{display:inline-block;margin:4px;max-width:230px;vertical-align:top}}
figure img{{max-width:100%;border:1px solid #333}}
figcaption{{font-size:.75em;color:#aaa}}
.badge{{background:#2b6;color:#000;padding:1px 5px;border-radius:4px;
margin-left:5px;font-weight:bold}}
.verdict{{background:#1c1c1c;border:1px solid #333;border-radius:6px;
padding:8px;margin:6px 0}}
.verdict.new{{border-color:#2b6}}
.verdict h4{{margin:0 0 4px}} .verdict .body{{font-size:.8em;
white-space:pre-wrap;color:#bbb;max-height:180px;overflow-y:auto}}
.warn{{background:#553;color:#fd7;padding:4px 8px;margin:4px 0;
border-radius:4px;font-size:.85em}}
.miss{{color:#f66;font-size:.8em}}
</style>
<h1>지목 27샷 — G+Q 새 검증 루틴 재실행 대조</h1>
<p>판정 소스 4: 기존 프로덕션(Opus±Sol) · 8/9 Gemini/Qwen 재선정(실험
계약) · 이번 G+Q(프로덕션 계약 v7 + 관찰→취합→수정→재판정). 선정
뒤집힘 {n_flip}/{len(TAGS)}샷. 이미지 클릭=원본.</p>
<p>바로가기: {" ".join(f'<a href="#{t}">{t}</a>' for t in TAGS)}</p>
"""
    OUT_HTML.write_text(head + "".join(rows), "utf-8")
    print(f"갤러리: {OUT_HTML}")


if __name__ == "__main__":
    main()
