"""재생성 전후 대조 갤러리 — 지목 25건을 나란히, 나머지는 전체 목록으로.

개악 여부는 **대조 없이 판정하지 않는다**. 8/6 산출을 왼쪽에, 새 산출을
오른쪽에 놓고 같은 크기로 건다. 판정문·critique·수정 프롬프트도 전후를
함께 실어 "왜 그 후보가 뽑혔는가"까지 되짚을 수 있게 한다.

before = artifact/20260807_재생성_대조/before_20260806/ (재생성 전 백업)
after  = projects/<proj>/images/<epi>/scene/recipe/       (새 산출)

사용
  .venv/bin/python build_rerun_gallery.py
"""
from __future__ import annotations

import html
import json
from pathlib import Path
from typing import Any, Dict, List, Optional

ROOT = Path(__file__).resolve().parent.parent
PROJ = "e716bafb-24bb-42b7-aea0-fdb383844ee8"
EPI = "d6a9aa85-b75e-400c-980c-4ee7e876a15b"
AFTER_REL = f"projects/{PROJ}/images/{EPI}/scene/recipe"
BEFORE_REL = "artifact/20260807_재생성_대조/before_20260806"
OUT = ROOT / "artifact/20260807_재생성_대조"

# 2026-08-07 육안 판정 25건 — 축별로 무엇을 봐야 하는지 함께 건다.
FLAGGED: List[tuple] = [
    ("A 자동차 내부", "S13sh3", "핸들 림 이중 / 인물이 조수석"),
    ("A 자동차 내부", "S13sh4", "좁은 실내 구도"),
    ("A 자동차 내부", "S15sh3", "좁은 실내 구도"),
    ("A 자동차 내부", "S15sh5", "룸미러 반사가 광학적으로 불가능"),
    ("A 자동차 내부", "S18sh1", "좌석·운전석 위치"),
    ("A 자동차 내부", "S18sh5", "좌석이 돌아가 있다"),
    ("B i2i 개악", "S112sh8", "팔이 여러 개"),
    ("B i2i 개악", "S98sh6", "머리 스타일이 달라짐"),
    ("B i2i 개악", "S97sh5", "눈의 황금빛이 사라짐"),
    ("B i2i 개악", "S97sh1", "등판을 뚫고 나오는데 피가 사라짐"),
    ("B i2i 개악", "S85sh6", "순찰정이 더 이상해짐"),
    ("B i2i 개악", "S76sh5", "도축장 걸쇠 등이 사라짐"),
    ("B i2i 개악", "S74sh8", "옷이 완전히 달라짐"),
    ("B i2i 개악", "S73sh4", "갑자기 둘 다 비니를 씀"),
    ("B i2i 개악", "S67sh9", "후레쉬가 사라짐"),
    ("B i2i 개악", "S61sh12", "수정 전이 여러 사람이 잘 보여 더 좋음"),
    ("C 선정 판정", "S104sh7", "B 가 훨씬 좋은데 A 를 골랐다"),
    ("D 표현·해석", "S88sh3", "'고정된 자세'가 차렷 마네킹 일렬로"),
    ("D 표현·해석", "S93sh8", "사람이 서로 붙은 것 같다"),
    ("D 표현·해석", "S93sh10", "사람이 서로 붙은 것 같다"),
    ("D 표현·해석", "S93sh5", "강영란이 잘 표현 안 됨"),
    ("D 표현·해석", "S79sh1", "'밝은 조명'인데 촬영용 조명 기구가 중앙에"),
    ("D 표현·해석", "S57sh3", "비석의 '수리영' 글자가 퍼즐처럼"),
    ("D 표현·해석", "S1sh7", "들어가는 장면인지 나가는 장면인지 이상"),
    ("D 표현·해석", "S2sh5", "전체가 코미디 슬랩스틱처럼"),
    ("E 휴대폰", "S42sh4", "폰이 손 없이 뜨고 화면에 주유기가 비침"),
    ("F 지폐", "S62sh4", "지폐 국적 — 미국 달러"),
]

CSS = """
:root{--bg:#12141a;--card:#1b1e26;--fg:#e8eaf0;--dim:#9aa0ae;--line:#2c3140;
--new:#3ddc84;--old:#7d8595;--warn:#ffb454;--bad:#ff6b6b}
*{box-sizing:border-box}
body{margin:0;padding:24px;background:var(--bg);color:var(--fg);
font-family:-apple-system,BlinkMacSystemFont,"Apple SD Gothic Neo",sans-serif;
line-height:1.6}
h1{font-size:22px;margin:0 0 6px}
.sub{color:var(--dim);font-size:13px;margin-bottom:20px;max-width:860px}
.axis{font-size:15px;color:var(--warn);margin:26px 0 10px;font-weight:700}
.card{background:var(--card);border:1px solid var(--line);border-radius:10px;
padding:16px;margin-bottom:20px}
.card h2{font-size:16px;margin:0 0 3px}
.note{color:var(--warn);font-size:12.5px;margin-bottom:12px}
.pair{display:grid;grid-template-columns:1fr 1fr;gap:14px}
@media(max-width:900px){.pair{grid-template-columns:1fr}}
.side img{width:100%;border-radius:6px;display:block;border:2px solid transparent}
.side.after img{border-color:var(--new)}
.side.before img{border-color:var(--old)}
.side .lab{font-size:12.5px;margin-top:6px;color:var(--dim)}
.side.after .lab{color:var(--new)}
.missing{padding:40px;text-align:center;color:var(--dim);border:1px dashed
var(--line);border-radius:6px;font-size:13px}
.meta{margin-top:12px;font-size:12px;color:var(--dim)}
.meta b{color:var(--fg)}
.flag{display:inline-block;font-size:11px;padding:1px 7px;border-radius:9px;
margin-left:6px;background:#3a2a2a;color:var(--bad)}
details{margin-top:8px}
summary{cursor:pointer;color:var(--dim);font-size:12.5px}
pre{white-space:pre-wrap;font-size:11.5px;color:var(--dim);
background:#161920;padding:10px;border-radius:6px;overflow-x:auto}
.all{display:grid;grid-template-columns:repeat(auto-fill,minmax(230px,1fr));
gap:10px}
.all a{color:var(--dim);font-size:11.5px;text-decoration:none}
.all img{width:100%;border-radius:5px;display:block}
"""


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


def load_records(path: Path) -> Dict[str, Any]:
    try:
        return json.loads(path.read_text("utf-8"))
    except Exception:
        return {}


def side(kind: str, rel: str, stem: str, label: str) -> str:
    p = ROOT / rel / f"{stem}_sel.png"
    if not p.exists():
        return (f'<div class="side {kind}"><div class="missing">'
                f'{esc(label)} — 산출 없음</div></div>')
    return (f'<div class="side {kind}">'
            f'<img src="/{rel}/{stem}_sel.png" loading="lazy">'
            f'<div class="lab">{esc(label)}</div></div>')


def rec_meta(r: Optional[Dict[str, Any]], tag: str) -> str:
    if not isinstance(r, dict):
        return ""
    bits = [f"선정 <b>{esc(r.get('selected'))}</b>",
            f"경로 {esc(r.get('ref_mode'))}"]
    if r.get("repair_mode"):
        bits.append(f"수정 {esc(r.get('repair_mode'))}")
    rj = r.get("fix_rejudge") or {}
    if rj:
        bits.append("재판정 " + ("수정본 승" if rj.get("fix_won") else "원본 유지"))
    flags = []
    if r.get("needs_reshoot"):
        flags.append('<span class="flag">재촬영 대상</span>')
    if r.get("initial_roll_all_fail"):
        flags.append('<span class="flag">초기 롤 전부 실패</span>')
    if r.get("fix_ref_count"):
        bits.append(f"편집 참조 {esc(r['fix_ref_count'])}장")
    out = f'<div class="meta">{tag}: ' + " · ".join(bits) + "".join(flags) + "</div>"
    issues = [i.get("issue_ko", "") for i in (r.get("critique") or {}).get("issues") or []]
    if issues:
        out += ("<details><summary>결함 지적 " + str(len(issues)) + "건</summary><pre>"
                + esc("\n".join(f"· {x}" for x in issues)) + "</pre></details>")
    return out


def main() -> None:
    before = load_records(ROOT / BEFORE_REL / "records.json")
    after = load_records(ROOT / AFTER_REL / "records.json")

    cards, cur_axis = [], None
    for axis, stem, note in FLAGGED:
        if axis != cur_axis:
            cards.append(f'<div class="axis">{esc(axis)}</div>')
            cur_axis = axis
        cards.append(
            f'<div class="card"><h2>{esc(stem)}</h2>'
            f'<div class="note">지목: {esc(note)}</div>'
            f'<div class="pair">'
            + side("before", BEFORE_REL, stem, "재생성 전 (8/6)")
            + side("after", AFTER_REL, stem, "재생성 후 (8/7)")
            + "</div>"
            + rec_meta(before.get(stem), "전")
            + rec_meta(after.get(stem), "후")
            + "</div>")

    # 전체 목록 — 지목 밖 샷도 훑을 수 있게
    flagged = {s for _, s, _ in FLAGGED}
    all_stems = sorted(
        (k for k, v in after.items()
         if "::" not in k and isinstance(v, dict) and v.get("selected")),
        key=lambda s: (len(s), s))
    tiles = "".join(
        f'<a href="/{AFTER_REL}/{s}_sel.png" target="_blank">'
        f'<img src="/{AFTER_REL}/{s}_sel.png" loading="lazy"><div>{esc(s)}'
        + (" ★" if s in flagged else "") + "</div></a>"
        for s in all_stems
        if (ROOT / AFTER_REL / f"{s}_sel.png").exists())

    reshoot = [s for s in all_stems if (after.get(s) or {}).get("needs_reshoot")]

    doc = (
        '<meta charset="utf-8">\n'
        "<title>재생성 전후 대조 — 2026-08-07</title>\n"
        f"<style>{CSS}</style>\n"
        "<h1>재생성 전후 대조</h1>\n"
        '<div class="sub">왼쪽이 8/6 산출, 오른쪽이 수정 뒤 새 산출이다. '
        "개악 여부는 대조 없이 판정하지 않는다 — 두 장을 나란히 놓고 본다. "
        f"전체 {len(all_stems)}장 중 지목 {len(FLAGGED)}건을 먼저 건다."
        + (f' · <b>재촬영 대상 {len(reshoot)}건</b>: {esc(", ".join(reshoot))}'
           if reshoot else "")
        + "</div>\n"
        + "".join(cards)
        + f'<div class="axis">전체 {len(all_stems)}장</div>'
        + f'<div class="all">{tiles}</div>'
    )
    OUT.mkdir(parents=True, exist_ok=True)
    (OUT / "index.html").write_text(doc, encoding="utf-8")
    print(f"→ {OUT/'index.html'}  (지목 {len(FLAGGED)} / 전체 {len(all_stems)})")


if __name__ == "__main__":
    main()
