#!/usr/bin/env python3
"""마커 맵 모델 비교 갤러리 — 어느 모델이 부채꼴을 제대로 그리는가.

`ab_marker_map_engine.py` 가 남긴 `result.json` 과 PNG 를 읽어 한 쪽으로
묶는다. **판정 딱지는 프로덕션 검사기가 낸 것**이고 이 빌더는 옮겨 적기만
한다 — 눈으로 다시 매기지 않는다.

프로덕션이 실제로 겪은 두 장(gpt-image-2 반려본 · grok 통과본)도 같이
넣는다. 실험만 보면 「실험실에서만 되는 것」과 구분이 안 된다.

usage:
  build_marker_map_gallery.py [--dir artifact/20260825_marker_map_engine_ab]
"""
import argparse
import base64
import html
import json
import shutil
from pathlib import Path

ROOT = Path("/Users/manta/Documents/Projects/TheRoad-I1")
DEFAULT = ROOT / "artifact" / "20260825_marker_map_engine_ab"
STATE = json.loads(
    (ROOT / "scratchpad" / "minimal_e2e_state.json").read_text())
PID, EID = STATE["project_id"], STATE["episode_id"]

CSS = """
:root{color-scheme:dark}
*{box-sizing:border-box}
body{margin:0;padding:28px;background:#12141a;color:#e8e6e3;
 font:15px/1.65 -apple-system,BlinkMacSystemFont,"Apple SD Gothic Neo",sans-serif}
h1{font-size:22px;margin:0 0 6px}
h2{font-size:16px;margin:34px 0 12px;color:#9fb4d0;
 border-bottom:1px solid #262a34;padding-bottom:6px}
.sub{color:#8b93a3;font-size:13px;margin-bottom:22px}
.grid{display:grid;gap:16px;grid-template-columns:repeat(auto-fill,minmax(320px,1fr))}
.card{background:#191c24;border:1px solid #262a34;border-radius:10px;
 overflow:hidden;display:flex;flex-direction:column}
.card img{width:100%;display:block;background:#0d0f14;cursor:zoom-in}
.meta{padding:10px 12px;font-size:13px}
.name{font-weight:600;margin-bottom:4px;word-break:break-all}
.why{color:#9aa3b2;font-size:12px;margin-top:5px;line-height:1.5}
.tag{display:inline-block;padding:2px 9px;border-radius:999px;
 font-size:11.5px;font-weight:600}
.ok{background:#12331f;color:#5fd68a;border:1px solid #1f5433}
.no{background:#3a1a1c;color:#ff8b8b;border:1px solid #5e2b2e}
.err{background:#3a2f14;color:#e3c169;border:1px solid #5e4a1f}
table{border-collapse:collapse;margin:6px 0 4px;font-size:14px}
td,th{padding:6px 14px 6px 0;text-align:left;vertical-align:top}
th{color:#9fb4d0;font-weight:600}
.note{background:#171a21;border-left:3px solid #3c4657;padding:12px 16px;
 border-radius:0 8px 8px 0;margin:14px 0;font-size:13.5px;color:#c3c9d4}
"""

JS = """
document.addEventListener('click',e=>{
 if(e.target.tagName!=='IMG')return;
 const o=document.createElement('div');
 o.style.cssText='position:fixed;inset:0;background:#000d;z-index:99;'+
  'display:flex;align-items:center;justify-content:center;cursor:zoom-out';
 const i=document.createElement('img');
 i.src=e.target.src;
 i.style.cssText='max-width:96vw;max-height:96vh;object-fit:contain';
 o.appendChild(i);o.onclick=()=>o.remove();document.body.appendChild(o);
});
"""


def b64(p: Path) -> str:
    """그림을 페이지에 심는다 — 파일이 흩어져 있어도 한 쪽으로 본다."""
    return ("data:image/png;base64,"
            + base64.b64encode(p.read_bytes()).decode("ascii"))


def card(img: Path, name: str, verdict: str, why: str = "") -> str:
    cls = {"통과": "ok", "반려": "no"}.get(verdict, "err")
    return (
        f'<div class="card"><img src="{b64(img)}" alt="{html.escape(name)}">'
        f'<div class="meta"><div class="name">{html.escape(name)}</div>'
        f'<span class="tag {cls}">{html.escape(verdict)}</span>'
        + (f'<div class="why">{html.escape(why)}</div>' if why else "")
        + "</div></div>")


def main() -> int:
    ap = argparse.ArgumentParser(description=__doc__)
    ap.add_argument("--dir", default=str(DEFAULT),
                    help="쉼표로 여러 판을 준다 — 한 쪽에 모은다")
    ap.add_argument("--out", default="",
                    help="index.html 을 쓸 곳 (기본: 첫 판 디렉토리)")
    ap.add_argument("--serve-dir", default="")
    args = ap.parse_args()

    # ★여러 판을 **한 쪽에** 모은다. 판이 갈려 있으면 「어느 표가 유효한가」를
    #  사람이 머리로 맞춰야 하고, 그러다 무효한 값을 유효한 것으로 읽는다.
    dirs = []
    for part in args.dir.split(","):
        p = Path(part.strip())
        if not p.is_absolute():
            p = ROOT / p
        if (p / "result.json").exists():
            dirs.append(p)
    if not dirs:
        raise SystemExit(f"★result.json 이 있는 판이 없다: {args.dir}")
    d = Path(args.out) if args.out else dirs[0]
    if not d.is_absolute():
        d = ROOT / d
    d.mkdir(parents=True, exist_ok=True)

    rows = []
    res = {"tag": ""}
    for src in dirs:
        r = json.loads((src / "result.json").read_text())
        res["tag"] = res["tag"] or r.get("tag", "")
        for row in r["rows"]:
            row["_dir"] = src
            # ★호출 자체가 안 된 칸은 **집계에서 뺀다.** 그것은 「못 그렸다」가
            #  아니라 「불러 보지도 못했다」이고, 섞으면 모델을 억울하게 깎는다.
            row["_counted"] = row.get("ok") is not None
            rows.append(row)

    # 모델별 집계 — 개수만 세고 판정은 옮겨 적는다
    per = {}
    for r in rows:
        if not r["_counted"]:
            continue
        a = per.setdefault(r["model"], [0, 0])
        a[1] += 1
        if r.get("ok"):
            a[0] += 1

    imgs = ROOT / "projects" / PID / "images" / EID / "conti"
    base_map = (ROOT / "projects" / PID / "images" / "background_chain"
                / "canon_bg_wet_alley_intersection_map.png")

    out = [f'<meta charset="utf-8"><title>마커 맵 모델 비교</title>',
           f"<style>{CSS}</style>",
           "<h1>마커 맵 — 어느 모델이 부채꼴을 제대로 그리는가</h1>",
           f'<div class="sub">샷 {html.escape(res["tag"])} · '
           f'같은 문안·같은 바탕 지도 · 판정은 <b>프로덕션 검사기</b>'
           f'(lane_marker_map_check)가 낸 값</div>']

    out.append('<div class="note"><b>부채꼴이 무엇인가</b> — 야외 갈래길 '
               '샷은 위에서 내려다본 지도에 카메라 자리와 시야각을 마커로 '
               '그려 참조로 쓴다. 이 샷의 기하는 카메라 (0.88, 0.88) '
               '남동쪽 → 보는 지점 (0.50, 0.50) 한가운데다. 부채꼴이 '
               '반대로 열리면 쓰러진 사람 대신 등 뒤 빈 골목을 찍는 셈이 '
               '된다.</div>')

    out.append("<h2>모델별 통과율</h2><table>"
               "<tr><th>모델</th><th>통과</th></tr>")
    for m, (ok, n) in sorted(per.items(), key=lambda kv: -kv[1][0] / max(kv[1][1], 1)):
        out.append(f"<tr><td>{html.escape(m)}</td>"
                   f"<td>{ok}/{n}</td></tr>")
    out.append("</table>")

    out.append("<h2>바탕 지도 (마커 없음)</h2><div class=grid>")
    if base_map.exists():
        out.append(card(base_map, base_map.name, "원본"))
    out.append("</div>")

    out.append("<h2>프로덕션이 실제로 겪은 것</h2><div class=grid>")
    stale = sorted(imgs.glob("lane_marker_map_S2sh1.stale_*.png"))
    if stale:
        out.append(card(stale[-1], "gpt-image-2 (프로덕션 · 3회 소진)",
                        "반려",
                        "카메라 시야각 부채꼴이 피사체 방향(남동)이 아닌 "
                        "반대(북서)로 열려 있다"))
    live = imgs / "lane_marker_map_S2sh1.png"
    if live.exists():
        out.append(card(live, "x-ai/grok-imagine-image-2.0 (프로덕션 · 후퇴 후 3회차)",
                        "통과", "모델 후퇴가 발화해 통과 — 스텝이 partial "
                        "에서 completed 로 바뀌었다"))
    out.append("</div>")

    out.append("<h2>실험 — 같은 재료, 모델만 교체</h2><div class=grid>")
    for r in sorted(rows, key=lambda x: (not x["_counted"], x["model"],
                                         x["round"])):
        f = r.get("file")
        src = r.get("_dir") or d
        if not f or not (src / f).exists():
            out.append(
                f'<div class="card"><div class="meta">'
                f'<div class="name">{html.escape(r["model"])} r{r["round"]}'
                f'</div><span class="tag err">불려 보지도 못함</span>'
                f'<div class="why">{html.escape(str(r.get("error") or ""))}'
                f"<br><b>집계에서 뺀다</b> — 그림 실력이 아니라 호출 방식 "
                f"문제였다.</div></div></div>")
            continue
        v = "통과" if r.get("ok") else ("반려" if r.get("ok") is False
                                       else "판정 실패")
        out.append(card(src / f, f'{r["model"]} r{r["round"]}', v,
                        str(r.get("details_ko") or r.get("error") or "")))
    out.append("</div>")
    out.append(f"<script>{JS}</script>")

    page = d / "index.html"
    page.write_text("\n".join(out), encoding="utf-8")
    print(f"갤러리 → {page}  ({page.stat().st_size:,} bytes)")

    if args.serve_dir:
        dst = Path(args.serve_dir)
        if not dst.is_absolute():
            dst = ROOT / dst
        dst.mkdir(parents=True, exist_ok=True)
        shutil.copy2(page, dst / "index.html")
        print(f"배포 → {dst}/index.html")
    return 0


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