#!/usr/bin/env python3
"""s36 — 옥탑 그룹 콘티에 '빌라 전체 캐논 실사'를 참조 주입 (2026-07-12, 실험 전용·커밋 금지).

배경: s35 풀 런 육안 분석 — 옥탑 4그룹(외부/내부/계단/골목)은 서사상 같은
건물인데 place_en 텍스트만으론 건물 '개체' 동일성이 안 잡힘(외관 드리프트,
S17sh2 딴 건물, S25sh1 내부 이탈).

실험: production 2회차(114a8883) W22 캐논 체인이 만든
canon_bg_rooftop_villa_complex_master.png (nb2 3롤 → GPT/Gemini VLM 합산
판정 → 결함 수정) 를 콘티 생성(i2) 참조로 첨부 — "이 건물 사진을 보고
관련된 부분들을 참고해서 그려라". s35 콘티(무참조)와 시트/패널 비교.

사용: backend/.venv/bin/python s36_conti_canonref.py --only <conti|crop|html>
"""
import argparse
import html as _html
import sys
from pathlib import Path

HERE = Path(__file__).parent
sys.path.insert(0, str(HERE))
import forest_lib as F  # noqa: E402
import s34_conti_sheet as C  # noqa: E402 — v24 콘티 계약 재사용
import s35_fullrun as R  # noqa: E402 — s35 플랜·크롭 로직 재사용

BASE = HERE
OUTC = BASE / "out" / "conti"
OUTP = BASE / "out" / "conti" / "panels36"
PAGE = BASE / "s36_canonref_conti.html"
PLAN = "s36_canonref_v1"
S35_PLAN = "s35_full_v1"

CANON_PHOTO = Path(
    "/Users/manta/Documents/Projects/TheRoad-I1/projects/"
    "114a8883-e960-4b97-97c7-0ba00a469750/images/background_chain/"
    "canon_bg_rooftop_villa_complex_master.png")

ROOF_GROUPS = ["rooftop_room_exterior", "rooftop_room_interior",
               "exterior_metal_staircase", "residential_alley_courtyard"]

REF_NOTE = "\n".join([
    "REFERENCE (BUILDING PHOTOGRAPH — architectural grounding, not",
    "style): the attached photograph shows the ACTUAL building where",
    "every panel of this sheet takes place — an old Korean multi-unit",
    "villa seen from above: a narrow ground-level alley and small",
    "courtyard, an exterior steel staircase climbing the wall, and a",
    "flat rooftop with a small rooftop room (its door and window), a",
    "water tank, a clothesline and a low parapet.",
    "Whenever a panel shows ANY part of this building — exterior walls,",
    "the steel staircase, the rooftop, the rooftop room, the courtyard",
    "or alley — draw THAT building: take its architecture, massing,",
    "proportions, materials and the positions of its fixed features",
    "(door, window, stairs, water tank, parapet) from this photograph,",
    "translated into monochrome pencil storyboard drawing. Never invent",
    "a different building and never contradict the photo's geometry.",
    "For INTERIOR panels, stay consistent with this building's scale",
    "and its window/door placement seen in the photo.",
    "Do NOT copy the photograph's style, colors, camera angle or crop —",
    "each panel keeps its own shot framing; the photo is spatial truth",
    "only.",
])


def stage_conti():
    s35 = F.load_plan(S35_PLAN)
    plan = F.load_plan(PLAN) if (BASE / "plans" / f"{PLAN}.json").exists() \
        else {}
    sheets = plan.setdefault("sheets", {})
    gmap = {g["key"]: g for g in s35["groups"]}
    assert CANON_PHOTO.exists(), f"캐논 사진 없음: {CANON_PHOTO}"
    for gkey in ROOF_GROUPS:
        g = gmap[gkey]
        mv = s35.get("movement", {}).get(gkey, {})
        for i, chunk in enumerate(R._chunks(g["shots"])):
            keys = [R._key(t) for t in chunk]
            slots, layout, size, ar = C._grid(len(keys))
            prompt = C._prompt(g["place_en"], keys, mv) + "\n\n" + REF_NOTE
            fn = f"conti36_{gkey}_{i}_i2.png"
            sheets[f"{gkey}_{i}"] = {
                "gkey": gkey, "file": fn, "shots": chunk,
                "slots": slots, "prompt": prompt,
                "ref": str(CANON_PHOTO)}
            F.img_gpt(f"s36_conti_{gkey}_{i}_i2", prompt,
                      refs=[CANON_PHOTO], size=size, out_path=OUTC / fn)
            print(f"[conti] {gkey}_{i}: {len(chunk)}샷 → {fn}")
    F.save_plan(PLAN, plan)


# s35 육안 검수에서 거터 오인이 있던 시트가 나오면 여기 추가
EQUAL_FORCE = set()


def stage_crop():
    from PIL import Image
    import numpy as np
    plan = F.load_plan(PLAN)
    OUTP.mkdir(parents=True, exist_ok=True)
    GRIDMAP = {2: (2, 1), 4: (2, 2), 6: (3, 2)}
    for skey, sh in plan["sheets"].items():
        sheet = Image.open(OUTC / sh["file"]).convert("RGB")
        mask = sheet.convert("L").point(lambda v: 255 if v < 242 else 0)
        bbox = mask.getbbox()
        if bbox:
            pad = 4
            bbox = (max(0, bbox[0] - pad), max(0, bbox[1] - pad),
                    min(sheet.size[0], bbox[2] + pad),
                    min(sheet.size[1], bbox[3] + pad))
            sheet = sheet.crop(bbox)
        W, H = sheet.size
        cols, rows = GRIDMAP[sh["slots"]]
        arr = np.asarray(sheet.convert("L"))
        dark = arr < 120

        def _lines(profile, n_expect, total):
            cand = [i for i, v in enumerate(profile) if v > 0.55]
            gs = []
            for i in cand:
                if gs and i - gs[-1][-1] <= 6:
                    gs[-1].append(i)
                else:
                    gs.append([i])
            centers = [sum(x) // len(x) for x in gs
                       if total * 0.05 < sum(x) / len(x) < total * 0.95]
            if len(centers) == n_expect:
                return centers
            return [total * (k + 1) // (n_expect + 1)
                    for k in range(n_expect)]

        if skey in EQUAL_FORCE:
            v_lines = [W * (k + 1) // cols for k in range(cols - 1)]
            h_lines = [H * (k + 1) // rows for k in range(rows - 1)]
        else:
            v_lines = _lines(dark.mean(axis=0), cols - 1, W) if cols > 1 else []
            h_lines = _lines(dark.mean(axis=1), rows - 1, H) if rows > 1 else []
        xs = [0] + v_lines + [W]
        ys = [0] + h_lines + [H]
        for i, tag in enumerate(sh["shots"]):
            c, r = i % cols, i // cols
            box = (xs[c] + 3, ys[r] + 3, xs[c + 1] - 3, ys[r + 1] - 3)
            sheet.crop(box).save(OUTP / f"{tag}.png")
        print(f"[crop] {skey}: {len(sh['shots'])}패널 v={v_lines} h={h_lines}")


def stage_html():
    import shutil
    s35 = F.load_plan(S35_PLAN)
    plan = F.load_plan(PLAN)
    gmap = {g["key"]: g for g in s35["groups"]}
    REFD = OUTC / "refs36"
    REFD.mkdir(parents=True, exist_ok=True)
    canon_rel = f"out/conti/refs36/{CANON_PHOTO.name}"
    if not (REFD / CANON_PHOTO.name).exists():
        shutil.copy(CANON_PHOTO, REFD / CANON_PHOTO.name)

    def esc(t):
        return _html.escape(t or "")

    secs = []
    for gkey in ROOF_GROUPS:
        g = gmap[gkey]
        sheet_rows = ""
        for skey, sh in plan["sheets"].items():
            if sh["gkey"] != gkey:
                continue
            old_fn = f"out/conti/conti35_{skey}_i2.png"
            new_fn = f"out/conti/{sh['file']}"
            sheet_rows += (
                f"<h3>시트 {esc(skey)} <span class=k>({len(sh['shots'])}샷:"
                f" {esc(', '.join(sh['shots']))})</span></h3>"
                f"<div class=row>"
                f"<figure><a href='{old_fn}' target=_blank>"
                f"<img src='{old_fn}' loading=lazy></a>"
                f"<figcaption>s35 기존 콘티 (참조 없음)</figcaption></figure>"
                f"<figure><a href='{new_fn}' target=_blank>"
                f"<img src='{new_fn}' loading=lazy></a>"
                f"<figcaption><b>s36 신규 콘티 (빌라 캐논 참조)</b>"
                f"</figcaption></figure></div>")
            panel_rows = ""
            for tag in sh["shots"]:
                oldp = f"out/conti/panels35/{tag}.png"
                newp = f"out/conti/panels36/{tag}.png"
                desc = R.SHOT[R._key(tag)]["desc"]
                panel_rows += (
                    f"<div class=pcell><div class=note>{tag} — "
                    f"{esc(desc)}</div>"
                    f"<div class=prow>"
                    f"<figure class=pn><a href='{oldp}' target=_blank>"
                    f"<img src='{oldp}' loading=lazy></a>"
                    f"<figcaption>s35</figcaption></figure>"
                    f"<figure class=pn><a href='{newp}' target=_blank>"
                    f"<img src='{newp}' loading=lazy></a>"
                    f"<figcaption>s36 캐논 참조</figcaption></figure>"
                    f"</div></div>")
            sheet_rows += (f"<details><summary>패널별 비교"
                           f" ({len(sh['shots'])}샷)</summary>"
                           f"<div class=pgrid>{panel_rows}</div></details>"
                           f"<details><summary>프롬프트 전문 (s36)</summary>"
                           f"<pre>{esc(sh['prompt'])}</pre></details>")
        secs.append(
            f"<section><h2>{esc(g['name_ko'])} <span class=k>({gkey} ·"
            f" 샷 {len(g['shots'])})</span></h2>"
            f"<div class=note>LOCATION lock: {esc(g['place_en'])}</div>"
            f"{sheet_rows}</section>")
    PAGE.write_text(f"""<!DOCTYPE html>
<html lang=ko><head><meta charset=utf-8>
<meta name=viewport content="width=device-width, initial-scale=1">
<title>s36 — 옥탑 콘티 빌라 캐논 참조 실험</title>
<style>
body {{ margin:0; padding:24px; background:#0f1216; color:#e6e6e6;
       font:14px/1.6 -apple-system,'Apple SD Gothic Neo',sans-serif; }}
h1 {{ font-size:20px; }} h2 {{ font-size:17px; margin:34px 0 8px;
     border-bottom:1px solid #333; padding-bottom:5px; }}
h3 {{ font-size:15px; margin:14px 0 6px; }}
.k {{ color:#8a939e; font-size:13px; font-weight:400; }}
.row {{ display:flex; gap:14px; align-items:flex-start; margin:10px 0;
        flex-wrap:wrap; }}
figure {{ margin:0; }} figcaption {{ color:#c9d2dc; font-size:12.5px;
          max-width:620px; }}
img {{ max-width:620px; width:100%; border-radius:8px;
      border:1px solid #2a2f36; }}
.note {{ color:#8a939e; }}
.box {{ background:#161b22; border:1px solid #2a2f36; border-radius:8px;
       padding:12px 16px; margin:12px 0; }}
.pgrid {{ display:flex; flex-direction:column; gap:14px; margin:10px 0; }}
.pcell {{ border-top:1px dashed #2a2f36; padding-top:8px; }}
.prow {{ display:flex; gap:10px; flex-wrap:wrap; }}
figure.pn img {{ max-width:300px; }}
figure.pn figcaption {{ font-size:11px; }}
details {{ margin:8px 0; }} summary {{ color:#9ecbff; cursor:pointer; }}
pre {{ background:#161b22; border:1px solid #2a2f36; padding:10px;
      white-space:pre-wrap; font-size:12px; }}
.canon img {{ max-width:760px; }}
</style></head><body>
<h1>s36 — 옥탑 콘티 생성에 '빌라 전체 캐논 실사' 참조 주입</h1>
<div class=box>실험: s35 풀 런에서 옥탑 4그룹(외부·내부·계단·골목)은
같은 건물인데 콘티가 place_en 텍스트만 보고 그려져 건물 개체가
드리프트. 이번엔 production 2회차 W22 캐논 체인 산출
<b>canon_bg_rooftop_villa_complex_master.png</b>(nb2 3롤 → VLM 이중
판정 → 결함 수정)을 i2 콘티 생성에 <b>BUILDING PHOTOGRAPH 참조</b>로
첨부 — "패널이 이 건물의 어느 부분이라도 보여줄 땐 이 사진의 건축·
비례·고정물 위치를 따라 그려라(스타일·앵글은 복사 금지)". 좌=기존
s35(무참조) / 우=신규 s36(캐논 참조).</div>
<section><h2>참조로 쓴 빌라 캐논</h2>
<figure class=canon><a href='{canon_rel}' target=_blank>
<img src='{canon_rel}'></a>
<figcaption>production 2회차(114a8883) outdoor_place_canon —
골목·마당(자전거)·외벽 철제 계단·옥탑방(문·창)·물탱크·빨래줄·낮은
난간이 한 프레임에 담긴 마스터. VLM(GPT+Gemini 합산) 판정 통과본.
</figcaption></figure></section>
{''.join(secs)}
</body></html>""")
    print(f"[html] {PAGE}")


if __name__ == "__main__":
    ap = argparse.ArgumentParser()
    ap.add_argument("--only", required=True,
                    choices=["conti", "crop", "html"])
    a = ap.parse_args()
    {"conti": stage_conti, "crop": stage_crop, "html": stage_html}[a.only]()
