"""실내/실외 공용 체인 단계 — 임시샷 → (VLM 분석|i2i 윤곽) → 배경 재생성 → 최종샷.

경로 정의 (fractional A/B — Codex 의견 수용):
  A: 직행 — 환경배경 + 배치도(control) + passport → 최종샷
  B: 임시샷 → VLM 구도 텍스트 분석 → 배경 재생성 → 최종샷
  C: 임시샷 → i2i 윤곽(contour) → 배경 재생성 → 최종샷 (사용자 지정 주경로)
ref 라벨 = 단계별 SOT 위계 명시(이미지 참조끼리 싸우지 않게).
시나리오 중립: 모든 구체 내용은 데이터 문자열 주입.
"""
from __future__ import annotations

import json
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple

import forest_lib as F

LBL_ENV = ("ENVIRONMENT BACKGROUND reference — the empty environment: its space, "
           "architecture, materials and lighting are the ground truth. It contains "
           "NO people.")
LBL_CONTROL = ("CAMERA AND PLACEMENT DIAGRAM (top-down control image) — use it ONLY "
               "to understand where the camera stands, what it looks at, and where "
               "each person is. NEVER copy its drawing style, lines, circles, "
               "letters or any marks into the output.")
LBL_CONTOUR = ("COMPOSITION GUIDE (line contour) — the framing, perspective and "
               "layout ground truth. Follow its composition exactly; do not copy "
               "its line style.")
LBL_IDENTITY = "IDENTITY REFERENCE photo of the person referred to as: "


def staging_lines(s: Dict[str, Any]) -> str:
    st = s.get("staging") or {}
    parts = []
    if st.get("camera_direction"):
        parts.append(f"CAMERA: {st['camera_direction']}")
    if st.get("framing_scale"):
        parts.append(f"FRAMING: {st['framing_scale']}")
    if st.get("lighting_mood"):
        parts.append(f"LIGHTING: {st['lighting_mood']}")
    for ca in (st.get("character_angles") or []):
        parts.append("PERSON: " + json.dumps(ca, ensure_ascii=False))
    fsc = st.get("frame_spatial_contract") or {}
    if fsc.get("constraints"):
        parts.append("SPATIAL CONSTRAINTS: "
                     + json.dumps(fsc["constraints"], ensure_ascii=False))
    return "\n".join(parts)


def shot_content_lines(s: Dict[str, Any]) -> str:
    lines = [f"SHOT CONTENT: {s.get('description')}"]
    if s.get("characters"):
        lines.append(f"PEOPLE IN SHOT: {', '.join(s['characters'])}")
    return "\n".join(lines)


def people_contract(s: Dict[str, Any]) -> str:
    """인원 계약 — 데이터(characters)로만 결정. 1.5라운드 수정(군중 발명 차단)."""
    names = [n for n in (s.get("characters") or []) if isinstance(n, str)]
    if names:
        return (f"PEOPLE COUNT CONTRACT: exactly {len(names)} person(s) in frame"
                f" — {', '.join(names)}. Absolutely no other people, bystanders"
                " or figures anywhere in the frame, foreground or background.")
    return ("PEOPLE COUNT CONTRACT: this shot contains NO people at all — the"
            " frame must be completely unpopulated, with no figures anywhere.")


MARKER_NOT_PEOPLE = ("Any circled letters, numbers or legend text on the control"
                     " diagram are SITE MARKERS only — they are never people and"
                     " must not become people or objects in the output.")

EYE_LEVEL_CONTRACT = ("CAMERA HEIGHT CONTRACT: the camera stands ON the ground"
                      " INSIDE the scene at human eye height (about 1.6 m)."
                      " Sky or horizon must be visible where the view allows it."
                      " Do NOT render an overhead, elevated, aerial or drone"
                      " view — this is a ground-level cinematic shot.")


def temp_shot(tag: str, s: Dict[str, Any], env_png: Path, control_png: Path,
              out_path: Path,
              contracts: Optional[List[str]] = None) -> Path:
    """1차 임시샷 — 구도/표현 발견 전용. passport 미첨부(정체성 무관), 실인간."""
    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 placement exactly.",
        "",
        shot_content_lines(s),
        staging_lines(s),
        "",
        *(list(contracts) + [""] if contracts else []),
        "Keep the environment's architecture, materials and lighting from the",
        "environment reference. Place the camera and every person exactly as the",
        "control diagram indicates. Absolutely no text, letters, circles, arrows",
        "or diagram marks in the output.",
    ])
    return F.img_nb2(tag, prompt,
                     [(LBL_ENV, env_png), (LBL_CONTROL, control_png)],
                     out_path=out_path)


VLM_COMP_SCHEMA = {
    "type": "object",
    "properties": {
        "camera": {"type": "object", "properties": {
            "height": {"type": "string"}, "angle": {"type": "string"},
            "lens_feel": {"type": "string"}},
            "required": ["height", "angle", "lens_feel"],
            "additionalProperties": False},
        "horizon_and_depth": {"type": "string"},
        "major_surfaces": {"type": "array", "items": {"type": "string"}},
        "subjects": {"type": "array", "items": {"type": "object", "properties": {
            "label": {"type": "string"},
            "screen_zone": {"type": "string"},
            "depth_plane": {"type": "string"},
            "pose": {"type": "string"},
            "support_contact": {"type": "string"}},
            "required": ["label", "screen_zone", "depth_plane", "pose",
                         "support_contact"],
            "additionalProperties": False}},
        "occlusion_order": {"type": "string"},
        "empty_background_requirements": {"type": "string",
            "description": "what the EMPTY background must contain/frame so the "
                           "subjects can be re-inserted later"},
    },
    "required": ["camera", "horizon_and_depth", "major_surfaces", "subjects",
                 "occlusion_order", "empty_background_requirements"],
    "additionalProperties": False,
}


def vlm_composition(tag: str, temp_png: Path) -> Dict[str, Any]:
    user = [
        {"type": "text", "text":
            "Analyse this film still's composition precisely. Describe the camera,"
            " horizon/depth, major visible surfaces, each subject's screen position /"
            " depth / pose / support contact, occlusion order, and what an EMPTY"
            " background plate (no people) must contain so the people could be"
            " re-inserted with the same composition."},
        F.png_data_url(temp_png),
    ]
    comp = F.llm(f"forest_vlm_comp", "You analyse cinematography precisely.",
                 user, VLM_COMP_SCHEMA, model="gpt")
    F.save_plan(f"comp_{tag}", comp)
    return comp


def contour(tag: str, temp_png: Path, out_path: Path) -> Path:
    prompt = (
        "Convert the attached photograph into a clean LINE-ART CONTOUR drawing: "
        "thin dark ink lines on a plain white background. Preserve the EXACT "
        "composition, framing, perspective and proportions — trace the outline of "
        "every structure, surface edge and human figure as it appears. No shading, "
        "no colour fills, no photo texture, no text, no added or removed objects."
    )
    return F.img_gpt(tag, prompt, refs=[temp_png], out_path=out_path)


def bg_regen(tag: str, s: Dict[str, Any], env_png: Path, out_path: Path,
             comp: Optional[Dict[str, Any]] = None,
             contour_png: Optional[Path] = None,
             mannequin: bool = False,
             contracts: Optional[List[str]] = None) -> Path:
    """최종 배경 재생성 — 빈 환경(±마네킹). comp(텍스트) 또는 contour(이미지) 기반."""
    base = [
        "Create the photorealistic EMPTY background plate for one film shot:",
        "the environment exactly as the environment reference shows it (same",
        "architecture, materials, lighting), seen from the shot's camera.",
        "", shot_content_lines(s), staging_lines(s), "",
    ]
    if comp is not None:
        base += ["The required composition, extracted from a draft of this shot:",
                 json.dumps(comp, ensure_ascii=False), ""]
    if contour_png is not None:
        base += ["Follow the attached line contour's composition EXACTLY —",
                 "same framing, perspective and structure placement.", ""]
    if mannequin:
        base += [
            "Where each person would stand or sit, place a featureless matte-grey",
            "artist mannequin in that exact pose and scale (no face, no clothing",
            "detail) — everything else stays an empty environment.",
        ]
    else:
        base += [
            "The plate must contain NO people, no figures, and no silhouettes —",
            "just the environment, composed so the described people could be",
            "inserted later.",
        ]
    if contracts:
        base += list(contracts)
    base += ["Absolutely no text, letters, circles, arrows or diagram marks."]
    refs: List[Path] = []
    if contour_png is not None:
        refs.append(contour_png)
    refs.append(env_png)
    return F.img_gpt(tag, "\n".join(base), refs=refs, out_path=out_path)


def final_shot(tag: str, s: Dict[str, Any], bg_png: Path, passports: Dict[str, str],
               out_path: Path, control_png: Optional[Path] = None,
               contracts: Optional[List[str]] = None) -> Path:
    """최종샷 — 배경 + passport(정체성 SOT) + (경로 A만 control 첨부)."""
    refs: List[Tuple[str, Path]] = [(LBL_ENV + " Use it as the shot's background,"
                                     " matching its space and lighting exactly.",
                                     bg_png)]
    if control_png is not None:
        refs.append((LBL_CONTROL, control_png))
    # passports: {display_name -> path} 매핑은 호출자가 구성
    for label, p in passports.items():
        refs.append((LBL_IDENTITY + label, Path(p)))
    prompt = "\n".join([
        "Create the FINAL photorealistic cinematic film still of this shot.",
        "", shot_content_lines(s), staging_lines(s), "",
        *(list(contracts) + [""] if contracts else []),
        "Stage the shot inside the attached environment background — keep its",
        "architecture, materials and lighting exactly. Every person is a REAL",
        "human being, photorealistic, matching their identity reference photo",
        "(face, hair, build). Natural film lighting and grain consistent with",
        "the environment.",
        "Absolutely no text, letters, circles, arrows, diagram marks, and no",
        "mannequin-like or statue-like appearance on any person.",
    ])
    return F.img_nb2(tag, prompt, refs, out_path=out_path)


def passports_for_shot(recon: Dict[str, Any], s: Dict[str, Any]) -> Dict[str, str]:
    """샷 등장 인물명 → passport 경로. 이름↔short_id 매핑은 DB 이름으로 조인."""
    # entity_canon 이름 목록을 다시 질의하지 않도록 recon 에 저장된 short_id→path 와
    # 샷의 characters(이름)를 이름→short_id 매핑으로 연결한다.
    name_to_sid = recon.get("character_name_to_sid") or {}
    out = {}
    for name in (s.get("characters") or []):
        sid = name_to_sid.get(name)
        if sid and sid in recon["passports"]:
            out[name] = recon["passports"][sid]
        else:
            F.runlog({"kind": "warn", "msg": f"no passport for character {name!r}"})
    return out
