"""s8 — 4라운드(사용자 재정의): 건물 전체 통합 도면 + 실외샷 1개(무마킹·무인).

사용자 의도(2026-07-05): 뿌리 = 옥탑방 레벨만이 아니라 **옥탑방이 얹힌 건물
전체** — 지상 출입구/마당(인물들이 도착하는 쪽), 외부 계단, 옥상 레벨 — 를
한 장의 top-down 도면으로. 그걸 기반으로 필요한 실외샷 하나를 카메라/엔티티
없는 빈 형태로 생성. 거기까지만.

기하 앵커 = 그룹의 실외 fp 전부(옥상 레벨 + 지상 마당 + 계단). 요소 선정은
LLM(전 샷 내용 원문), 시나리오 중립.
사용: .venv/bin/python s8_root_v2.py [--only plan|root|readback|shot]
산출: plans/root_plan_v2.json, out/root_v2/root_v2.png, out/outdoor_d/<sk>/
"""
import argparse
import json
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).parent))
import forest_lib as F  # noqa: E402
import forest_chain as C  # noqa: E402
import s1_root as S1  # noqa: E402

OUTR = F.OUT / "root_v2"

R1V2_SYSTEM = """You are the master planner for a group of connected filming locations
(one real-world place: a building and its grounds, with indoor and outdoor parts).
You are given the place's member descriptions and the FULL content of every shot.

Design ONE comprehensive TOP-DOWN site plan of the WHOLE property — not just its
top level. The plan must show, together in one drawing:
- the full BUILDING footprint (the multi-storey building the upper dwelling sits on),
- the GROUND level around it: the yard/alley, the building's street-side entrance
  and the approach path where characters arrive,
- the exterior STAIR route connecting ground to the top level,
- the TOP level: the upper dwelling's roof outline, its terrace and fixtures,
- a modest margin of the immediate surroundings.

Select the elements to mark on this one plan. Hard rules:
- NEVER expose interior floor plans or interior furniture; enclosed spaces show
  only their outline and boundary openings (doors, windows, gates).
- Cover every shot: for each shot, the elements needed to locate its action.
- At most 12 elements, each with a single capital letter ID and a SHORT English
  name (1-3 words). Placement phrases must be relative and physical, grounded in
  the given data only. Generic physical terms, no proper names."""


def r1_v2(recon):
    members = F.members_block(recon["members"])
    shot_blocks = "\n\n".join(
        F.shot_block(k, s) for k, s in sorted(recon["shots"].items()))
    user = ("PLACE MEMBERS:\n" + members
            + "\n\nALL SHOTS AT THIS PLACE:\n\n" + shot_blocks
            + "\n\nDesign the whole-property site plan element set now.")
    plan = F.llm("forest_root_plan_v2", R1V2_SYSTEM, user, S1.R1_SCHEMA,
                 model="gpt")
    F.save_plan("root_plan_v2", plan)
    covered = {c["shot_key"] for c in plan.get("coverage", [])}
    print(f"R1v2: {len(plan['elements'])} elements, coverage "
          f"{len(covered)}/{len(recon['shots'])}")
    for e in plan["elements"]:
        print(f"  ({e['id']}) {e['name_en']} [{e['category']}]")
    return plan


def exterior_fps(recon):
    """그룹 실외 loc 의 fp 전부 (레벨별 기하 앵커) — 데이터 순서 결정론."""
    fps = []
    for m in recon["members"]:
        if m["is_indoor"]:
            continue
        for fp in (recon["fp_by_loc"].get(m["loc_id"]) or []):
            fps.append((m["loc_id"], fp["fp_id"], fp["png_path"]))
    return fps


def root_v2(recon, plan):
    fps = exterior_fps(recon)
    if not fps:
        raise SystemExit("no exterior fps")
    ref_lines = "\n".join(
        f"- attached image #{i + 1}: top-down plan of one part of the property"
        f" (its geometry for that part is ground truth)"
        for i in range(len(fps)))
    el_lines = "\n".join(f"({e['id']}) {e['name_en']} — {e['placement']}"
                         for e in plan["elements"])
    legend_lines = ", ".join(f"{e['id']}: {e['name_en'].upper()}"
                             for e in plan["elements"])
    prompt = "\n".join([
        "ONE hand-drawn TOP-DOWN aerial SITE PLAN of a single real-world",
        "property, seen straight from above: dark ink outlines with light flat",
        "colour washes. NOT a photograph, NOT 3D, no perspective.",
        "",
        "Draw the WHOLE property in one coherent drawing:",
        "- the full multi-storey BUILDING footprint (roof plane seen from above),",
        "- the upper-level dwelling on that roof: its outline, terrace, fixtures",
        "  (draw ONLY what its attached plan shows — never interior rooms),",
        "- the exterior STAIR route connecting the ground to the top level,",
        "- the GROUND level around the building: the yard/alley, the street-side",
        "  building ENTRANCE and the approach path where people arrive,",
        "- a modest margin of neighbouring structures and the street.",
        "Make the levels readable from above: the building's roof edge/parapet",
        "outline separates the top level from the ground around it; the stair",
        "visibly connects them; the ground entrance sits on the building's",
        "street side.",
        "",
        "The attached images are the property's real top-down plans — compose",
        "them into this ONE plan, keeping each part's geometry, proportions and",
        "opening positions EXACTLY (do not move, resize, mirror or invent):",
        ref_lines,
        "",
        "THE PLACE (production data):",
        F.members_block(recon["members"]),
        "",
        "Mark each element with a small circle containing its capital letter:",
        el_lines,
        "",
        "On the RIGHT side, add a clean legend panel listing each circled letter",
        "with its English name in small neat capital letters:",
        legend_lines,
        "",
        "The circled letters and the legend are the ONLY text and the ONLY",
        "circular marks allowed. No zone captions, no numbers, no other labels.",
    ])
    return F.img_gpt("root_v2_whole_property", prompt,
                     refs=[p for _, _, p in fps],
                     out_path=OUTR / "root_v2.png")


def pick_ground_shot(recon):
    """실외샷 1개 선택 — 데이터 규칙: 지상 접근 loc(실외 & anchor 아닌 실외
    멤버 중 shot 최소 loc = 접근/입구 성격) 샷 우선, 없으면 wide 실외."""
    outdoor = [(k, s) for k, s in sorted(recon["shots"].items())
               if not s["is_indoor"]]
    # 접근 성격 loc = 실외 loc 중 fp 가 2개(레벨 분할: 마당+계단)이거나 샷 수 최소
    loc_counts = {}
    for _, s in outdoor:
        for l in s["loc_ids"]:
            loc_counts[l] = loc_counts.get(l, 0) + 1
    ground_loc = min(loc_counts, key=lambda l: loc_counts[l])
    cands = [(k, s) for k, s in outdoor if ground_loc in s["loc_ids"]]
    # framing wide 우선
    cands.sort(key=lambda kv: 0 if (kv[1].get("staging") or {})
               .get("framing_scale") == "wide" else 1)
    return cands[0][0]


def shot_plate(recon, plan, sk):
    s = recon["shots"][sk]
    d = F.OUT / "outdoor_d" / sk
    d.mkdir(parents=True, exist_ok=True)
    root_png = OUTR / "root_v2.png"
    # camview 라인아트 — 마킹 없이 staging 텍스트로 카메라 지정 (hop, 검증된 시점 변환)
    sketch_prompt = "\n".join([
        "The attached image is a TOP-DOWN site plan of one real property",
        "(circled letters are site markers, never people).",
        "",
        "Draw the clean LINE-ART sketch of the EYE-LEVEL view described below:",
        "thin dark ink lines on plain white, no colour, no shading. The camera",
        "stands ON the ground INSIDE the scene at human eye height (about",
        "1.6 m) — NOT above it. Correct perspective at real-world human scale,",
        "horizon at natural eye level. Show the structures, surfaces, openings",
        "and approaches the plan places in front of that camera.",
        "No people, no text, no letters, no circles — pure line drawing.",
        "",
        "CAMERA (from production staging):",
        str((s.get("staging") or {}).get("camera_direction") or ""),
        C.staging_lines(s),
    ])
    sketch = F.img_gpt(f"{sk}_camview_v2", sketch_prompt, refs=[root_png],
                       out_path=d / "sketch.png")
    locs = [m for m in recon["members"] if m["loc_id"] in s["loc_ids"]]
    loc_lines = "\n".join(f"- {m.get('label')}: {m.get('summary')}" for m in locs)
    plate_prompt = "\n".join([
        "The FIRST attached image is a LINE-ART sketch of one camera's",
        "eye-level view of a real outdoor place — composition and perspective",
        "ground truth. The SECOND attached image is the property's top-down",
        "site plan — use it ONLY for spatial relations and fixtures; never",
        "copy its top-down viewpoint or markers.",
        "",
        "Render the PHOTOREALISTIC EMPTY view exactly as the line-art frames",
        "it: same framing, perspective, structures, real-world human scale.",
        "Completely unpopulated — no people, no figures. No text, letters,",
        "circles, arrows or sketch lines. Pure photographic realism, natural",
        "weathered materials.",
        "",
        "WHAT THIS PLACE LOOKS LIKE (production data):",
        loc_lines,
        "", C.staging_lines(s), "", C.EYE_LEVEL_CONTRACT,
    ])
    F.img_gpt(f"{sk}_plate_v2root", plate_prompt, refs=[sketch, root_png],
              out_path=d / "plate.png")


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--only", default="all",
                    choices=["all", "plan", "root", "readback", "shot"])
    ap.add_argument("--shot", default=None)
    args = ap.parse_args()
    recon = F.load_recon()
    if args.only in ("all", "plan"):
        plan = r1_v2(recon)
    else:
        plan = F.load_plan("root_plan_v2")
    if args.only in ("all", "root"):
        root_v2(recon, plan)
    if args.only in ("all", "readback"):
        png = OUTR / "root_v2.png"
        if png.exists():
            user = [{"type": "text", "text":
                     "This is a top-down site plan with circled capital letters"
                     " and a legend panel. List every circled capital letter,"
                     " list the legend entries as written, and say whether any"
                     " interior room layout is exposed."},
                    F.png_data_url(png)]
            rb = F.llm("forest_root_readback", "You read technical plans precisely.",
                       user, S1.READBACK_SCHEMA, model="gpt")
            F.save_plan("root_readback_v2", rb)
            print("readback v2:", rb.get("letters_found"),
                  "interior:", rb.get("interior_layout_exposed"))
    if args.only in ("all", "shot"):
        sk = args.shot or pick_ground_shot(recon)
        print("chosen outdoor shot:", sk)
        shot_plate(recon, plan, sk)
    F.runlog({"kind": "stage", "stage": "s8_root_v2", "done": args.only})


if __name__ == "__main__":
    main()
