"""shot_continuity_author — 자세 정본·중의어 교정·carried state 저작 (2026-07-13).

s40/s41 레시피의 텍스트 안전망 3종(스틸·콘티 프롬프트에 주입될 연속성 문장)을
LLM 으로 저작한다. 정본: scratchpad/forest_exp/s37_rooftop_rebuild.py
stage_pose_canon / stage_pose_fix / stage_int_cont.

  1) pose_canon: 움직일 수 없는 인물(사망·의식불명)의 자세 정본 —
     등장 전 샷에 글자 그대로 재사용, 중의어(위치 lie 등) 회피.
  2) carried: 샷별 '이어받는 상태' — ★양방향 전파·씬 경계 차단은
     프롬프트 계약이 담당(코드 재구현 금지). 실험의 실내 한정(int_cont)을
     전 샷으로 일반화(계약 첫 줄만 수정, 규칙 본문 verbatim).
  3) pose_fix: 정본 샷의 movement/figures/carried 문장 중의성 최소 교정.
     production v1 은 movement/figures 저작이 없으므로 빈 문자열로 전달
     (carried 교정이 실효) — 도입 시 그대로 확장.

코드 강제: pose_canon.shots 의 미지 태그 제거(+경고), carried 전 샷 누락
fail-fast, character_ko → EntityCanon short_id 정확 일치 매핑(불일치=None,
글자 부분매칭 금지).
"""
from __future__ import annotations

import logging
from typing import Any, Callable, Dict, List, Optional

from app.core.errors import AppError
from app.modules.prompt_loader import load_prompt, load_schema
from app.modules.pipeline.shot_ref_classify import tag_of

logger = logging.getLogger(__name__)

_MODULE = "shot_continuity"

PROMPT_VERSION_MAP = {
    "1": "1.202607132300",
    # v2 (E2E6 피드백 ④, 2026-07-16): pose_canon character_ko 를
    # EntityCanon 정본 명단(닫힌 집합)의 표기 그대로 저작하도록 계약 —
    # LLM 자유 표기("강민숙(민숙)")가 정확 일치 매핑을 깨서 short_id=None
    # 이 되던 identity 해소 공백 교정. 명단 밖 단역은 원문 표기 유지.
    "2": "2.202607161500",
}

# v2+ 팩만 '등장인물 정본 명단' 입력 블록을 받는다 (v1 조립 byte-identical)
_NAME_ROSTER_PACKS = {"2"}


def resolve_prompt_version(selector: str) -> str:
    try:
        return PROMPT_VERSION_MAP[selector]
    except KeyError:
        raise ValueError(
            f"unknown shot_continuity prompt version selector "
            f"{selector!r} (known: {sorted(PROMPT_VERSION_MAP)})"
        )


def run_shot_continuity(
    *,
    shots: List[Dict[str, Any]],
    scene_texts: Dict[int, str],
    scene_headings: Dict[int, str],
    char_name_to_short: Dict[str, str],
    prompt_version: str = "1",
    call_structured_fn: Optional[Callable[..., Dict[str, Any]]] = None,
    project_config: Optional[Dict[str, Any]] = None,
    opik_metadata: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
    """연속성 3종 저작 → 체크포인트 payload.

    Returns:
        {"pose_canon": [{character_ko, character_short_id, pose_en, shots,
                          basis_ko}],
         "carried": {tag: {carried_en, basis_ko}},
         "pose_fix": {tag: {movement_en, figures_en, carried_en}}}
    """
    if call_structured_fn is None:
        from app.modules.llm.llm_client import call_structured

        call_structured_fn = call_structured

    resolved = resolve_prompt_version(prompt_version)
    # 판정 모델 명시 — 실험 정본=gpt(분석 계열), unknown-step silent
    # fallback(gemini-pro) 차단 (shot_ref_classify 와 동일 E2E 실측 대응)
    project_config = {
        **(project_config or {}),
        f"{_MODULE}_pose_canon": {"model": "gpt"},
        f"{_MODULE}_carried": {"model": "gpt"},
        f"{_MODULE}_pose_fix": {"model": "gpt"},
    }
    ordered = sorted(
        shots, key=lambda s: (int(s["scene_index"]), int(s["shot_index"]))
    )
    tags = [tag_of(int(s["scene_index"]), int(s["shot_index"])) for s in ordered]
    if not tags:
        return {"pose_canon": [], "carried": {}, "pose_fix": {}}
    desc_by_tag = {
        t: (s.get("description") or "") for t, s in zip(tags, ordered)
    }
    scene_ids = sorted({int(s["scene_index"]) for s in ordered})
    # 씬 원문 전문 — 절대 자르지 않는다
    scenes_txt = "\n\n".join(
        f"[씬 {si}] {scene_headings.get(si, '')}\n{scene_texts.get(si, '')}"
        for si in scene_ids
    )
    shots_txt = "\n".join(f"{t}: {desc_by_tag[t]}" for t in tags)

    # ── 1) pose_canon ────────────────────────────────────────────────
    pc_sys = load_prompt(_MODULE, "pose_canon_system", version=resolved)
    pc_schema = load_schema(
        _MODULE, "pose_canon_schema", None, version=resolved
    )
    pc_user = f"씬 원문 전문:\n\n{scenes_txt}\n\n샷 목록(시간순):\n{shots_txt}"
    if prompt_version in _NAME_ROSTER_PACKS and char_name_to_short:
        roster_txt = "\n".join(
            f"- {name}" for name in sorted(char_name_to_short)
        )
        pc_user += f"\n\n등장인물 정본 명단:\n{roster_txt}"
    pc_res = call_structured_fn(
        f"{_MODULE}_pose_canon",
        pc_sys,
        pc_user,
        pc_schema,
        project_config=project_config,
        schema_name=f"{_MODULE}_pose_canon",
        opik_metadata=opik_metadata,
    )
    tag_set = set(tags)
    pose_canon: List[Dict[str, Any]] = []
    for it in pc_res.get("immobile") or []:
        known = [t for t in it.get("shots") or [] if t in tag_set]
        unknown = [t for t in it.get("shots") or [] if t not in tag_set]
        if unknown:
            logger.warning(
                "shot_continuity pose_canon: 미지 샷 제거 %s (character=%s)",
                unknown, it.get("character_ko"),
            )
        name = it.get("character_ko") or ""
        pose_canon.append(
            {
                "character_ko": name,
                # 정확 일치만 — 글자 부분매칭 금지 규칙
                "character_short_id": char_name_to_short.get(name),
                "pose_en": it.get("pose_en") or "",
                "shots": known,
                "basis_ko": it.get("basis_ko") or "",
            }
        )

    # ── 2) carried ───────────────────────────────────────────────────
    ca_sys = load_prompt(_MODULE, "carried_system", version=resolved)
    ca_schema = load_schema(_MODULE, "carried_schema", None, version=resolved)
    ca_res = call_structured_fn(
        f"{_MODULE}_carried",
        ca_sys,
        f"씬 원문 전문:\n\n{scenes_txt}"
        f"\n\n샷 목록(시간순 — 각각 carried state 판정):\n{shots_txt}",
        ca_schema,
        project_config=project_config,
        schema_name=f"{_MODULE}_carried",
        opik_metadata=opik_metadata,
    )
    ca_got = {it.get("shot"): it for it in ca_res.get("items") or []}
    missing = [t for t in tags if t not in ca_got]
    if missing:
        raise AppError(
            code=f"step.contract_violation.{_MODULE}",
            message=f"carried state 누락 샷: {missing}",
            status_code=422,
        )
    carried = {
        t: {
            "carried_en": ca_got[t].get("carried_en") or "",
            "basis_ko": ca_got[t].get("basis_ko") or "",
        }
        for t in tags
    }

    # ── 3) pose_fix (정본 샷만) ──────────────────────────────────────
    pf_sys = load_prompt(_MODULE, "pose_fix_system", version=resolved)
    pf_schema = load_schema(
        _MODULE, "pose_fix_schema", None, version=resolved
    )
    pose_fix: Dict[str, Dict[str, str]] = {}
    for it in pose_canon:
        for tag in it["shots"]:
            user = (
                f"자세 정본({it['character_ko']}): {it['pose_en']}\n\n"
                f"샷 텍스트: {desc_by_tag[tag]}\n\n"
                # production v1: movement/figures 저작 부재 — 빈 문자열 전달
                f"movement: \n\n"
                f"figures: \n\n"
                f"carried: {carried[tag]['carried_en']}"
            )
            res = call_structured_fn(
                f"{_MODULE}_pose_fix",  # 고정 tag — 모델 명시 매칭
                pf_sys,
                user,
                pf_schema,
                project_config=project_config,
                schema_name=f"{_MODULE}_pose_fix",
                opik_metadata=opik_metadata,
            )
            pose_fix[tag] = {
                "movement_en": res.get("movement_en") or "",
                "figures_en": res.get("figures_en") or "",
                "carried_en": res.get("carried_en") or "",
            }

    return {"pose_canon": pose_canon, "carried": carried, "pose_fix": pose_fix}
