#!/usr/bin/env python3
"""정정 반영 fresh E2E(1b4a975b) 검증 리포트 생성 → v16corr_report.html (8897).

검사: ①철제 계단 정정 상류→최종 관통 ②발명 어휘 전수 스캔+씬 원문 근거 대조
③FP 12장 갤러리(경찰서 FP=TASK 2 스타일 참조). 커밋 금지.
"""
import html
import json
import re
import shutil
from pathlib import Path

ROOT = Path("/Users/manta/Documents/Projects/TheRoad-I1")
PID = "1b4a975b-f4e5-46d7-8b28-876071d19907"
EID = "d5c3bffb-2ae7-4446-a17d-2221fea713b5"
CP = ROOT / "projects" / PID / "checkpoints" / "episodes" / EID
FP_DIR = ROOT / "projects" / PID / "episodes" / EID / "images" / "floor_plan"
OUT_DIR = ROOT / "scratchpad" / "forest_exp"
IMG_OUT = OUT_DIR / "out" / "fp_v16corr"

VOCAB = ["amber", "fog", "mist", "damp", "wet", "concrete", "cement",
         r"45 ?m", "ominous", "steel", "콘크리트", "시멘트", "철제"]

def load(step):
    return json.load(open(CP / step / "manifest.json", encoding="utf-8"))

def t2i_texts(shot):
    out = []
    for v in shot.get("t2i_variations") or []:
        if isinstance(v, dict) and v.get("t2i_prompt"):
            out.append((v.get("variant_label", "?"), v["t2i_prompt"]))
    return out

def ctx_hits(text, kw, pad=90):
    res = []
    for m in re.finditer(kw, text, re.I):
        a, b = max(0, m.start() - pad), min(len(text), m.end() + pad)
        res.append("…" + text[a:b] + "…")
    return res

# ── 데이터 수집 ──
detail = load("scene_detail")
shots = detail["data"]["scenes"]
segs = {s["scene_index"]: s for s in load("scene_save")["data"]["segments"]}

vocab_rows = []   # (kw, count, [(sid, ctx, verdict)])
for kw in VOCAB:
    k = kw.replace(" ?", "")
    entries = []
    for sh in shots:
        sid = f"S{sh['scene_index']}_{sh.get('_shot_index')}"
        for label, t in t2i_texts(sh):
            for c in ctx_hits(t, kw):
                entries.append((sid, label, c))
    vocab_rows.append((k, entries))

# 씬 원문 근거 요약
def scene_ev(idx, kws):
    seg = segs.get(idx)
    if not seg:
        return {}
    txt = seg["text"]
    return {w: len(re.findall(w, txt)) for w in kws if re.findall(w, txt)}

EV = {
    24: scene_ev(24, ["소나기", " 비", "젖"]),
    26: scene_ev(26, ["안개", "포구", "바다", "소나기"]),
    30: scene_ev(30, ["안개", "바다"]),
    4: scene_ev(4, ["콘크리트", "시멘트", "옥탑", "옥상"]),
    11: scene_ev(11, ["철제", "계단", "콘크리트"]),
}

# 정정 관통 증거 — 상류 shot_extract 계단 서술
extract_raw = json.dumps(load("shot_extract")["data"], ensure_ascii=False)
stair_upstream = ctx_hits(extract_raw, "철제 증축 계단", pad=120)
staging_raw = json.dumps(load("shot_staging")["data"], ensure_ascii=False)
stair_staging = ctx_hits(staging_raw, r"steel stair|철제 증축", pad=120)[:4]

# 최종 t2i 계단 발췌
stair_final = []
for sh in shots:
    sid = f"S{sh['scene_index']}_{sh.get('_shot_index')}"
    for label, t in t2i_texts(sh):
        for c in ctx_hits(t, r"steel (stair|steps)", pad=150):
            stair_final.append((sid, label, c))

# FP 이미지 복사
IMG_OUT.mkdir(parents=True, exist_ok=True)
fp_imgs = sorted(FP_DIR.glob("*.png"))
for p in fp_imgs:
    shutil.copy2(p, IMG_OUT / p.name)

# ── HTML ──
def esc(s):
    return html.escape(str(s))

VERDICT = {
    "amber": ("0건", "ok", "이전 v15 run 86건 → 소멸 유지"),
    "fog": ("전건 근거", "ok",
            f"S26(원문 안개×{EV[26].get('안개',0)})·S30(안개×{EV[30].get('안개',0)}) — 무근거 0"),
    "mist": ("근거", "ok", "S30 바다/안개 씬"),
    "damp": ("0건", "ok", ""),
    "wet": ("근거", "ok", f"S24 소나기 씬(원문 소나기×{EV[24].get('소나기',0)})"),
    "concrete": ("잔존 1건(비계단)", "warn",
                 "S4_1 'concrete roof deck' — 옥상 바닥 재질어, 원문 무근거(S#4 콘크리트 0회). "
                 "계단 아님. 이전 audit의 low_parapet 콘크리트와 동류 — 사용자 판단 후보"),
    "cement": ("0건", "ok", ""),
    "45m": ("0건", "ok", ""),
    "ominous": ("0건", "ok", ""),
    "steel": ("정정 반영", "good", "S4_1/S11_3/S11_4 — 철제 증축 계단 정정이 최종 t2i까지 관통"),
    "콘크리트": ("0건", "ok", "(한글 표기 기준)"),
    "시멘트": ("0건", "ok", ""),
    "철제": ("0건(영문으로 반영)", "ok", "최종 t2i는 영문 — steel로 반영됨"),
}

rows = []
for k, entries in vocab_rows:
    v = VERDICT.get(k, ("", "", ""))
    cls = {"ok": "#e7f5e7", "good": "#d9ecff", "warn": "#fff3cd"}.get(v[1], "#fff")
    ctxs = "".join(
        f"<div class='ctx'><b>[{esc(sid)} {esc(lb)}]</b> {esc(c)}</div>"
        for sid, lb, c in entries[:10])
    rows.append(
        f"<tr style='background:{cls}'><td><code>{esc(k)}</code></td>"
        f"<td>{len(entries)}</td><td>{esc(v[0])}</td><td>{esc(v[2])}"
        f"{ctxs}</td></tr>")

stair_up_html = "".join(f"<div class='ctx'>{esc(c)}</div>" for c in stair_upstream)
stair_st_html = "".join(f"<div class='ctx'>{esc(c)}</div>" for c in stair_staging)
stair_fin_html = "".join(
    f"<div class='ctx'><b>[{esc(sid)} {esc(lb)}]</b> {esc(c)}</div>"
    for sid, lb, c in stair_final)

imgs_html = "".join(
    f"<figure><img src='out/fp_v16corr/{esc(p.name)}' loading='lazy'>"
    f"<figcaption>{esc(p.name)}"
    f"{' ★ 경찰서 (TASK 2 스타일 참조)' if 'police' in p.name else ''}"
    f"</figcaption></figure>"
    for p in fp_imgs)

doc = f"""<!doctype html><html lang=ko><head><meta charset=utf-8>
<title>v16corr 정정 반영 fresh E2E 검증 리포트</title>
<style>
body{{font-family:'Apple SD Gothic Neo',sans-serif;margin:24px;max-width:1400px}}
table{{border-collapse:collapse;width:100%;font-size:13px}}
td,th{{border:1px solid #ccc;padding:6px 8px;vertical-align:top;text-align:left}}
.ctx{{font-family:Menlo,monospace;font-size:11px;color:#333;background:#fafafa;
     border-left:3px solid #bbb;margin:4px 0;padding:3px 6px;white-space:pre-wrap}}
figure{{display:inline-block;width:31%;margin:1%}}
img{{width:100%;border:1px solid #ddd}}
figcaption{{font-size:12px;text-align:center}}
.badge{{display:inline-block;padding:2px 8px;border-radius:4px;font-size:12px}}
h2{{border-bottom:2px solid #333;padding-bottom:4px;margin-top:36px}}
</style></head><body>
<h1>정정 반영 fresh E2E 검증 리포트 (v16corr · 2026-07-08)</h1>
<p><b>프로젝트</b> 금월도 E2E v16 정정검증 20260708 — pid <code>{PID}</code> /
ep <code>{EID}</code><br>
<b>실행</b> 53스텝 순차(텍스트 39 + floor_plan_render + FPDEP 13) 전부 완주, 실패 0 ·
scene_detail 54/54 completed(redo 불요) · 씬 스틸 이미지 0<br>
<b>정정 PUT 타이밍</b> 프로젝트 생성 <u>직후, 문서 업로드 이전</u> —
steel-retrofit-stair 1건 active</p>

<h2>1. 철제 계단 정정 — 상류→최종 관통 증거</h2>
<h3>① shot_extract (최상류 소비 사이트)</h3>{stair_up_html}
<h3>② shot_staging</h3>{stair_st_html}
<h3>③ 최종 scene_detail t2i</h3>{stair_fin_html}
<p><b>판정: 정정이 3개 주입 사이트를 거쳐 최종 t2i 프롬프트까지 관통.</b>
콘크리트 계단 재발명 0 (이전 run 유일 잔존이던 S13 concrete stair 소멸 —
staging의 "rather than a concrete stair" 1건은 정정의 명시적 부정 재진술).</p>

<h2>2. 발명 어휘 전수 스캔 — 최종 t2i {len(shots)}샷 × 전 variation</h2>
<table><tr><th>어휘</th><th>건수</th><th>판정</th><th>근거/문맥</th></tr>
{''.join(rows)}</table>
<p>씬 원문 근거(scene_save segments): S#24 해안가/소나기(소나기×{EV[24].get('소나기',0)}) ·
S#26 포구(안개×{EV[26].get('안개',0)}) · S#30 바다(안개×{EV[30].get('안개',0)}) ·
S#4 옥탑방(콘크리트 0회 — concrete roof deck 잔존의 무근거 판정 근거)</p>
<p>상류 참고: shot_extract 한글 '콘크리트' 2건(옥탑방 외벽 · 포구 바닥 —
계단 아님, 최종 t2i 미생존) — 씬 원문 무근거 재질어로 동일 계열이나 하류에서 소거됨.</p>

<h2>3. FP 렌더 {len(fp_imgs)}장 (★ = TASK 2 경찰서 스타일 참조)</h2>
{imgs_html}
</body></html>"""

out = OUT_DIR / "v16corr_report.html"
out.write_text(doc, encoding="utf-8")
print(f"written: {out} ({len(doc):,} bytes) / imgs: {len(fp_imgs)}")
