"""background_share_plan 스텝 — 에피소드 전체 배경 공유·참조 계획 (B-2).

2026-07-19 사용자 확정: 샷별 개별 판정 대신 에피소드 전체를 조망하는 LLM
계획으로 '배경 참조 vs 앞쪽 샷(prev) 참조'를 구분 지휘. 스틸 소비 시 이
계획이 shot_ref_classify prev 판정의 상위 권위(부재=기존 판정 fail-safe).
flag ``background_share_plan_enabled`` default OFF — OFF=no-op(불변).
"""
from __future__ import annotations

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

from app.core.step_runner import StepRunner

logger = logging.getLogger(__name__)

SCHEMA_VERSION = 1
# ★팩은 v1 을 그대로 쓴다 (2026-09-05). v2 (`2.202609051240`) 는 「group_key 는
# share_groups 칸이다」를 적어 발생 빈도를 줄이지만, 팩 버전이 config_hash 에
# 실려 **이미 완주한 화의 CP 가 drift** 로 걸린다. 이 결함의 실제 방어는
# 코드(재시도 루프 안으로 들인 스키마 위반 + `normalize_shot_plans`)라 팩이
# 없어도 막힌다. v2 는 새로 시작하는 프로젝트에서 올린다.
PROMPT_VERSION = "1"


class BackgroundSharePlanStep(StepRunner):
    step_id = "background_share_plan"

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

        from app.modules.pipeline.background_share_plan import (
            VALIDATION_VERSION,
            resolve_prompt_version,
        )

        payload = {
            "schema_version": SCHEMA_VERSION,
            "pack": resolve_prompt_version(PROMPT_VERSION),
            # E2E10 Codex 재리뷰: 검증 계약(v2=group_key 유일성) 전환도
            # 기존 완료 CP 를 stale 로 감지해야 한다
            "validation_version": VALIDATION_VERSION,
        }
        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(
                    "background_share_plan: %s 로드 실패: %s", step_id, exc
                )
        return None

    def _execute(self, mode: str = "resume") -> Dict[str, Any]:
        from app.core.config import settings
        from app.modules.pipeline.background_share_plan import (
            run_background_share_plan,
        )

        def _result(data: Dict[str, Any], *, applicable: int,
                    completed: int, failed: int) -> Dict[str, Any]:
            return {
                "status": (
                    "completed" if failed == 0 else
                    ("partial" if completed else "failed")),
                "applicable_count": applicable,
                "completed_count": completed,
                "failed_count": failed,
                "schema_version": SCHEMA_VERSION,
                "config_hash": self._config_hash(),
                "data": data,
            }

        if not bool(getattr(
                settings, "background_share_plan_enabled", False)):
            return _result({}, applicable=0, completed=0, failed=0)

        # ── 입력 조립: 씬 원문 전문 + 선택 샷 목록(스토리 순) ────────
        scene_cp = self._load_prev_checkpoint("scene_save") or {}
        scene_texts: Dict[int, str] = {}
        for seg in (scene_cp.get("data", {}) or {}).get("segments", []) or []:
            si = seg.get("scene_index")
            if isinstance(si, int):
                scene_texts[si] = str(seg.get("text") or "")

        sel_cp = self._load_prev_checkpoint("shot_selection") or {}
        selected: Dict[int, List[int]] = {}
        for sc in (sel_cp.get("data", {}) or {}).get("scenes", []) or []:
            si = sc.get("scene_index")
            if si is not None:
                selected[int(si)] = [
                    int(x) for x in sc.get("selected_shot_indices") or []]

        val_cp = self._load_prev_checkpoint("shot_validator") or {}
        director_cp = self._load_prev_checkpoint("scene_director") or {}
        loc_by_scene: Dict[int, str] = {}
        for sc in (director_cp.get("data", {}) or {}).get("scenes", []) or []:
            si = sc.get("scene_index")
            if si is not None and sc.get("primary_location"):
                loc_by_scene[int(si)] = str(sc["primary_location"])

        shot_tags: List[str] = []
        shot_lines: List[str] = []
        for sc in (val_cp.get("data", {}) or {}).get("scenes", []) or []:
            si = sc.get("scene_index")
            if si is None:
                continue
            si = int(si)
            for sh in sc.get("shots", []) or []:
                shi = sh.get("shot_index")
                if shi is None or int(shi) not in (selected.get(si) or []):
                    continue
                tag = f"S{si}sh{int(shi)}"
                shot_tags.append(tag)
                shot_lines.append(
                    f"- {tag} | scene {si}"
                    f" | location: {loc_by_scene.get(si, '')}"
                    f" | {str(sh.get('description') or '')}"
                )
        if not shot_tags:
            return _result({}, applicable=0, completed=0, failed=0)

        scenes_block = "\n\n".join(
            f"[scene {si}]\n{scene_texts[si]}"
            for si in sorted(scene_texts)
        )
        shots_block = "\n".join(shot_lines)

        try:
            out = run_background_share_plan(
                shots_block=shots_block,
                scenes_block=scenes_block,
                shot_tags=shot_tags,
                scene_texts=scene_texts,
                project_config=self.project_config,
                prompt_version=PROMPT_VERSION,
                opik_metadata=self.build_opik_metadata(
                    "background_share_plan"),
            )
        except Exception as exc:  # noqa: BLE001 — fail-closed 격리
            logger.exception("background_share_plan: 계획 저작 실패")
            return _result(
                {"error": str(exc)}, applicable=1, completed=0, failed=1)

        data = {
            "plan": out["plan"],
            "attempts": out["attempts"],
            "shot_tags": shot_tags,
        }
        logger.info(
            "background_share_plan: 그룹 %d · 샷 %d (attempts=%d)",
            len(out["plan"].get("share_groups") or []),
            len(shot_tags), out["attempts"],
        )
        return _result(data, applicable=1, completed=1, failed=0)
