#!/usr/bin/env python3
"""s41 — 배경 플레이트 참조 샷별 경량 콘티만 먼저 (2026-07-13).

사용자 지시(s40 후속): "관련 배경 이미지를 참조 이미지로 해서 각각을
매우 가벼운 콘티 이미지로 작성. 배경 전용 샷은 제외해도 됨(prev 만으로
되는 샷도 실험이니 그려라). 배경을 그대로 따라갈 필요 없이 참조만.
콘티만 그리라는 것 — 스틸은 아직."

- 대상: bgonly(배경 전용) 판정 False 인 13샷 (배경 전용 10샷 제외).
- 엔진: i2(gpt-image), 참조=해당 샷의 s40 수정 플레이트 1장.
- 스타일: s38 LIGHT_STYLE 의 단일 프레임 변형(얇은 윤곽선만).
- 배경 참조 계약: 느슨한 공간 참고용 — 카메라·디테일 복사 불요.

※ 복잡 구도(예: 버스 정류장류)는 항공 맵+카메라+엔티티 마커 방식이
  필요하나 옥탑 샷엔 불요 — 지금 구현 안 함(사용자 메모).

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

HERE = Path(__file__).parent
sys.path.insert(0, str(HERE))
import forest_lib as F  # noqa: E402
import s35_fullrun as R  # noqa: E402
import s37_rooftop_rebuild as W  # noqa: E402
import s38_lightconti_full as L8  # noqa: E402
import s39_threeroll_vlm as N9  # noqa: E402
import s40_full_multiref as M0  # noqa: E402

OUT = W.OUT.parent / "s41"
PAGE = HERE / "s41_conti_light.html"
PLAN = "s41_conti_v1"

LIGHT_FRAME = "\n".join([
    "STYLE — LAYOUT-ONLY THUMBNAIL SKETCH: the lightest possible",
    "director's layout drawing. THIN, CLEAN, UNIFORM PENCIL CONTOUR",
    "LINES ONLY on pure white paper. Absolutely NO shading, NO tone, NO",
    "hatching, NO texture, NO grayscale fill, NO light-and-shadow, NO",
    "facial features beyond a bare head outline, NO clothing detail, NO",
    "material rendering. The frame contains ONLY: the camera framing,",
    "each figure as a simple correct-proportion human contour in its",
    "exact pose and screen position/size, and the few major set masses",
    "as plain outline shapes. It must read as a quick layout guide,",
    "never as a finished drawing.",
])

BG_REF_CLAUSE = "\n".join([
    "The attached photograph is the LOCATION where this shot happens.",
    "Use it ONLY as a LOOSE spatial reference — the rough set masses",
    "and where things sit relative to each other. You do NOT need to",
    "follow it exactly: do not copy its camera, its lighting or its",
    "details. Choose the camera and framing the SHOT TEXT needs, and",
    "draw only the few set masses that matter as plain outline shapes.",
])


def _targets():
    p37 = L8._plan37()
    bgonly = F.load_plan(N9.PLAN)["bgonly"]
    tags = L8._all_tags_story_order(p37)
    keep = [t for t in tags if bgonly[t]["person_visible"]]
    skip = [t for t in tags if not bgonly[t]["person_visible"]]
    return p37, keep, skip


def stage_conti():
    p37, keep, skip = _targets()
    plan35 = W._s35()
    mv = W._movement(plan35)
    gmap = {g["key"]: g for g in plan35["groups"]}
    tag2g = {t: gk for gk in W.EXT_GROUPS for t in gmap[gk]["shots"]}
    tag2g.update({t: W.INT_GROUP for t in p37["int_tags"]})
    plan = F.load_plan(PLAN) if (HERE / "plans" / f"{PLAN}.json").exists() \
        else {}
    rec = plan.setdefault("conti", {})
    plan["skipped_bgonly"] = skip
    OUT.mkdir(parents=True, exist_ok=True)
    print(f"[conti] 대상 {len(keep)}샷 / 배경 전용 제외 {len(skip)}샷:"
          f" {skip}")
    for tag in keep:
        out = OUT / f"conti_{tag}.png"
        if out.exists():
            print(f"[conti] {tag} skip(exists)")
            continue
        gkey = tag2g[tag]
        plate = M0._plate40(tag, gkey, p37)
        assert plate.exists(), plate
        s = R.SHOT[R._key(tag)]
        fix = p37.get("pose_fix", {}).get(tag) or {}
        m = mv.get(tag, {})
        movement = fix.get("movement_en") or m.get("movement", "")
        figures = fix.get("figures_en") or m.get("figures", "")
        carried = fix.get("carried_en") or (
            p37.get("int_cont", {}).get(tag, {}).get("carried_en", "")
            if gkey == W.INT_GROUP else "")
        parts = [
            "Draw ONE storyboard layout frame, 16:9, filling the entire"
            " image edge-to-edge.",
            LIGHT_FRAME,
            BG_REF_CLAUSE,
            f"THE LOCATION: {gmap[gkey]['place_en']}",
            f"SHOT TEXT (authoritative, Korean): {S_soft(s['desc'])}",
        ]
        if movement:
            parts.append("MOVEMENT (follow exactly): " + S_soft(movement))
        if figures:
            parts.append("FIGURES — apparent size & depth (follow"
                         " exactly): " + S_soft(figures))
        pc = L8._pose_clauses(p37, [tag])
        if pc:
            parts.append("\n".join(pc))
        if carried:
            parts.append("CARRIED STATE (persist exactly): "
                         + S_soft(carried))
        parts.append("ABSOLUTELY NO TEXT anywhere: no captions, no"
                     " numbers, no labels of any kind.")
        prompt = "\n\n".join(parts)
        F.img_gpt(f"s41_conti_{tag}", prompt, refs=[plate],
                  size="1536x864", out_path=out)
        rec[tag] = {"prompt": prompt, "plate": str(plate),
                    "gkey": gkey}
        F.save_plan(PLAN, plan)
        print(f"[conti] {tag} 완료 (plate={plate.name})")


def S_soft(t):
    import s34_stillrun as S
    return S._soften(t)


def stage_stills():
    """실사 스틸 (07-13 사용자: "이전 샷을 참조하는 페이지는 제외하고
    배경과 콘티를 둘 다 + 엔티티 포함해서 실사") — prev 없는 콘티
    5샷, 참조=[플레이트+경량 콘티+캐릭터·소품], s40 파이프(3롤→
    Gemini 선정→결함 i2i 수정) 재사용."""
    import s34_stillrun as S
    p37, keep, _skip = _targets()
    prev2 = F.load_plan(L8.PLAN)["prev2"]
    targets = [t for t in keep if not (prev2.get(t) or {}).get("prev")]
    plan35 = W._s35()
    mv = W._movement(plan35)
    gmap = {g["key"]: g for g in plan35["groups"]}
    tag2g = {t: gk for gk in W.EXT_GROUPS for t in gmap[gk]["shots"]}
    tag2g.update({t: W.INT_GROUP for t in p37["int_tags"]})
    chars = S._char_refs()
    props = S._prop_refs()
    ve = S._ve_ids()
    scene_union = {}
    for (si, _shi), ids in ve.items():
        scene_union.setdefault(si, set()).update(ids)
    plan = F.load_plan(PLAN)
    rec = plan.setdefault("stills", {})
    OUTS = OUT / "stills"
    OUTS.mkdir(parents=True, exist_ok=True)
    print(f"[stills] 대상 {len(targets)}샷 (prev 참조 샷 제외):"
          f" {targets}")
    for tag in targets:
        stem = OUTS / tag
        if (OUTS / f"{tag}_sel.png").exists():
            if rec.get(tag) and M0._resume(rec, tag, f"s41st_{tag}",
                                           rec[tag]["prompt"], stem):
                F.save_plan(PLAN, plan)
            else:
                print(f"[stills] {tag} skip(exists)")
            continue
        gkey = tag2g[tag]
        plate = M0._plate40(tag, gkey, p37)
        conti = OUT / f"conti_{tag}.png"
        assert plate.exists() and conti.exists(), tag
        refs = [(W.PLATE_STILL_LABEL, plate),
                (L8.LIGHT_PANEL_LABEL, conti)]
        ve_ids = ve.get(R._key(tag), [])
        if not ve_ids:
            ve_ids = sorted(scene_union.get(R._key(tag)[0], set()))
        for cid in ve_ids:
            if cid in chars:
                name, p = chars[cid]
                if p.exists():
                    refs.append((
                        f"CHARACTER REFERENCE — {name}: the exact"
                        " person appearing in this shot; match face,"
                        " hair and build exactly.", p))
            elif cid in props:
                name, p = props[cid]
                if p.exists():
                    refs.append((
                        f"PROP REFERENCE — {name}: the exact object"
                        " appearing in this shot; match its look,"
                        " material and wear exactly.", p))
        prompt = N9._build_prompt(tag, gkey, p37, mv, gmap,
                                  None, "", bg_only=False)
        M0._select(rec, tag, f"s41st_{tag}", N9.JUDGE_SYSTEM, prompt,
                   refs, stem)
        rec[tag].update({"gkey": gkey,
                         "ref_mode": "플레이트+콘티+엔티티"})
        F.save_plan(PLAN, plan)


def stage_html():
    p37, keep, skip = _targets()
    plan35 = W._s35()
    plan = F.load_plan(PLAN)
    gmap = {g["key"]: g for g in plan35["groups"]}
    rec = plan.get("conti", {})
    REFD = OUT / "refs"
    REFD.mkdir(parents=True, exist_ok=True)

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

    def _rel(path_s):
        src = Path(path_s)
        if str(src).startswith(str(HERE)):
            return str(src.relative_to(HERE))
        dst = REFD / src.name
        if not dst.exists():
            shutil.copy(src, dst)
        return f"out/s41/refs/{src.name}"

    tag2g = {t: gk for gk in W.EXT_GROUPS for t in gmap[gk]["shots"]}
    tag2g.update({t: W.INT_GROUP for t in p37["int_tags"]})
    body = ""
    cur = None
    for tag in L8._all_tags_story_order(p37):
        gkey = tag2g[tag]
        if gkey != cur:
            body += (f"<h2>{esc(gmap[gkey]['name_ko'])}"
                     f" <span class=k>({gkey})</span></h2>")
            cur = gkey
        if tag in skip:
            body += (f"<div class=shot><h3>{tag}"
                     f" <span class=off>배경 전용 — 콘티 제외</span></h3>"
                     f"<div class=note>"
                     f"{esc(R.SHOT[R._key(tag)]['desc'])}</div></div>")
            continue
        it = rec.get(tag)
        if not it:
            body += (f"<div class=shot><h3>{tag}</h3>"
                     f"<div class=note>미생성</div></div>")
            continue
        body += (
            f"<div class=shot><h3>{tag}"
            f" <span class=on>경량 콘티</span></h3>"
            f"<div class=note>{esc(R.SHOT[R._key(tag)]['desc'])}</div>"
            f"<div class=row>"
            f"<figure><a href='out/s41/conti_{tag}.png' target=_blank>"
            f"<img src='out/s41/conti_{tag}.png' loading=lazy></a>"
            f"<figcaption>경량 레이아웃 콘티 (i2)</figcaption></figure>"
            f"<figure class=pn><a href='{_rel(it['plate'])}'"
            f" target=_blank><img src='{_rel(it['plate'])}'"
            f" loading=lazy></a><figcaption>배경 참조 (느슨)"
            f"<br><span class=k>{esc(Path(it['plate']).name)}</span>"
            f"</figcaption></figure></div>"
            f"<details><summary>프롬프트 전문</summary>"
            f"<pre>{esc(it['prompt'])}</pre></details></div>")

    stills = plan.get("stills", {})
    stills_html = ""
    for tag, it in stills.items():
        vd = {v["label"]: v for v in it["verdicts"]}
        cands = ""
        for lab in ("A", "B", "C"):
            sel = it["selected"] == lab
            v = vd.get(lab, {})
            cands += (
                f"<figure class=cand{' sel' if sel else ''}>"
                f"<a href='out/s41/stills/{tag}_{lab.lower()}.png'"
                f" target=_blank><img"
                f" src='out/s41/stills/{tag}_{lab.lower()}.png'"
                f" loading=lazy></a>"
                + ("<div class=selbadge>★ 선정</div>" if sel else "")
                + f"<figcaption><b>{lab} — Gemini"
                f" {v.get('score', '?')}점</b><br>"
                f"{esc(v.get('verdict_ko', ''))}</figcaption></figure>")
        if it.get("fix_skipped"):
            fixb = ("<div class=note>✔ Gemini 결함 검사: 0건 — 선정본"
                    " 그대로</div>")
        else:
            iss = "".join(
                f"<li>{esc(i['issue_ko'])} <span class=k>→"
                f" {esc(i['fix_en'])}</span></li>"
                for i in it.get("critique", []))
            sl = it["selected"].lower()
            fixb = (
                f"<div class=note>⚠ Gemini 결함"
                f" {len(it.get('critique', []))}건 → i2i 수정본이 최종"
                f"</div><ul class=iss>{iss}</ul><div class=row>"
                f"<figure class=cand><a"
                f" href='out/s41/stills/{tag}_{sl}.png' target=_blank>"
                f"<img src='out/s41/stills/{tag}_{sl}.png'"
                f" loading=lazy></a><figcaption>수정 전 (선정 원본"
                f" {it['selected']})</figcaption></figure>"
                f"<figure class=cand fixed><a"
                f" href='out/s41/stills/{tag}_fix.png' target=_blank>"
                f"<img src='out/s41/stills/{tag}_fix.png'"
                f" loading=lazy></a><figcaption><b>수정 후 (최종)</b>"
                f"</figcaption></figure></div>")
        ref_items = "".join(
            f"<figure class=pn><a href='{_rel(r['path'])}'"
            f" target=_blank><img src='{_rel(r['path'])}'"
            f" loading=lazy></a>"
            f"<figcaption>{esc(r['label'].split(':')[0].strip()[:70])}"
            f"<br><span class=k>{esc(Path(r['path']).name)}</span>"
            f"</figcaption></figure>" for r in it.get("refs", []))
        stills_html += (
            f"<div class=shot><h3>{tag}"
            f" <span class=on>{esc(it.get('ref_mode', ''))}</span></h3>"
            f"<div class=note>{esc(R.SHOT[R._key(tag)]['desc'])}</div>"
            f"<div class=row>{cands}</div>{fixb}"
            f"<div class=row><div class=refs><div class=note>참조"
            f" ({len(it.get('refs', []))}장)</div>"
            f"<div class=refrow>{ref_items}</div></div></div>"
            f"<details><summary>프롬프트 전문</summary>"
            f"<pre>{esc(it['prompt'])}</pre></details></div>")
    if stills_html:
        stills_html = (
            "<h2>실사 스틸 — prev 참조 샷 제외, 참조=플레이트+콘티+"
            "엔티티, 3롤→Gemini 선정→결함 i2i 수정</h2>" + stills_html)

    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>s41 — 배경 참조 경량 콘티 + 실사 5샷</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:6px 0; }}
.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; max-width:620px; }}
figcaption {{ color:#c9d2dc; font-size:12px; }}
img {{ width:100%; border-radius:8px; border:1px solid #2a2f36;
      background:#fff; }}
figure.pn {{ max-width:300px; }}
.note {{ color:#8a939e; }}
.box {{ background:#161b22; border:1px solid #2a2f36; border-radius:8px;
       padding:12px 16px; margin:12px 0; }}
.shot {{ border-top:1px dashed #2a2f36; padding:12px 0; }}
.on {{ background:#1e3a2a; color:#7ee2a8; border-radius:5px;
      padding:1px 8px; font-size:12px; margin-left:6px; }}
.off {{ background:#3a2a1e; color:#e2b57e; border-radius:5px;
       padding:1px 8px; font-size:12px; margin-left:6px; }}
details {{ margin:6px 0; }} summary {{ color:#9ecbff; cursor:pointer; }}
pre {{ background:#161b22; border:1px solid #2a2f36; padding:10px;
      white-space:pre-wrap; font-size:12px; }}
figure.cand {{ max-width:430px; position:relative; }}
figure.cand.sel img {{ border:4px solid #7ee2a8;
  box-shadow:0 0 14px rgba(126,226,168,.45); }}
figure.cand[fixed] img {{ border:3px solid #9ecbff; }}
.selbadge {{ position:absolute; top:8px; left:8px; background:#1e3a2a;
  color:#7ee2a8; border:1px solid #7ee2a8; border-radius:6px;
  padding:2px 10px; font-size:13px; font-weight:700; }}
.refs {{ max-width:820px; }}
.refrow {{ display:flex; gap:8px; flex-wrap:wrap; }}
figure.pn {{ max-width:150px; }} figure.pn figcaption {{ font-size:11px;
  max-width:150px; }}
ul.iss {{ margin:4px 0 8px 18px; color:#c9d2dc; font-size:13px; }}
</style></head><body>
<h1>s41 — 배경 플레이트 참조 샷별 경량 콘티 ({len(rec)}/{len(keep)}샷,
배경 전용 {len(skip)}샷 제외)</h1>
<div class=box>사용자 확정 규칙: ①<b>콘티만 먼저</b>(스틸 없음)
②샷별 개별 생성 — 참조=해당 샷의 s40 수정 플레이트 1장,
<b>느슨한 공간 참고용</b>(카메라·디테일 복사 불요, 샷 텍스트가
요구하는 카메라 선택) ③스타일=초경량 레이아웃 선화(단일 프레임)
④배경 전용 샷 {len(skip)}건 제외: {esc(', '.join(skip))}.
※복잡 구도(정류장류)의 항공 맵+카메라+엔티티 방식은 차후 —
옥탑 샷엔 불요. 이전 판: <a href='s40_full_multiref.html'
style='color:#9ecbff'>s40</a></div>
{stills_html}
{body}
</body></html>""")
    print(f"[html] {PAGE}")


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