#!/usr/bin/env python3
"""s32 — 접지된 스테이징 체인 (2026-07-09, 실험 전용·커밋 금지).

s30(장소 정확·구도 경직) × s31(구도 자유·장소 표류)의 절충:
마네킹 스테이징을 그릴 때 촬영지 사진(최초 bg)+약도(맵)를 참조로 첨부하고
"맵의 어느 지점인지"+시간·광원을 고정하되, 카메라 앵글 선택만 t2i 재량.
  [1] staging — 접지된 실사 마네킹 스테이징 (참조=최초 bg+맵, i2/nb2)
  [2] compose — 최종 스틸: 스테이징(카메라·블로킹 SOT)+최초 bg(룩 SOT)
      +passport. 스테이징 소스(i2/nb2)×합성 엔진(i2/nb2) 교차 4장/샷.
대상 4샷: S13_Shot3(s31 장소 표류)/S11_Shot3(s31 주간화)/S13_Shot5/S17_Shot1.
사용: backend/.venv/bin/python s32_grounded_staging.py --only <stage>
      [--engines i2,nb2] [--shots ...]
산출: out/grounded_staging/*.png + plans/s32_grounded_v1.json
      + grounded_staging.html
"""
import argparse
import html as _html
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
import s31_mannequin_first as S31  # noqa: E402 (place_desc·계약 재사용)

OUTD = F.OUT / "grounded_staging"
PAGE = F.EXP / "grounded_staging.html"
PLAN = "s32_grounded_v1"

SHOTS = ["S13_Shot3", "S11_Shot3", "S13_Shot5", "S17_Shot1"]
ENGINES = ["i2", "nb2"]

# ── [1] 접지된 스테이징 — 장소·시간 고정, 카메라만 재량 ──

GROUNDED_STAGING_HEAD = "\n".join([
    "Create ONE PHOTOREALISTIC staging photograph of a live-action film",
    "shot, taken ON LOCATION at the real filming property shown in the",
    "attached references: 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.",
])

FREE_CAMERA_GROUNDED = "\n".join([
    "YOU choose the camera: pick the angle, distance, height, lens and",
    "composition that realise the shot text most powerfully — the camera",
    "may stand anywhere plausible AT THE STATED SPOT of this property.",
    "No composition instructions are given on purpose.",
])

TIME_LOCK = "\n".join([
    "TIME & LIGHT (hard constraint): the time of day and lighting MUST",
    "follow the shot text above — a night scene stays night, a dawn scene",
    "stays dawn. Do not add weather, atmosphere or colour moods the shot",
    "text does not state.",
])

STAGING_LOOK_NOTE = "\n".join([
    "LOCATION REFERENCES: the attached PHOTOGRAPH shows this same",
    "property (sole source of how everything looks — building, materials,",
    "aging, colours, surroundings). The attached flat SITE-PLAN DRAWING",
    "shows where things are on the property — layout source only; never",
    "draw the plan itself, its colours, circles or labels.",
])


def _staging_prompt(spec, sk, s, ground):
    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([
        GROUNDED_STAGING_HEAD,
        "SPOT (fixed): this shot happens at "
        + S31._place_desc(spec, ground) + ".",
        "\n".join(lines),
        FREE_CAMERA_GROUNDED,
        TIME_LOCK,
        S30.WORLD_FACTS,
        STAGING_LOOK_NOTE,
        S30.NO_ANNOTATION,
    ])


# ── [2] 최종 합성 ──

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.",
        "CAMERA & BLOCKING: the FIRST attached image is the on-location"
        " staging photograph of this shot. Keep its camera (angle,"
        " distance, height, framing), its spatial layout and the exact"
        " placement and pose of every figure. Mannequin figures are"
        " stand-ins only.",
        "LOOK: the SECOND attached image is a real photograph of the same"
        " property — the source of truth for materials, aging and"
        " colours. The shot is at " + S31._place_desc(spec, ground)
        + "; keep the setting consistent with both references.",
        "MOMENT (authoritative, Korean): " + (s.get("description") or "")
        + f"\nscene_heading: {s.get('scene_heading')}",
    ]
    if chars:
        parts.append(
            "CHARACTERS: replace each mannequin 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_LOCK)
    parts.append(S30.WORLD_FACTS)
    parts.append(S30.NO_ANNOTATION)
    return "\n\n".join(parts)


# ── 스테이지 ──

def stage_prompts():
    spec = F.load_plan(S27.SPEC)
    s30 = F.load_plan(S30.PLAN)
    shots = S30._load_shots()
    plan = {"selected_shots": SHOTS, "staging": {}, "compose": {}}
    for sk in SHOTS:
        s, g = shots[sk], s30["ground"][sk]
        plan["staging"][sk] = {
            "prompt": _staging_prompt(spec, sk, s, g),
            **{eng: f"gstage_{sk}_{eng}.png" for eng in ENGINES}}
        plan["compose"][sk] = {
            "prompt": _compose_prompt(spec, sk, s, g),
            "place_desc": S31._place_desc(spec, g),
            "files": {f"{src}_by_{eng}": f"final_{sk}_src{src}_by{eng}.png"
                      for src in ENGINES for eng in ENGINES}}
    F.save_plan(PLAN, plan)
    print(f"[prompts] {len(SHOTS)}샷 저장")


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_staging(engines, shots_filter):
    plan = F.load_plan(PLAN)
    sel, engs = _targets(plan, engines, shots_filter)
    bg, mp = S30.PHOTO_PNG, S30.MAP_PNG
    for sk in sel:
        st = plan["staging"][sk]
        for eng in engs:
            out = OUTD / st[eng]
            if eng == "i2":
                F.img_gpt(f"s32_gstage_{sk}_i2", st["prompt"],
                          refs=[bg, mp], size="1536x1024", out_path=out)
            else:
                F.img_nb2(f"s32_gstage_{sk}_nb2", st["prompt"],
                          [("LOCATION PHOTOGRAPH — the same property; sole"
                            " source of how everything looks.", bg),
                           ("SITE PLAN — layout source only; never draw"
                            " this drawing or its markers.", mp)],
                          aspect_ratio="16:9", out_path=out)
        print(f"[staging] {sk}: {'+'.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 src in ENGINES:
            staging = OUTD / plan["staging"][sk][src]
            assert staging.exists(), f"스테이징 없음: {staging}"
            for eng in engs:
                fn = c["files"][f"{src}_by_{eng}"]
                out = OUTD / fn
                if eng == "i2":
                    F.img_gpt(f"s32_{fn[:-4]}", c["prompt"],
                              refs=[staging, bg] + [p for _, p in pp],
                              size="1536x1024", out_path=out)
                else:
                    refs = [("STAGING PHOTOGRAPH — camera & blocking"
                             " source; mannequins are stand-ins.", staging),
                            ("LOCATION PHOTOGRAPH — sole source of how"
                             " everything looks.", bg)] + pp
                    F.img_nb2(f"s32_{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)
    s31 = F.load_plan(S31.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]
        st = plan["staging"][sk]
        c = plan["compose"][sk]
        sfigs = "".join(fig(f"out/grounded_staging/{st[eng]}",
                            f"접지 스테이징 {eng}")
                        for eng in ENGINES if (OUTD / st[eng]).exists())
        ffigs = "".join(
            fig(f"out/grounded_staging/{c['files'][k]}",
                f"최종 src={src} → 합성={eng}")
            for src in ENGINES for eng in ENGINES
            for k in [f"{src}_by_{eng}"]
            if (OUTD / c["files"][k]).exists())
        cmp_figs = ""
        s30f = ((s30.get("compose") or {}).get(sk) or {}).get("file")
        if s30f and (F.OUT / "shot_apply" / s30f).exists():
            cmp_figs += fig(f"out/shot_apply/{s30f}", "비교: s30(플레이트 우선)")
        s31c = ((s31.get("compose") or {}).get(sk) or {})
        s31f = (s31c.get("files") or {}).get("photo_nb2_by_nb2")
        if s31f and (F.OUT / "mannequin_first" / s31f).exists():
            cmp_figs += fig(f"out/mannequin_first/{s31f}",
                            "비교: s31(자유 스테이징, photo nb2→nb2)")
        base = F.OUT / "shot_apply" / "baseline" / f"{sk}.png"
        if base.exists():
            cmp_figs += 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>고정 지점</b>: {esc(c.get('place_desc'))}</p>
<h3>① 접지 스테이징 (장소·시간 고정, 카메라 재량)</h3>{sfigs}
<h3>② 최종 합성 4교차</h3>{ffigs}
<h3>비교</h3>{cmp_figs}
<details><summary>프롬프트(스테이징 · 합성)</summary>
<pre>{esc(st['prompt'])}</pre><pre>{esc(c['prompt'])}</pre></details>
"""

    doc = f"""<!doctype html><html lang=ko><head><meta charset=utf-8>
<title>s32 — 접지된 스테이징 체인</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>s32 — 접지된 스테이징 (2026-07-09)</h1>
<p>스테이징을 그릴 때 촬영지 사진+약도를 첨부하고 지점·시간을 고정,
카메라 앵글만 t2i 재량. 그 스테이징+촬영지 사진+passport 로 최종 합성.
s30(장소 정확·구도 경직)/s31(구도 자유·장소 표류)의 절충 검증.</p>
{fig('out/forest_map/top2/top2_photo_fixed_nb2.png', '촬영지 사진(룩 SOT)', 31)}
{fig('out/forest_map/top2/top2_map_gpt.png', '약도(배치 SOT)', 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", "staging", "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 == "staging":
        stage_staging(engines, shots_filter)
    elif a.only == "compose":
        stage_compose(engines, shots_filter)
    else:
        stage_html()
