#!/usr/bin/env python3
"""s31 — 스테이징 우선 체인: 마네킹 구도 → bg+엔티티 합성 (2026-07-09, 커밋 금지).

s30(배경 플레이트 우선)과 대조되는 사용자 지시 체인:
  [1] mannequin — 샷 원문만 주고 t2i 가 카메라 구도를 '스스로' 잡는다
      (개입 없음 — 카메라/구도 지시 0). 두 형태 × i2/nb2 = 샷당 4장:
        sketch: 스토리보드 스케치, 사람=목제 마네킹
        photo : 실사 스테이징, 사람=회색 마네킹
  [2] compose — 최종샷: 스테이징(카메라·블로킹 SOT) + 최초 bg(장소·룩
      SOT, "bg 의 어느 지점인지" 명시=s30 grounding 존+앵커 서술명 재사용,
      ID-free) + passport 엔티티. 스테이징 소스(i2/nb2)×합성 엔진(i2/nb2)
      전 교차: A(sketch 유래) 4장 + B(photo 유래) 4장 = 샷당 8장.
대상=s30 선택 7샷 → 총 84장. 판정 지식: ID-free·발명 경계·철제 계단 정정.
사용: backend/.venv/bin/python s31_mannequin_first.py --only <stage>
      [--engines i2,nb2] [--shots S4_Shot1,...]   (병렬 분할용)
산출: out/mannequin_first/*.png + plans/s31_mannequin_v1.json
      + mannequin_first.html
"""
import argparse
import html as _html
import json
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).parent))
import forest_lib as F  # noqa: E402
import s27_forest_map as S27  # noqa: E402
import s30_shot_apply as S30  # noqa: E402 (계약 블록·데이터 조인 재사용)

OUTD = F.OUT / "mannequin_first"
PAGE = F.EXP / "mannequin_first.html"
PLAN = "s31_mannequin_v1"

ENGINES = ["i2", "nb2"]
FORMS = ["sketch", "photo"]  # 1=스케치 마네킹, 2=실사 마네킹

# ── [1] 마네킹 스테이징 — 카메라 개입 0 (t2i 재량) ──

FREE_CAMERA = "\n".join([
    "YOU decide the camera: choose the angle, distance, height, lens and",
    "composition that best realise the shot text below. No camera or",
    "composition instructions are given on purpose — stage it yourself.",
])

MANNEQUIN_SKETCH = "\n".join([
    "Draw ONE storyboard frame for a live-action film shot: a monochrome",
    "pencil production sketch on white paper — rough construction lines,",
    "loose grey shading, no colour.",
    "Every person in the frame appears as a featureless wooden artist's",
    "mannequin (jointed figure, no face, no hair, no clothing detail).",
    "If the shot text implies no people, draw none.",
])

MANNEQUIN_PHOTO = "\n".join([
    "Create ONE PHOTOREALISTIC staging photograph of a live-action film",
    "shot on a real location set: the environment is fully real and",
    "photographic, but every person in the frame is a featureless grey",
    "display mannequin (smooth blank head, no face, no hair).",
    "If the shot text implies no people, include none.",
])

FORM_HEAD = {"sketch": MANNEQUIN_SKETCH, "photo": MANNEQUIN_PHOTO}


def _mannequin_prompt(form, sk, s):
    lines = ["SHOT TEXT (authoritative, Korean):",
             f"scene_heading: {s.get('scene_heading')}",
             f"shot: {s.get('description')}"]
    if s.get("characters"):
        lines.append("people in shot: " + ", ".join(s["characters"]))
    return "\n\n".join([FORM_HEAD[form], FREE_CAMERA, "\n".join(lines),
                        S30.NO_ANNOTATION])


# ── [2] 최종 합성 — 스테이징=카메라 SOT, 최초 bg=장소·룩 SOT ──

def _place_desc(spec, ground):
    """s30 grounding 존+앵커를 ID-free 서술로 (마커 코드 미노출)."""
    c2n = {it["code"]: it["name_en"] for it in spec["detail"]["items"]}
    names = [c2n[c] for c in ground["anchor_markers"] if c in c2n]
    return (f'the "{ground["map_zone"]}" area of the property, by the '
            + ", ".join(names))


STAGING_NOTE = "\n".join([
    "CAMERA & BLOCKING: the FIRST attached image is the staging frame of",
    "this shot. Reproduce its camera (angle, distance, height, framing)",
    "and the exact placement and pose of every figure. Its drawing or",
    "mannequin look is ONLY a staging aid — never carry its style,",
    "blankness or monochrome into the output.",
])

LOCATION_NOTE_HEAD = "\n".join([
    "LOCATION: the SECOND attached image is a real photograph of the",
    "entire filming property, and the single source of truth for how",
    "everything looks (building, materials, aging, colours, town).",
])


def _compose_prompt(spec, sk, s, ground):
    chars = s.get("characters") or []
    parts = [
        "Create the FINAL photorealistic live-action film still of the"
        " moment below.",
        STAGING_NOTE,
        LOCATION_NOTE_HEAD
        + f"\nThis shot is taken at {_place_desc(spec, ground)} —"
        " rebuild the surroundings seen by the staging camera from that"
        " exact spot of the property, consistent with the photograph.",
        "MOMENT (authoritative, Korean): " + (s.get("description") or "")
        + f"\nscene_heading: {s.get('scene_heading')}",
    ]
    if chars:
        parts.append(
            "CHARACTERS: replace each mannequin figure with the matching"
            " reference person, in the mannequin's exact position and"
            " pose — " + ", ".join(chars) + ". Match each reference"
            " person's identity exactly (face, hair, build); dress them"
            " as the moment describes.")
    else:
        parts.append("No people appear unless the moment itself says so.")
    parts.append("TIME & LIGHT: only as stated by the shot text above —"
                 " do not add weather, atmosphere or colour moods beyond"
                 " it.")
    parts.append(S30.WORLD_FACTS)
    parts.append(S30.NO_ANNOTATION)
    return "\n\n".join(parts)


# ── 스테이지 ──

def stage_prompts():
    """전 프롬프트 결정·저장 (LLM 0, 이후 gen 스테이지는 png 만 쓴다)."""
    spec = F.load_plan(S27.SPEC)
    s30 = F.load_plan(S30.PLAN)
    shots = S30._load_shots()
    sel = s30["selected_shots"]
    plan = {"selected_shots": sel, "mannequin": {}, "compose": {}}
    for sk in sel:
        s = shots[sk]
        plan["mannequin"][sk] = {
            form: {"prompt": _mannequin_prompt(form, sk, s),
                   **{eng: f"mq_{form}_{sk}_{eng}.png" for eng in ENGINES}}
            for form in FORMS}
        cp = _compose_prompt(spec, sk, s, s30["ground"][sk])
        plan["compose"][sk] = {
            "prompt": cp,
            "place_desc": _place_desc(spec, s30["ground"][sk]),
            "files": {f"{form}_{src}_by_{eng}":
                      f"final_{'A' if form == 'sketch' else 'B'}_{sk}"
                      f"_{form}{src}_by{eng}.png"
                      for form in FORMS for src in ENGINES
                      for eng in ENGINES}}
    F.save_plan(PLAN, plan)
    print(f"[prompts] {len(sel)}샷 × (마네킹 4 + 합성 8) 프롬프트 저장")


def _targets(plan, engines, shots_filter):
    sel = [k for k in plan["selected_shots"]
           if not shots_filter or k in shots_filter]
    return sel, [e for e in ENGINES if e in engines]


def stage_mannequin(engines, shots_filter):
    plan = F.load_plan(PLAN)
    sel, engs = _targets(plan, engines, shots_filter)
    for sk in sel:
        for form in FORMS:
            m = plan["mannequin"][sk][form]
            for eng in engs:
                out = OUTD / m[eng]
                if eng == "i2":
                    F.img_gpt(f"s31_mq_{form}_{sk}_i2", m["prompt"],
                              refs=None, size="1536x1024", out_path=out)
                else:
                    F.img_nb2(f"s31_mq_{form}_{sk}_nb2", m["prompt"], [],
                              aspect_ratio="16:9", out_path=out)
            print(f"[mannequin] {sk} {form}: {'+'.join(engs)} 완료")


def stage_compose(engines, shots_filter):
    plan = F.load_plan(PLAN)
    shots = S30._load_shots()
    passports = F.query_passports()
    name_to_sid = F.load_recon()["character_name_to_sid"]
    sel, engs = _targets(plan, engines, shots_filter)
    bg = S30.PHOTO_PNG
    for sk in sel:
        c = plan["compose"][sk]
        s = shots[sk]
        pp = [(f"CHARACTER REFERENCE — {n}: the exact person who replaces"
               " one mannequin figure.", passports[name_to_sid[n]])
              for n in (s.get("characters") or [])
              if name_to_sid.get(n) and passports.get(name_to_sid.get(n))]
        for form in FORMS:
            for src in ENGINES:
                staging = OUTD / plan["mannequin"][sk][form][src]
                assert staging.exists(), f"스테이징 없음: {staging}"
                for eng in engs:
                    fn = c["files"][f"{form}_{src}_by_{eng}"]
                    out = OUTD / fn
                    if eng == "i2":
                        F.img_gpt(f"s31_{fn[:-4]}", c["prompt"],
                                  refs=[staging, bg] + [p for _, p in pp],
                                  size="1536x1024", out_path=out)
                    else:
                        refs = [("STAGING FRAME — camera & blocking source"
                                 " only; never copy its style.", staging),
                                ("LOCATION PHOTOGRAPH — the whole filming"
                                 " property; sole source of how everything"
                                 " looks.", bg)] + pp
                        F.img_nb2(f"s31_{fn[:-4]}", c["prompt"], refs,
                                  aspect_ratio="16:9", out_path=out)
        print(f"[compose] {sk}: {'+'.join(engs)} 완료")


def stage_html():
    plan = F.load_plan(PLAN)
    shots = S30._load_shots()
    s30 = F.load_plan(S30.PLAN)

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

    def fig(rel, cap, width=23):
        return (f"<figure style='width:{width}%'><a href='{rel}'>"
                f"<img src='{rel}' loading='lazy'></a>"
                f"<figcaption>{esc(cap)}</figcaption></figure>")

    secs = ""
    for sk in plan["selected_shots"]:
        s = shots[sk]
        m = plan["mannequin"][sk]
        c = plan["compose"][sk]
        mfigs = "".join(
            fig(f"out/mannequin_first/{m[form][eng]}",
                f"{'①스케치' if form == 'sketch' else '②실사'} 마네킹 {eng}")
            for form in FORMS for eng in ENGINES
            if (OUTD / m[form][eng]).exists())
        rows = ""
        for form, lab in (("sketch", "A(스케치 유래)"), ("photo", "B(실사 유래)")):
            ffigs = "".join(
                fig(f"out/mannequin_first/{c['files'][k]}",
                    f"{lab} src={src} → 합성={eng}")
                for src in ENGINES for eng in ENGINES
                for k in [f"{form}_{src}_by_{eng}"]
                if (OUTD / c["files"][k]).exists())
            rows += f"<h3>{lab} 최종 4교차</h3>{ffigs}"
        s30cmp = ""
        s30f = ((s30.get("compose") or {}).get(sk) or {}).get("file")
        if s30f and (F.OUT / "shot_apply" / s30f).exists():
            s30cmp = fig(f"out/shot_apply/{s30f}", "비교: s30 최종(배경 플레이트 우선)")
        base = F.OUT / "shot_apply" / "baseline" / f"{sk}.png"
        if base.exists():
            s30cmp += fig(f"out/shot_apply/baseline/{sk}.png",
                          "비교: 기존 production 스틸")
        secs += f"""
<h2>{esc(sk)} — {esc(s.get('scene_heading'))}</h2>
<p>{esc(s.get('description'))}</p>
<p><b>bg 지점 명시</b>: {esc(c.get('place_desc'))}</p>
<h3>① / ② 마네킹 스테이징 (카메라 t2i 재량)</h3>{mfigs}
{rows}
{s30cmp}
<details><summary>프롬프트(마네킹 스케치/실사 · 합성 공통)</summary>
<pre>{esc(m['sketch']['prompt'])}</pre>
<pre>{esc(m['photo']['prompt'])}</pre>
<pre>{esc(c['prompt'])}</pre></details>
"""

    doc = f"""<!doctype html><html lang=ko><head><meta charset=utf-8>
<title>s31 — 스테이징 우선 체인 (마네킹 구도 → bg 합성)</title><style>
body{{font-family:'Apple SD Gothic Neo',sans-serif;margin:24px;max-width:1500px}}
figure{{display:inline-block;margin:1%;vertical-align:top}}
img{{width:100%;border:1px solid #ccc}} figcaption{{font-size:13px;text-align:center}}
pre{{font-size:11px;background:#f7f7f7;border:1px solid #ddd;padding:8px;
white-space:pre-wrap;max-height:320px;overflow:auto}}
h2{{border-bottom:2px solid #333;padding-bottom:4px;margin-top:36px}}
h3{{margin:14px 0 4px}}</style></head><body>
<h1>s31 — 스테이징 우선 체인 (2026-07-09)</h1>
<p>체인: [1] 샷 원문만으로 t2i 가 카메라 구도를 스스로 잡은 마네킹 스테이징
(①스케치/②실사 × i2/nb2) → [2] 스테이징(카메라·블로킹 SOT)+최초 bg(장소·룩
SOT, 지점 명시)+passport 로 최종 합성(소스×엔진 전 교차 8장/샷).
s30(배경 플레이트 우선)과 비교용.</p>
{fig('out/forest_map/top2/top2_photo_fixed_nb2.png', '장소·룩 SOT — 최초 bg', 31)}
{secs}
</body></html>"""
    PAGE.write_text(doc, encoding="utf-8")
    print(f"[html] {PAGE}")


if __name__ == "__main__":
    ap = argparse.ArgumentParser()
    ap.add_argument("--only", required=True,
                    choices=["prompts", "mannequin", "compose", "html"])
    ap.add_argument("--engines", default="i2,nb2")
    ap.add_argument("--shots", default="")
    a = ap.parse_args()
    OUTD.mkdir(parents=True, exist_ok=True)
    engines = [e.strip() for e in a.engines.split(",") if e.strip()]
    shots_filter = {s.strip() for s in a.shots.split(",") if s.strip()}
    if a.only == "prompts":
        stage_prompts()
    elif a.only == "mannequin":
        stage_mannequin(engines, shots_filter)
    elif a.only == "compose":
        stage_compose(engines, shots_filter)
    else:
        stage_html()
