"""씬 감독 — 물리적 존재 판별 (v2 방식)."""

import copy
import json
import logging
from typing import Dict, List, Optional, Sequence, Tuple

from app.modules.llm.llm_client import call_structured
from app.modules.pipeline import segment_key as segkey
from app.modules.prompt_loader import load_prompt, load_schema

logger = logging.getLogger(__name__)
_MODULE = "scene_director"

# 씬 키 잠금·파리티는 `segment_key` 로 옮겼다 — 같은 결함이 scene_director /
# outlook_phase2 두 곳에서 났고, 앞으로 씬 전체를 한 콜에 넣는 스텝이 늘어날수록
# 각자 구현하면 또 갈린다. 자세한 근거는 그 모듈의 docstring.


def direct_scenes(
    segments: List[Dict],
    fulltext: str = "",
    entities: Dict[str, List[Dict]] = None,
    visual_rules: str = "",
    short_id_map: Optional[Dict[str, str]] = None,
    project_config: Optional[Dict] = None,
    opik_metadata: Optional[Dict] = None,
) -> Dict:
    """전체 씬 — 물리적 존재 여부 판별. short_id enum 사용.

    반환은 하류 계약대로 `scene_index` 를 단다 — 입력 세그먼트의 인덱스를
    코드가 되돌려 붙이므로 LLM 이 인덱스를 지어낼 여지가 없다.
    """
    if entities is None:
        entities = {}
    if not segments:
        return {"scenes": []}

    system = load_prompt(_MODULE, "system")
    schema = copy.deepcopy(load_schema(_MODULE, "analyze_schema"))

    keys = segkey.segment_keys(len(segments))
    key_to_index = segkey.key_index_map(keys, segments)

    # Build entity block with short_ids
    entity_lines: List[str] = []
    for etype in ["characters", "locations", "props"]:
        for e in entities.get(etype, []):
            sid = e.get("short_id", e.get("name", "?"))
            desc = e.get("description", "")
            entity_lines.append(f"- {sid}: {e.get('name', '')} ({etype[:-1]}) — {desc}")

    # Build scene block — 블록 머리는 불투명 키. 본문은 자르지 않고 통째로.
    scene_lines: List[str] = []
    for key, seg in zip(keys, segments):
        heading = seg.get("heading", "")
        text_preview = seg.get("text", "")
        scene_lines.append(f"## {key}: {heading}\n{text_preview}")

    # Inject enum constraint into schema — segment_key + present_entity_ids + primary_location
    segkey.pin_key_field(schema["properties"]["scenes"]["items"], keys)

    all_ids = [
        e.get("short_id", e.get("name"))
        for etype in ["characters", "locations", "props"]
        for e in entities.get(etype, [])
    ]
    location_ids = [
        e.get("short_id")
        for e in entities.get("locations", [])
        if e.get("short_id")
    ]
    if all_ids:
        items_def = schema["properties"]["scenes"]["items"]["properties"]
        items_def["present_entity_ids"]["items"] = {"type": "string", "enum": all_ids}
        # primary_location도 location short_id enum으로 제약
        if location_ids:
            items_def["primary_location"] = {
                "type": "string",
                "enum": location_ids,
                "description": "이 씬의 주 촬영 위치 — 배경 엔티티의 short_id (예: L05)",
            }

    rules_block = f"시각적 세계관 규칙:\n{visual_rules}\n\n" if visual_rules else ""

    user_prompt = (
        f"{rules_block}"
        f"엔티티 목록:\n{chr(10).join(entity_lines)}\n\n"
        f"{chr(10).join(scene_lines)}"
    )

    def _parity_ok(payload: Dict) -> bool:
        rows = (payload or {}).get("scenes") or []
        return segkey.parity_ok(rows, keys, step="scene_director")

    result = call_structured(
        step="scene_director",
        system_prompt=system,
        user_prompt=user_prompt,
        response_schema=schema,
        project_config=project_config,
        schema_name="scene_director",
        opik_metadata=opik_metadata,
        validate_response=_parity_ok,
    )

    # 파리티 게이트 — validate_response 는 Tier 3 에 적용되지 않으므로
    # (마지막 시도 보호) 최종 판정은 반드시 여기서 한 번 더 한다.
    rows_in = (result or {}).get("scenes") or []
    segkey.assert_parity(rows_in, keys, step="scene_director")

    # 반환 dict 는 제자리 변형하지 않는다 — 호출자가 payload 를 재사용할 수 있다.
    return {**(result or {}), "scenes": segkey.map_back(rows_in, key_to_index)}
