"""ShotContinuityStep — 자세 정본·중의어 교정·carried state 저작 (order 21.91).

s40/s41 레시피의 텍스트 안전망 데이터 스텝:
  data.pose_canon[] = {character_ko, character_short_id, pose_en, shots, basis_ko}
  data.carried[tag] = {carried_en, basis_ko}
  data.pose_fix[tag] = {movement_en, figures_en, carried_en}

still_recipe_mode="v1" 일 때만 applicable(if_still_recipe).
계약 정본: backend/app/modules/pipeline/shot_continuity_author.py docstring.
"""

import json
import logging
from pathlib import Path
from typing import Any, Dict, Optional

from app.core.step_runner import StepRunner

logger = logging.getLogger(__name__)

SCHEMA_VERSION = 2
# v2: pose_canon character_ko=EntityCanon 정본 명단 표기 강제 (E2E6 ④ —
# short_id 정확 일치 매핑 복원). config_hash 는 resolve 값으로 자동 드리프트.
PROMPT_VERSION = "3"


def _pack_content_hash(resolved: str) -> str:
    """그 팩 디렉토리의 **스템 바이트**를 접는다.

    `shot_continuity` 는 스템이 여섯뿐이고 셋이 한 스텝에서 함께 쓰이므로
    디렉토리 통째로 접어도 과하지 않다(스틸 쪽은 스템이 수십 개라 한 장씩
    접는다).
    """
    import hashlib
    from pathlib import Path

    from app.modules.prompt_loader import PROMPTS_BASE

    d = Path(PROMPTS_BASE) / "shot_continuity" / resolved
    h = hashlib.sha256()
    for f in sorted(d.glob("*")):
        if f.is_file():
            h.update(f.name.encode("utf-8"))
            h.update(f.read_bytes())
    return h.hexdigest()[:16]


class ShotContinuityStep(StepRunner):
    """샷 연속성 저작 — pose_canon·pose_fix·carried (레시피 이식)."""

    def _config_hash(self) -> str:
        import hashlib
        import json as _json

        from app.core.config import settings
        from app.modules.pipeline.shot_continuity_author import (
            resolve_prompt_version,
        )

        from app.core.step_runner import compute_config_hash

        payload = {
            "still_recipe_mode": settings.still_recipe_mode,
            "schema_version": SCHEMA_VERSION,
            "prompt_version": resolve_prompt_version(PROMPT_VERSION),
            # ★**버전 문자열만 접으면 팩 내용을 고쳐도 안 돈다**
            #  (2026-08-27 자체 리뷰). 이 판에서 실제로 그랬다 — 팩을
            #  낸 뒤 `carried_system.md` 에 규칙 한 줄을 더했는데
            #  config_hash 가 그대로라, 그 팩으로 이미 저작한 판은
            #  재개해도 새 규칙을 **한 번도 못 본다.** 사람은 반영됐다고
            #  믿는다.
            "prompt_pack_content": _pack_content_hash(
                resolve_prompt_version(PROMPT_VERSION)),
            "model": settings.openai_model,
            # HIGH-5: project_config step override(모델 라우팅) 변경 감지
            "project_config_hash": compute_config_hash(self.project_config),
        }
        return hashlib.sha256(
            _json.dumps(payload, sort_keys=True).encode("utf-8")
        ).hexdigest()[:16]

    def _load_prev_checkpoint(self, step_id: str) -> Optional[Dict[str, Any]]:
        from app.core.config import settings

        cp = (
            Path(settings.projects_dir)
            / self.project_id / "checkpoints" / "episodes"
            / self.episode_id / step_id / "manifest.json"
        )
        if cp.exists():
            try:
                return json.loads(cp.read_text(encoding="utf-8"))
            except Exception as exc:  # noqa: BLE001
                logger.warning(
                    "shot_continuity: %s 로드 실패: %s", step_id, exc
                )
        return None

    def _execute(self, mode: str = "resume") -> Dict[str, Any]:
        from app.modules.pipeline.outdoor_direct_common import (
            build_selected_keys,
            is_selected,
        )
        from app.modules.pipeline.shot_continuity_author import (
            run_shot_continuity,
        )

        validator_cp = self._load_prev_checkpoint("shot_validator")
        scenes = (validator_cp or {}).get("data", {}).get("scenes", []) or []
        selected_keys = build_selected_keys(
            self._load_prev_checkpoint("shot_selection")
        )

        shots: list = []
        scene_headings: Dict[int, str] = {}
        for sc in scenes:
            si = sc.get("scene_index")
            if si is None:
                continue
            si = int(si)
            if sc.get("scene_heading"):
                scene_headings[si] = sc["scene_heading"]
            for sh in sc.get("shots", []) or []:
                shi = sh.get("shot_index")
                if shi is None:
                    continue
                if not is_selected((si, int(shi)), selected_keys):
                    continue
                shots.append(
                    {
                        "scene_index": si,
                        "shot_index": int(shi),
                        "description": sh.get("description") or "",
                    }
                )

        scene_save_cp = self._load_prev_checkpoint("scene_save")
        scene_texts: Dict[int, str] = {}
        for seg in (scene_save_cp or {}).get("data", {}).get("segments", []) or []:
            si = seg.get("scene_index")
            if isinstance(si, int):
                # 씬 원문 전문 — 절대 자르지 않는다
                scene_texts[si] = seg.get("text") or ""
                if seg.get("heading") and si not in scene_headings:
                    scene_headings[si] = seg["heading"]

        if not shots:
            logger.info("shot_continuity: 선택 샷 없음 — no-op")
            return {
                "applicable_count": 0,
                "completed_count": 0,
                "failed_count": 0,
                "schema_version": SCHEMA_VERSION,
                "config_hash": self._config_hash(),
                "data": {"pose_canon": [], "carried": {}, "pose_fix": {}},
            }

        # character 이름 → short_id (정확 일치 매핑용)
        from app.models.project import EntityCanon

        rows = (
            self.db.query(EntityCanon)
            .filter(
                EntityCanon.project_id == self.project_id,
                EntityCanon.entity_type == "character",
            )
            .all()
        )
        char_name_to_short = {
            e.name: e.short_id for e in rows if e.short_id
        }

        data = run_shot_continuity(
            shots=shots,
            scene_texts=scene_texts,
            scene_headings=scene_headings,
            char_name_to_short=char_name_to_short,
            prompt_version=PROMPT_VERSION,
            project_config=self.project_config,
            opik_metadata=self.build_opik_metadata(),
        )
        return {
            "applicable_count": len(shots),
            "completed_count": len(shots),
            "failed_count": 0,
            "schema_version": SCHEMA_VERSION,
            "config_hash": self._config_hash(),
            "data": data,
        }
