"""Shot별 VE 판정 StepRunner — scene_director 이후 shot 단위 VE + 변형 확정."""
import hashlib
import json as _json
import logging
from typing import Any, Dict

from app.core.step_runner import StepRunner
from app.core.steps.entity_steps import _EntityStepMixin

logger = logging.getLogger(__name__)


# 2026-05-10 SOT-drift fix:
#   v1 cp 는 visible_entity_ids 가 broad list (description name match) 만 →
#   off-camera/gaze-target 인물 포함 → scene_detail Rule X-2 hard fail.
#   v2 는 deterministic gaze 제외 + variant LLM post-process + audit field.
# 2026-05-13 frame-visible SOT unification:
#   v3 는 deterministic ``_resolve_scene_no_variant`` (L/P always-keep) 영구
#   폐기 + 항상 LLM path. visible_entity_ids 의미를 "scene-present" 가 아닌
#   "camera-frame-visible" 로 통일 — character / location / prop 동일 기준.
#   v2 cp 는 L/P over-inclusion 상태 → schema bump 로 stale invalidation.
#   step_runner P0-3 mismatch 는 allowlist 외 → 운영자 명시 force 의무.
# ★2026-09-02: `location_parts` 가 정식 갈래로 들어왔다 — 옛 CP 는 LP 를
#  후보로 **받지도 못한** 판이라 재사용하면 안 된다 (Codex 조건 3).
SHOT_DIRECTOR_SCHEMA_VERSION = 4

# Prompt pack version — v6 는 Korean grammar example 4 pattern bundle 제거.
# Area #3 v1: visibility/physical presence SOT — 추상 원칙 + synthetic
# 캐릭터 (캐릭터A/B/C/D) 예시 + Korean grammar example 0.
# bump 시 _config_hash 변동 → step_runner P0-3 stale cp 자동 감지.
SHOT_DIRECTOR_PROMPT_VERSION = "6.202605172247"


class ShotDirectorStep(_EntityStepMixin, StepRunner):
    """Step 16.5: Shot별 visible entities 판정.

    scene_director의 씬 레벨 VE를 shot 단위로 세분화하고,
    변형 캐릭터의 전환 시점을 확정한다.
    """

    def _config_hash(self) -> str:
        """schema_version + prompt_version 조합 hash. step_runner._check_cp_mismatch
        가 호출 — bump 시 stale cp BLOCK (allowlist 외).
        """
        payload = {
            "schema_version": SHOT_DIRECTOR_SCHEMA_VERSION,
            "prompt_version": SHOT_DIRECTOR_PROMPT_VERSION,
        }
        return hashlib.sha256(
            _json.dumps(payload, sort_keys=True).encode("utf-8")
        ).hexdigest()[:16]

    def _execute(self, mode="resume") -> Dict[str, Any]:
        # scene_save segments (text 필드 포함)
        save_cp = self._load_prev_checkpoint("scene_save")
        segments = (save_cp or {}).get("data", {}).get("segments", [])

        # scene_director
        director_cp = self._load_prev_checkpoint("scene_director")
        if not director_cp or not director_cp.get("data", {}).get("scenes"):
            from app.core.errors import AppError
            raise AppError(
                code="step.no_input",
                message="scene_director 결과 없음",
                status_code=400,
            )

        # shot_extract
        shot_cp = self._load_prev_checkpoint("shot_validator")
        if not shot_cp or not shot_cp.get("data", {}).get("scenes"):
            from app.core.errors import AppError
            raise AppError(
                code="step.no_input",
                message="shot_extract 결과 없음",
                status_code=400,
            )

        from app.core.steps.shot_validator_step import assert_no_failed_scenes
        assert_no_failed_scenes(shot_cp, self.project_config, consumer_step="shot_director")

        # shot_selection (optional)
        sel_cp = self._load_prev_checkpoint("shot_selection")

        # entity_relation (optional)
        rel_cp = self._load_prev_checkpoint("entity_relation")

        # entity list (from filter > merge > extract)
        filter_cp = self._load_prev_checkpoint("entity_filter")
        merge_cp = self._load_prev_checkpoint("entity_merge")
        if filter_cp and filter_cp.get("data", {}).get("filtered_entities"):
            entities = filter_cp["data"]["filtered_entities"]
        elif merge_cp and merge_cp.get("data"):
            entities = {
                "characters": merge_cp["data"].get("characters", []),
                "locations": merge_cp["data"].get("locations", []),
                "props": merge_cp["data"].get("props", []),
            }
        else:
            entities = {"characters": [], "locations": [], "props": []}

        from app.modules.pipeline.shot_director import direct_shots

        result = direct_shots(
            segments=segments,
            scene_director_data=director_cp["data"],
            shot_extract_data=shot_cp["data"],
            shot_selection_data=sel_cp.get("data") if sel_cp else None,
            entity_relation_data=rel_cp.get("data") if rel_cp else None,
            entities=entities,
            project_config=self.project_config,
            opik_metadata=self.build_opik_metadata(),
        )

        # cp 에 SCHEMA/PROMPT version + config_hash stamp.
        # step_runner P0-3 가 bump 후 stale cp 차단. shot_director 는
        # _LEGACY_SCHEMA_BUMP_ALLOWLIST 외 → BLOCK 으로 운영자 명시 force.
        result["schema_version"] = SHOT_DIRECTOR_SCHEMA_VERSION
        result["prompt_version"] = SHOT_DIRECTOR_PROMPT_VERSION

        return {
            "completed_count": len(result.get("scenes", [])),
            "applicable_count": len(segments),
            "failed_count": 0,
            "data": result,
            "schema_version": SHOT_DIRECTOR_SCHEMA_VERSION,
            "config_hash": self._config_hash(),
        }
