"""s19 — 공간지각 중심 재설계 (사용자 재정의, 2026-07-05 저녁):

  - **Stage 1 (LLM, gpt-5.5)**: 씬 원문 전체+장소 멤버+제작자 정정 → 사람이
    공간을 지각하듯 **관계 중심 공간 서술** 생성 — 포함(무엇이 무엇 위/안에),
    상대 위치(어느 부분), 상대 크기(부모 대비), 층수/높이(추정 표시), 이격,
    동선. 좌표/치수/요소 설명/재질/무드 전부 금지. EN(렌더 소비)+KO(열람).
  - **Stage 2**: 같은 SPATIAL LAYOUT 줄들을 공유하고 스타일 계약만 분기 —
    bd(블록 다이어그램, s18 STYLE_FLAT)와 fp(도면, s15 STYLE_2D+스타일 ref)를
    **각각 직행 T2I** 생성. fp 는 bd 이미지를 참조하지 않는다(체인 아님).
  - 산출: out/blockset/spatial_{bd,fp}_{gpt,nb2}.png, plans/spatial_layout_v1,
    spatial.html (부분 게시 + 30s 새로고침).
사용: .venv/bin/python s19_spatial.py [--only all|llm|images|html]
"""
import argparse
import datetime
import html as html_mod
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).parent))
import forest_lib as F  # noqa: E402
import s13_blockset_v2 as S13  # noqa: E402
import s14_blockgen as S14  # noqa: E402
import s15_fp_redo as S15  # noqa: E402
import s18_blocks_lean as S18  # noqa: E402

OUTB = F.OUT / "blockset"
PAGE = Path(__file__).parent / "spatial.html"

SPATIAL_SYSTEM = "\n".join([
    "You are analysing ONE real filming location from the FULL ORIGINAL",
    "SCENE TEXTS and place-member data given below. Write its SPATIAL",
    "STRUCTURE the way a person perceives a space — relationships, not an",
    "inventory of parts:",
    "",
    "- CONTAINMENT: what stands on / sits inside what (ground, building,",
    "  open decks, units, fixtures).",
    "- RELATIVE POSITION: which part of its parent each thing occupies",
    "  (front/back, left/right, corner/edge/centre). Pick ONE consistent",
    "  viewpoint for the whole property and keep it throughout.",
    "- RELATIVE SIZE: rough share of the parent (about a third of the deck,",
    "  a one-room hut, a stride wide). NO coordinates, NO metric dimensions.",
    "- LEVELS & HEIGHT: how many storeys the building has (estimate when the",
    "  text is silent — put it in the inferred notes), which level each open",
    "  area or unit sits on, what is higher than what.",
    "- SPACING: roughly how far apart neighbouring things are (steps, arm",
    "  spans), and what directly touches or abuts what.",
    "- CIRCULATION: the path a person walks through the property",
    "  (street -> entrance -> ... -> top), naming which side each link is on.",
    "",
    "Rules:",
    "- One spatial fact per line, plain present tense, roughly 8-18 words.",
    "- Keep it LEAN: 12-22 lines covering every permanent structure the",
    "  scenes rely on — nothing else.",
    "- Never define or describe what things are: no materials, colours,",
    "  mood, story props, people. Names + spatial relations only.",
    "- Never reveal the interior room layout of any enclosed unit.",
    "- spatial_layout_ko: the same lines in Korean, same order.",
    "- evidence: for facts grounded in the texts, cite source (scene index /",
    "  member id) + the exact quote. Assumptions go to inferred_notes.",
    "- A creator-correction block, when present, overrides the scene texts.",
])

SPATIAL_SCHEMA = {
    "type": "object",
    "additionalProperties": False,
    "properties": {
        "spatial_layout_en": {"type": "array", "items": {"type": "string"}},
        "spatial_layout_ko": {"type": "array", "items": {"type": "string"}},
        "inferred_notes_en": {"type": "array", "items": {"type": "string"}},
        "inferred_notes_ko": {"type": "array", "items": {"type": "string"}},
        "evidence": {
            "type": "array",
            "items": {
                "type": "object",
                "additionalProperties": False,
                "properties": {
                    "about_en": {"type": "string"},
                    "source": {"type": "string"},
                    "quote": {"type": "string"},
                },
                "required": ["about_en", "source", "quote"],
            },
        },
    },
    "required": ["spatial_layout_en", "spatial_layout_ko",
                 "inferred_notes_en", "inferred_notes_ko", "evidence"],
}


def corrections_block():
    ov = S14.load_overrides()
    lines = list(ov.get("facts_extra") or [])
    if not lines:
        return ""
    return ("CREATOR CORRECTIONS (override the scene texts where they"
            " conflict):\n" + "\n".join(lines) + "\n\n")


def run_llm(recon):
    idxs, scenes = S13.scene_texts(recon)
    user = (corrections_block()
            + "PLACE MEMBERS (production data):\n" + S14.loc_lines(recon)
            + "\n\nFULL ORIGINAL SCENE TEXTS (every scene at this place):\n\n"
            + scenes)
    sp = F.llm("s19_spatial_extract", SPATIAL_SYSTEM, user, SPATIAL_SCHEMA)
    F.save_plan("spatial_layout_v1", sp)
    print(f"spatial: scenes {idxs} | layout {len(sp['spatial_layout_en'])}줄"
          f" | evidence {len(sp['evidence'])}건")
    return sp


def bullet(lines):
    return "\n".join(f"- {l}" for l in lines)


def bd_prompt(sp):
    return (S18.STYLE_FLAT
            + "\n\nSPATIAL LAYOUT — what is where (containment, relative"
            " position & size, levels, spacing). Draw exactly these"
            " relations as blocks:\n" + bullet(sp["spatial_layout_en"]))


def fp_prompt(sp):
    return "\n\n".join([
        "SPATIAL LAYOUT — what is where (containment, relative position &"
        " size, levels, spacing). Honour every relation below; compose the"
        " exact plan geometry yourself:\n" + bullet(sp["spatial_layout_en"]),
        S15.STYLE_REF_NOTE,
        S15.STYLE_2D,
    ])


def run_images(sp, recon):
    prompts = {}
    try:
        prompts = F.load_plan("spatial_prompts")
    except Exception:
        pass
    pb = bd_prompt(sp)
    pf = fp_prompt(sp)
    prompts.update({
        "spatial_bd_gpt.png": pb, "spatial_bd_nb2.png": pb,
        "spatial_fp_gpt.png": pf, "spatial_fp_nb2.png": pf,
    })
    F.save_plan("spatial_prompts", prompts)
    build_page(sp, prompts)
    F.img_nb2("s19_spatial_bd_nb2", pb, [],
              out_path=OUTB / "spatial_bd_nb2.png")
    build_page(sp, prompts)
    F.img_gpt("s19_spatial_bd_gpt", pb, refs=None,
              out_path=OUTB / "spatial_bd_gpt.png")
    build_page(sp, prompts)
    fp_style = S14.indoor_fp(recon)
    refs_nb2 = [(S14.NB2_STYLE_LABEL, fp_style)] if fp_style else []
    F.img_nb2("s19_spatial_fp_nb2", pf, refs_nb2,
              out_path=OUTB / "spatial_fp_nb2.png")
    build_page(sp, prompts)
    F.img_gpt("s19_spatial_fp_gpt", pf,
              refs=[fp_style] if fp_style else None,
              out_path=OUTB / "spatial_fp_gpt.png")
    build_page(sp, prompts)


def _esc(t):
    return html_mod.escape(str(t))


def _img_cell(fname, title):
    p = OUTB / fname
    if p.exists():
        ts = datetime.datetime.fromtimestamp(p.stat().st_mtime)
        return (f"<figure><img src='out/blockset/{fname}?v={int(ts.timestamp())}'>"
                f"<figcaption>{_esc(title)} — {fname}"
                f" ({ts:%H:%M:%S})</figcaption></figure>")
    return (f"<figure><div class='pending'>생성 중…</div>"
            f"<figcaption>{_esc(title)} — {fname}</figcaption></figure>")


def build_page(sp, prompts=None):
    rows_layout = "".join(
        f"<tr><td>{_esc(ko)}</td><td class='en'>{_esc(en)}</td></tr>"
        for ko, en in zip(sp["spatial_layout_ko"], sp["spatial_layout_en"]))
    rows_inf = "".join(
        f"<tr><td>{_esc(ko)}</td><td class='en'>{_esc(en)}</td></tr>"
        for ko, en in zip(sp["inferred_notes_ko"], sp["inferred_notes_en"]))
    rows_ev = "".join(
        f"<tr><td>{_esc(e['about_en'])}</td><td>{_esc(e['source'])}</td>"
        f"<td class='q'>{_esc(e['quote'])}</td></tr>"
        for e in sp["evidence"])
    prom_html = ""
    if prompts:
        prom_html = "".join(
            f"<details><summary>{_esc(k)}</summary><pre>{_esc(v)}</pre></details>"
            for k, v in sorted(prompts.items()))
    all_done = all((OUTB / f).exists() for f in (
        "spatial_bd_gpt.png", "spatial_bd_nb2.png",
        "spatial_fp_gpt.png", "spatial_fp_nb2.png"))
    refresh = "" if all_done else "<meta http-equiv='refresh' content='30'>"
    page = f"""<!doctype html><meta charset='utf-8'>
{refresh}
<title>s19 공간지각 — bd/fp 직행</title>
<style>
body{{font-family:system-ui,'Apple SD Gothic Neo',sans-serif;margin:24px;
background:#fafafa;color:#222;max-width:1500px}}
h1{{font-size:22px}} h2{{font-size:17px;margin-top:28px;border-bottom:2px solid #ddd;
padding-bottom:4px}}
table{{border-collapse:collapse;width:100%;font-size:13.5px;background:#fff}}
td,th{{border:1px solid #e0e0e0;padding:5px 9px;vertical-align:top}}
td.en{{color:#667;font-size:12.5px}} td.q{{color:#865;font-size:12.5px}}
.grid{{display:grid;grid-template-columns:1fr 1fr;gap:14px}}
figure{{margin:0;background:#fff;border:1px solid #ddd;padding:8px}}
img{{width:100%;display:block}}
figcaption{{font-size:12px;color:#555;padding-top:6px}}
.pending{{display:flex;align-items:center;justify-content:center;height:280px;
color:#999;font-size:15px;background:repeating-linear-gradient(45deg,#f4f4f4,
#f4f4f4 12px,#fcfcfc 12px,#fcfcfc 24px)}}
pre{{white-space:pre-wrap;background:#f4f4f4;padding:10px;font-size:12px}}
.note{{background:#fff8e6;border:1px solid #eeddaa;padding:10px 14px;
font-size:13.5px}}
</style>
<h1>s19 — 공간지각 중심: LLM 공간 서술 → bd / fp 각각 직행</h1>
<div class='note'>재설계(사용자 재정의): LLM 이 씬 원문에서 <b>사람의 공간지각처럼
관계 중심 서술</b>(포함·상대 위치·상대 크기·층수/높이·이격·동선)을 생성하고,
<b>같은 배치 정보</b>를 공유한 채 스타일 계약만 분기해 bd 와 fp 를 <b>각각 직행</b>
생성(fp 는 bd 를 참조하지 않음). 좌표/나열/요소 설명 없음.</div>
<h2>① LLM 공간 서술 (한국어 = 영어 원문 번역, 같은 순서)</h2>
<table><tr><th>한국어</th><th>EN (렌더 입력 원문)</th></tr>{rows_layout}</table>
<h3>추정 노트 (원문 무근거 — LLM 추정 표시)</h3>
<table><tr><th>한국어</th><th>EN</th></tr>{rows_inf or "<tr><td colspan=2>없음</td></tr>"}</table>
<h3>근거 인용 (evidence)</h3>
<table><tr><th>대상</th><th>출처</th><th>인용</th></tr>{rows_ev}</table>
<h2>② 블록 다이어그램 (bd) — 같은 SPATIAL LAYOUT + STYLE_FLAT</h2>
<div class='grid'>{_img_cell('spatial_bd_gpt.png', 'gpt-image-2')}
{_img_cell('spatial_bd_nb2.png', 'nb2')}</div>
<h2>③ 도면 (fp) — 같은 SPATIAL LAYOUT + STYLE_2D + 실내 fp 스타일 ref (bd 미경유)</h2>
<div class='grid'>{_img_cell('spatial_fp_gpt.png', 'gpt-image-2')}
{_img_cell('spatial_fp_nb2.png', 'nb2')}</div>
<h2>④ 실전송 프롬프트</h2>
{prom_html}
<p style='color:#999;font-size:12px'>30초 자동 새로고침 · 생성 순서: bd_nb2 →
bd_gpt → fp_nb2 → fp_gpt</p>
"""
    PAGE.write_text(page, encoding="utf-8")
    print(f"page -> {PAGE}")


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--only", default="all",
                    choices=["all", "llm", "images", "html"])
    args = ap.parse_args()
    recon = F.load_recon()
    if args.only in ("all", "llm"):
        sp = run_llm(recon)
    else:
        sp = F.load_plan("spatial_layout_v1")
    try:
        prompts = F.load_plan("spatial_prompts")
    except Exception:
        prompts = None
    build_page(sp, prompts)
    if args.only in ("all", "images"):
        run_images(sp, recon)
    F.runlog({"kind": "stage", "stage": "s19_spatial", "done": args.only})


if __name__ == "__main__":
    main()
