"""s21 blocking fp — 샷별 staging 마킹 fp(i2i) + 그 fp 를 참조에 추가한
스틸 재생성 비교 (사용자 지시, 2026-07-07).

  - 기반 = ⑯-2 산출(out/blockset/fpflat_marked_gpt.png — 완전 수직 fp +
    원형 알파벳 A~P 구조 마커, 원본 stairfix_nb2 계보).
  - 단계:
    ① 샷별 LLM(gpt) staging 저작 — 그 샷에 실제 등장하는 캐릭터/엔티티
      (동물 포함)의 top-down 위치와 카메라 위치·바라보는 방향을 알파벳
      요소 기준 상대 서술로. 발명 금지(무인이면 빈 배열)+evidence 인용.
      판단=LLM, 코드=조립만.
    ② gpt-image-2 edit: ⑯-2 fp 를 그대로 보존 + 원형 숫자(엔티티)와
      카메라 쐐기+화살표만 추가 — 샷당 1장. 마커 스타일=⑯-2 의
      원형 마커 계약(STYLE_2D_CIRCLED) 어휘 재사용.
    ③ nb2 스틸: s21_remake_bg 확정본(plans/outdoor_remake_bg.json)의
      프롬프트 **그대로**+같은 참조 구성(배경=stairfix_nb2 치환,
      스케치 제외분 유지)에 ② blocking fp 를 라벨드 참조로 **끝에 추가**
      (기존 image N 번호 보존). 새 참조 의미는 라벨 텍스트가 전달.
  - 산출: out/shots_outdoor/blocking/<샷>_blockfp.png + _blockstill.png,
    plans/outdoor_blocking_fp.json, blocking_fp.html(8897 — 3열 비교:
    원본/1차 배경교체/blocking, 샷별 fp+카메라+엔티티 작게).
  - DB write 0, 커밋 금지(scratchpad).
사용: backend/.venv/bin/python s21_blocking_fp.py \
        --shots S4_Shot1,S10_Shot5,S11_Shot4,S13_Shot3,S17_Shot1
"""
import argparse
import html as html_mod
import json
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).parent))
import forest_lib as F  # noqa: E402
import s20_numbered_bd as S20  # noqa: E402  (alpha_struct + ⑯-2 계약 차용)

ROOT = Path("/Users/manta/Documents/Projects/TheRoad-I1")
EXP = Path(__file__).parent
OUT = F.OUT / "shots_outdoor" / "blocking"
SRC_FP = F.OUT / "blockset" / "fpflat_marked_gpt.png"  # ⑯-2 산출
PAGE = EXP / "blocking_fp.html"

BLOCK_SYSTEM = "\n".join([
    "You are a film staging planner. You receive:",
    "- PLAN ELEMENTS: the lettered elements (circled letters) of a strict",
    "  top-down exterior site plan of one real location.",
    "- SHOT DATA: one shot's heading/description.",
    "- SHOT PROMPT: the full image-generation prompt actually used for",
    "  that shot (ground truth for what is on screen).",
    "",
    "Task: author the top-down STAGING for this exact shot on that plan.",
    "1. List every character/entity PHYSICALLY PRESENT ON SCREEN in this",
    "   shot per SHOT PROMPT / SHOT DATA only — people, animals, vehicles,",
    "   other movable subjects. Do NOT invent anyone; permanent structure",
    "   already on the plan is NOT an entity; if the shot is unmanned the",
    "   list is empty. Refer to each by a generic role wording taken from",
    "   the shot text (no proper names).",
    "2. For each entity give placement_en: its top-down position described",
    "   ONLY relative to the lettered plan elements (e.g. 'on F just",
    "   outside J, toward G'). If the shot text does not fix a position,",
    "   choose the position the shot's framing implies and mark",
    "   inferred=true.",
    "3. Camera: give its top-down position_en (relative to letters; it may",
    "   sit outside the drawn plan edge), facing_en (direction/target",
    "   letters or entity digits), and height_en (ground/eye-level/",
    "   elevated/overhead per the shot text).",
    "4. Quote evidence_ko: the shot-text wording (원문 인용) grounding each",
    "   entity and the camera. Keep every field short and factual — no",
    "   narrative, no mood, no invention beyond step 2's marked inference.",
    "Also give notes_ko: 한 줄 한국어 요약 (이 샷에서 누가/무엇이 어디에,",
    "카메라는 어디서 어디를 보는지).",
])

BLOCK_SCHEMA = {
    "type": "object",
    "additionalProperties": False,
    "properties": {
        "entities": {
            "type": "array",
            "items": {
                "type": "object",
                "additionalProperties": False,
                "properties": {
                    "name_en": {"type": "string"},
                    "name_ko": {"type": "string"},
                    "placement_en": {"type": "string"},
                    "inferred": {"type": "boolean"},
                    "evidence_ko": {"type": "string"},
                },
                "required": ["name_en", "name_ko", "placement_en",
                             "inferred", "evidence_ko"],
            },
        },
        "camera": {
            "type": "object",
            "additionalProperties": False,
            "properties": {
                "position_en": {"type": "string"},
                "facing_en": {"type": "string"},
                "height_en": {"type": "string"},
                "evidence_ko": {"type": "string"},
            },
            "required": ["position_en", "facing_en", "height_en",
                         "evidence_ko"],
        },
        "notes_ko": {"type": "string"},
    },
    "required": ["entities", "camera", "notes_ko"],
}

# ⑯-2 STYLE_2D_CIRCLED 의 원형 마커 어휘를 추가 마크에 재사용
BLOCKFP_HEAD = "\n".join([
    "The attached image is a strict top-down exterior SITE PLAN whose",
    "elements are identified by small plain CIRCLED LETTER markers.",
    "REPRODUCE the attached plan EXACTLY — same geometry, same colours,",
    "same circled letters, nothing moved, nothing redrawn — and ADD ONLY",
    "the shot-staging marks listed below.",
])

BLOCKFP_TAIL = "\n".join([
    "Marker style: each added entity mark is a small plain CIRCLED DIGIT",
    "(same graphic style as the existing circled letters), drawn once at",
    "its stated position. The camera is ONE small solid dark wedge",
    "(triangle) at its stated position with a short straight arrow showing",
    "its facing direction. Apart from the existing circled letters, these",
    "circled digits and the single camera wedge+arrow, add nothing: no",
    "text, no words, no other markers, no legend, no compass, no people,",
    "no objects.",
])

# 신규 참조 의미는 라벨이 전달 (본래 프롬프트 무변경 — 사용자 지시)
BLOCKING_REF_LABEL = (
    "shot STAGING PLAN (top-down diagram of this same location — "
    "annotation only: NEVER draw or copy this diagram, its flat colours, "
    "markers or top-down view into the photograph): circled letters = "
    "structure identifiers; each circled digit = where that on-screen "
    "character/entity stands; the single dark wedge with arrow = the "
    "camera's position and facing direction for THIS shot. Use it only to "
    "place subjects and the camera consistently with the prompt text.")


def blockfp_prompt(staging):
    lines = [BLOCKFP_HEAD, ""]
    ents = staging["entities"]
    if ents:
        lines.append("ENTITY MARKS (digits are identifiers only):")
        for i, e in enumerate(ents, 1):
            lines.append(f"{i}. {e['name_en']} — circled digit {i} at:"
                         f" {e['placement_en']}")
        lines.append("")
    cam = staging["camera"]
    lines.append("CAMERA MARK:")
    lines.append(f"- position: {cam['position_en']}")
    lines.append(f"- facing: {cam['facing_en']}")
    lines.append("")
    lines.append(BLOCKFP_TAIL)
    return "\n".join(lines)


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--shots", required=True)
    ap.add_argument("--only", default="all",
                    choices=["all", "llm", "blockfp", "still", "html"])
    args = ap.parse_args()
    shot_keys = [s.strip() for s in args.shots.split(",") if s.strip()]
    assert SRC_FP.exists(), f"⑯-2 산출 없음: {SRC_FP}"

    res = F.load_plan("numbered_bd_v1")
    alpha_lines, alpha_legend = S20.alpha_struct(res)
    remake = F.load_plan("outdoor_remake_bg")
    jobs = {j["shot_key"]: j for j in remake["jobs"]}
    collect = F.load_plan("outdoor_shots_collect")["shots"]
    for k in shot_keys:
        assert k in jobs, f"remake plan 에 없는 샷: {k}"

    try:
        plan = F.load_plan("outdoor_blocking_fp")
    except Exception:
        plan = {"src_fp": str(SRC_FP), "shots": {}}
    OUT.mkdir(parents=True, exist_ok=True)

    # ① 샷별 staging 저작 (LLM)
    if args.only in ("all", "llm"):
        for k in shot_keys:
            if k in plan["shots"] and plan["shots"][k].get("staging"):
                continue
            s = collect[k]
            user = "\n\n".join([
                "PLAN ELEMENTS (circled letters on the plan):\n"
                + "\n".join(alpha_lines),
                f"SHOT DATA:\nscene_heading: {s['scene_heading']}\n"
                f"description: {s['description']}",
                "SHOT PROMPT (full, ground truth):\n" + jobs[k]["prompt"],
            ])
            staging = F.llm(f"s21_blocking_{k}", BLOCK_SYSTEM, user,
                            BLOCK_SCHEMA)
            plan["shots"].setdefault(k, {})["staging"] = staging
            F.save_plan("outdoor_blocking_fp", plan)
            build_page(plan, shot_keys, alpha_legend)

    # ② blocking fp (gpt-image-2 edit, ⑯-2 fp 보존 + 마크 추가)
    if args.only in ("all", "blockfp"):
        for k in shot_keys:
            staging = plan["shots"][k]["staging"]
            p = blockfp_prompt(staging)
            plan["shots"][k]["blockfp_prompt"] = p
            F.save_plan("outdoor_blocking_fp", plan)
            F.img_gpt(f"s21_blockfp_{k}", p, refs=[SRC_FP],
                      out_path=OUT / f"{k}_blockfp.png")
            build_page(plan, shot_keys, alpha_legend)

    # ③ nb2 스틸 — remake 확정 프롬프트 그대로 + blocking fp 참조를 끝에 추가
    if args.only in ("all", "still"):
        bg = Path(remake["new_bg"])
        for k in shot_keys:
            j = jobs[k]
            labeled = []
            for r in j["refs"]:
                if r.get("dropped"):
                    continue
                path = bg if r.get("swapped") else ROOT / r["file_path"]
                labeled.append((r["label"], path))
            labeled.append((BLOCKING_REF_LABEL, OUT / f"{k}_blockfp.png"))
            F.img_nb2(f"s21_blockstill_{k}", j["prompt"], labeled,
                      out_path=OUT / f"{k}_blockstill.png")
            plan["shots"][k]["still_prompt"] = j["prompt"]
            plan["shots"][k]["still_ref_labels"] = [l for l, _ in labeled]
            F.save_plan("outdoor_blocking_fp", plan)
            build_page(plan, shot_keys, alpha_legend)

    build_page(plan, shot_keys, alpha_legend)
    F.runlog({"kind": "stage", "stage": "s21_blocking_fp", "done": args.only,
              "shots": shot_keys})
    print(f"done -> {OUT}\npage -> {PAGE}")


def build_page(plan, shot_keys, alpha_legend):
    def _esc(t):
        return html_mod.escape(str(t or ""))

    def _img(path_rel, cap, cls=""):
        p = EXP / path_rel
        if not p.exists():
            return (f"<div class='{cls}'><div class='pending'>생성 중…</div>"
                    f"<p class='cap'>{_esc(cap)}</p></div>")
        return (f"<div class='{cls}'><img src='{_esc(path_rel)}'>"
                f"<p class='cap'>{_esc(cap)}</p></div>")

    leg_rows = "".join(
        f"<tr><td><b>{_esc(r['letter'])}</b></td>"
        f"<td>{_esc(r.get('name_en'))}</td>"
        f"<td>{_esc(r.get('desc_ko'))}</td></tr>" for r in alpha_legend)

    secs = []
    for k in shot_keys:
        sh = plan["shots"].get(k) or {}
        st = sh.get("staging")
        if not st:
            secs.append(f"<h2>{_esc(k)}</h2><p class='pending'>staging"
                        " 저작 중…</p>")
            continue
        circled = "①②③④⑤⑥⑦⑧⑨⑩⑪⑫"
        ent_rows = "".join(
            f"<tr><td>{circled[i]}</td>"
            f"<td>{_esc(e['name_ko'])} ({_esc(e['name_en'])})</td>"
            f"<td>{_esc(e['placement_en'])}"
            f"{' <i>(추정)</i>' if e.get('inferred') else ''}</td>"
            f"<td class='ev'>{_esc(e['evidence_ko'])}</td></tr>"
            for i, e in enumerate(st["entities"]))
        cam = st["camera"]
        secs.append(f"""
<h2>{_esc(k)}</h2>
<p class='note'>{_esc(st['notes_ko'])}</p>
<div class='trio'>
  {_img(f"out/shots_outdoor/{plan_orig(k)}", '원본 최신 스틸 (기존 배경)')}
  {_img(f"out/shots_outdoor/remake/{k}_bgswap.png", '1차 remake (배경만 교체)')}
  {_img(f"out/shots_outdoor/blocking/{k}_blockstill.png",
        '신규 (배경 교체 + blocking fp 참조)')}
</div>
<div class='refrow'>
  {_img(f"out/shots_outdoor/blocking/{k}_blockfp.png",
        'blocking fp — 엔티티(원형 숫자)+카메라(쐐기·화살표)', 'ref')}
  {_img('out/blockset/stairfix_nb2.png', '배경 참조 stairfix_nb2', 'ref')}
  {_img('out/blockset/fpflat_marked_gpt.png', '⑯-2 원본 marked fp', 'ref')}
</div>
<table><tr><th>마커</th><th>엔티티</th><th>배치(top-down)</th><th>근거 인용</th></tr>
{ent_rows or "<tr><td colspan='4'>무인 샷 — 엔티티 없음</td></tr>"}
<tr><td>▲</td><td>카메라 ({_esc(cam['height_en'])})</td>
<td>위치: {_esc(cam['position_en'])}<br>방향: {_esc(cam['facing_en'])}</td>
<td class='ev'>{_esc(cam['evidence_ko'])}</td></tr></table>
<details><summary>blocking fp i2i 프롬프트 (gpt edit)</summary>
<pre>{_esc(sh.get('blockfp_prompt'))}</pre></details>
<details><summary>스틸 프롬프트 (remake 확정본 그대로) + 참조 라벨</summary>
<pre>{_esc(sh.get('still_prompt'))}\n\n--- ref labels ---\n{_esc(
    json.dumps(sh.get('still_ref_labels'), ensure_ascii=False, indent=1))}
</pre></details>
""")
    PAGE.write_text(f"""<!doctype html><meta charset='utf-8'>
<title>s21 blocking fp — 샷별 staging 마킹 fp + 스틸 재생성</title>
<style>
body{{font-family:system-ui,'Apple SD Gothic Neo',sans-serif;margin:24px;
background:#fafafa;color:#222;max-width:1560px}}
h1{{font-size:20px}} h2{{font-size:16px;margin-top:36px;border-bottom:2px
solid #ddd;padding-bottom:4px}}
img{{width:100%;display:block;background:#fff;border:1px solid #ddd;
padding:3px;box-sizing:border-box}}
.trio{{display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px}}
.cap{{font-size:12px;color:#333;margin:4px 0 10px;font-weight:600}}
.refrow{{display:grid;grid-template-columns:repeat(5,1fr);gap:8px;
margin:6px 0}}
.refrow .cap{{font-size:10.5px;color:#666;font-weight:400}}
.note{{font-size:13px;color:#345;background:#eef3f8;padding:6px 10px;
border-radius:6px}}
table{{border-collapse:collapse;font-size:12px;margin:8px 0;width:100%}}
td,th{{border:1px solid #ddd;padding:4px 8px;text-align:left;
vertical-align:top}}
.ev{{color:#666;font-size:11px}}
.pending{{padding:40px 10px;text-align:center;color:#999;background:#fff;
border:1px dashed #ccc;font-size:12px}}
pre{{white-space:pre-wrap;font-size:11px;background:#fff;border:1px solid
#e5e5e5;padding:10px}}
details{{margin:8px 0}} summary{{font-size:12px;color:#557;cursor:pointer}}
</style>
<h1>s21 blocking fp — ⑯-2 수직 fp(알파벳 마커) 위에 샷별 엔티티(원형 숫자)
+카메라(쐐기·화살표) i2i → 그 fp+새 배경(stairfix_nb2)을 참조로 기존
프롬프트 그대로 스틸 재생성</h1>
<p style='font-size:12.5px;color:#556'>3열: 좌=원본 최신 스틸 / 중=1차
remake(배경만 교체) / 우=신규(배경 교체+blocking fp 참조 추가, 프롬프트
불변 — 새 참조 의미는 라벨 텍스트 전달). staging 판단=LLM(gpt),
마커 부여·조립=코드. 매핑=plans/outdoor_blocking_fp.json</p>
<details><summary>⑯-2 알파벳 범례 (A~{alpha_legend[-1]['letter']
    if alpha_legend else ''})</summary>
<table><tr><th>글자</th><th>요소</th><th>서술</th></tr>{leg_rows}</table>
</details>
{''.join(secs)}
""", encoding="utf-8")


def plan_orig(k):
    # 원본 최신 스틸 파일명 = remake plan 기록 재사용
    remake = F.load_plan("outdoor_remake_bg")
    return next(j["orig_file"] for j in remake["jobs"]
                if j["shot_key"] == k)


if __name__ == "__main__":
    main()
