"""s11 — 6라운드: 새 뿌리(v3, fp 기하×fp형 도면+축측) 기반 실외 체인 재구동.

검증된 조합(2라운드 v3.1 + 4라운드 + FINDINGS):
  - 무인물: camview sketch-hop → photoreal plate (v2 계약 3줄). + 균일 A/B 로
    plate_direct(뿌리+staging 텍스트만, 눈높이 계약 없음) 대조 — 부감 staging 샷
    (예: 데이터가 요구하는 고공 establishing)은 직행이 정답일 수 있음을 검증.
  - 인물/근거리: 임시샷 유용 — temp(ENV=plate, CONTROL=눈높이 스케치 — 2라운드
    "top-down control 재주입 금지" 규칙) → 윤곽(C 경로) → 배경 재생성 → 최종샷.
샷 선정(데이터 규칙, 시나리오 중립): subset.json outdoor + s8 지상샷 규칙 +
인물 최다 실외샷(동수는 framing medium 우선 → 사전순).
사용: .venv/bin/python s11_outdoor_r6.py [--root a|b|c] [--only shots|noperson|person]
산출: out/outdoor_r6/<sk>/, plans/r6_shots.json, round6.html 갱신
"""
import argparse
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 s8_root_v2 as S8  # noqa: E402
import r6_page  # noqa: E402

OUTD = F.OUT / "outdoor_r6"

LBL_CONTROL_EYE = (
    "CAMERA AND COMPOSITION GUIDE (eye-level line-art sketch) — its framing,"
    " camera height and perspective are the composition ground truth for this"
    " shot. Follow that composition exactly; NEVER copy its line style or any"
    " sketch lines into the output.")


def pick_shots(recon):
    """대표 실외샷 선정 — 전부 데이터 규칙 (플랜 파일 + 구조 필드)."""
    subset = list(F.load_plan("subset")["outdoor"])
    ground = S8.pick_ground_shot(recon)
    outdoor = [(k, s) for k, s in sorted(recon["shots"].items())
               if not s["is_indoor"]]
    with_chars = [(k, s) for k, s in outdoor if s.get("characters")]

    def rank(kv):
        k, s = kv
        fr = (s.get("staging") or {}).get("framing_scale")
        return (-len(s["characters"]), 0 if fr == "medium" else 1, k)

    multi = sorted(with_chars, key=rank)[0][0] if with_chars else None
    keys = []
    for k in subset + [ground] + ([multi] if multi else []):
        if k not in keys:
            keys.append(k)
    no_person = [k for k in keys if not recon["shots"][k].get("characters")]
    person = [k for k in keys if recon["shots"][k].get("characters")]
    return no_person, person


def sketch_prompt(s) -> str:
    return "\n".join([
        "The attached image is a SITE PLAN of one real property, drawn at a",
        "high oblique angle (circled letters and the legend panel 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),
    ])


def loc_lines(recon, s) -> str:
    locs = [m for m in recon["members"] if m["loc_id"] in s["loc_ids"]]
    return "\n".join(f"- {m.get('label')}: {m.get('summary')}" for m in locs)


def plate_hop_prompt(recon, s, contracts) -> str:
    return "\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 site plan",
        "(high-oblique drawing) — use it ONLY for spatial relations, levels",
        "and fixtures; never copy its drawing style, 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(recon, s),
        "", C.staging_lines(s), "",
    ] + contracts)


def plate_direct_prompt(recon, s, contracts) -> str:
    return "\n".join([
        "The attached image is the SITE PLAN of one real property, drawn at a",
        "high oblique angle. Its geometry, levels and fixtures are the spatial",
        "ground truth. Circled letters and the legend panel are site markers",
        "only — never people or objects; render none of them.",
        "",
        "Render the PHOTOREALISTIC view of this place that the camera",
        "described below sees — the staging text is the camera ground truth",
        "(position, height, angle, framing). Real-world scale, natural",
        "weathered materials. This is an EMPTY plate: no people, no figures,",
        "no text, letters, circles, arrows or drawing lines.",
        "",
        "WHAT THIS PLACE LOOKS LIKE (production data):",
        loc_lines(recon, s),
        "",
        "CAMERA (from production staging):",
        str((s.get("staging") or {}).get("camera_direction") or ""),
        C.staging_lines(s), "",
    ] + contracts)


def temp_shot_eye(tag, s, env_png, sketch_png, out_path, contracts):
    """임시샷 — CONTROL 을 눈높이 스케치로 교체(2라운드 규칙). 실인간, 정체성 무관."""
    prompt = "\n".join([
        "Create a photorealistic cinematic still that stages this shot inside",
        "the given environment. This is a COMPOSITION DRAFT: the people are",
        "ordinary real humans whose identity does not matter — focus on a",
        "strong, natural composition that matches the camera and staging",
        "exactly.",
        "",
        C.shot_content_lines(s),
        C.staging_lines(s),
        "",
        *(list(contracts) + [""] if contracts else []),
        "Keep the environment's architecture, materials and lighting from the",
        "environment reference. Match the camera, framing and perspective of",
        "the composition guide exactly; place each person exactly as the",
        "staging describes. Absolutely no text, letters, circles, arrows or",
        "sketch lines in the output.",
    ])
    return F.img_nb2(tag, prompt,
                     [(C.LBL_ENV, env_png), (LBL_CONTROL_EYE, sketch_png)],
                     out_path=out_path)


def run_no_person(recon, sk, root_png):
    s = recon["shots"][sk]
    d = OUTD / sk
    d.mkdir(parents=True, exist_ok=True)
    contracts = [C.people_contract(s), C.MARKER_NOT_PEOPLE]
    sketch = F.img_gpt(f"r6_{sk}_sketch", sketch_prompt(s), refs=[root_png],
                       out_path=d / "sketch.png")
    r6_page.build()
    F.img_gpt(f"r6_{sk}_plate_hop",
              plate_hop_prompt(recon, s, contracts + [C.EYE_LEVEL_CONTRACT]),
              refs=[sketch, root_png], out_path=d / "plate_hop.png")
    r6_page.build()
    # 직행 대조 — staging 텍스트가 카메라 SOT (눈높이 계약 없음: 부감 staging 검증)
    F.img_gpt(f"r6_{sk}_plate_direct",
              plate_direct_prompt(recon, s, contracts),
              refs=[root_png], out_path=d / "plate_direct.png")
    r6_page.build()


def run_person(recon, sk, root_png):
    s = recon["shots"][sk]
    d = OUTD / sk
    d.mkdir(parents=True, exist_ok=True)
    contracts = [C.people_contract(s), C.MARKER_NOT_PEOPLE, C.EYE_LEVEL_CONTRACT]
    plate_contracts = [C.people_contract(s), C.MARKER_NOT_PEOPLE,
                       C.EYE_LEVEL_CONTRACT]
    passports = C.passports_for_shot(recon, s)
    sketch = F.img_gpt(f"r6_{sk}_sketch", sketch_prompt(s), refs=[root_png],
                       out_path=d / "sketch.png")
    r6_page.build()
    plate = F.img_gpt(f"r6_{sk}_plate_hop",
                      plate_hop_prompt(recon, s, plate_contracts),
                      refs=[sketch, root_png], out_path=d / "plate_hop.png")
    r6_page.build()
    temp = temp_shot_eye(f"r6_{sk}_temp", s, plate, sketch, d / "temp.png",
                         contracts)
    r6_page.build()
    cont = C.contour(f"r6_{sk}_contour", temp, d / "contour.png")
    r6_page.build()
    bg = C.bg_regen(f"r6_{sk}_bgC", s, plate, d / "bg_C.png", contour_png=cont,
                    contracts=contracts)
    r6_page.build()
    C.final_shot(f"r6_{sk}_finalC", s, bg, passports, d / "final_C.png",
                 contracts=contracts)
    r6_page.build()
    # 직행 대조 (Codex 의견 수용): temp/윤곽/재생성 생략, plate+passport 만으로
    # 최종 — 새 뿌리에서 temp 경유가 여전히 필요한지 균일 검증 (control 미첨부)
    C.final_shot(f"r6_{sk}_final_direct", s, plate, passports,
                 d / "final_direct.png", contracts=contracts)
    r6_page.build()


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--root", default=None, choices=["a", "b", "c"],
                    help="미지정 시 plans/r6_root_choice.json 사용")
    ap.add_argument("--only", default="all",
                    choices=["all", "shots", "noperson", "person"])
    args = ap.parse_args()
    recon = F.load_recon()
    root_v = args.root or (F.load_plan("r6_root_choice").get("root"))
    root_png = F.OUT / "root_v3" / f"root_v3{root_v}.png"
    if not root_png.exists():
        raise SystemExit(f"root image missing: {root_png}")
    no_person, person = pick_shots(recon)
    F.save_plan("r6_shots", {"no_person": no_person, "person": person,
                             "root": root_v})
    print("no_person:", no_person, "| person:", person, "| root:", root_v)
    r6_page.build()
    if args.only == "shots":
        return
    if args.only in ("all", "noperson"):
        for sk in no_person:
            run_no_person(recon, sk, root_png)
    if args.only in ("all", "person"):
        for sk in person:
            run_person(recon, sk, root_png)
    F.runlog({"kind": "stage", "stage": "s11_outdoor_r6", "done": args.only,
              "root": root_v, "no_person": no_person, "person": person})


if __name__ == "__main__":
    main()
