"""Shot별 촬영 기법 선정 — selected shot에 대해 technique 2개씩 배정.

DB shot_type 테이블에서 30개 촬영 기법을 로드하고,
15 shots씩 번들로 묶어 병렬 처리.
"""
import json
import logging
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path
from typing import Any, Dict, List, Optional

from app.core.step_runner import StepRunner
from app.modules.llm.llm_client import call_structured
from app.modules.prompt_loader import load_prompt, load_schema

logger = logging.getLogger(__name__)

BUNDLE_MAX_SHOTS = 15
MAX_WORKERS = 4


class ShotCinematographyStep(StepRunner):
    """Shot별 촬영 기법 2개 선정."""

    def _load_prev_checkpoint(self, step_id: str) -> Optional[Dict]:
        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():
            return json.loads(cp.read_text(encoding="utf-8"))
        return None

    def _execute(self, mode="resume") -> Dict[str, Any]:
        # shot_extract + shot_selection 로드
        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_shots", message="shot_extract 결과 없음", status_code=400)

        sel_cp = self._load_prev_checkpoint("shot_selection")
        selected_map = {}
        if sel_cp and sel_cp.get("data", {}).get("scenes"):
            for s in sel_cp["data"]["scenes"]:
                selected_map[s["scene_index"]] = set(s.get("selected_shot_indices", []))

        # selected shots만 추출
        all_shots = []
        for s in shot_cp["data"]["scenes"]:
            si = s["scene_index"]
            selected_indices = selected_map.get(si)
            for sh in s.get("shots", []):
                if selected_indices is None or sh.get("shot_index") in selected_indices:
                    all_shots.append({
                        "scene_index": si,
                        "scene_heading": s.get("scene_heading", ""),
                        "shot_index": sh.get("shot_index", 0),
                        "description": sh["description"],
                        "characters": sh.get("characters", []),
                        "based_on_beat": sh.get("based_on_beat", 0),
                    })

        if not all_shots:
            return {"completed_count": 0, "applicable_count": 0, "failed_count": 0,
                    "data": {"shots": [], "total_shots": 0}}

        # DB에서 촬영 기법 목록 로드
        from sqlalchemy import text as sql_text
        shot_rows = self.db.execute(sql_text(
            "SELECT name, category, description, llm_description "
            "FROM shot_type WHERE is_active = true ORDER BY sort_order"
        )).fetchall()
        shot_types_block = "\n".join(
            f"- {r[0]} [{r[1]}]: {r[2]} | Camera: {r[3]}"
            for r in shot_rows
        )

        system = load_prompt("shot_cinematography", "system", db=self.db)
        user_template = load_prompt("shot_cinematography", "user", db=self.db)
        schema = load_schema("shot_cinematography", "cine_schema", db=self.db)

        # 번들 구성
        bundles = []
        for i in range(0, len(all_shots), BUNDLE_MAX_SHOTS):
            bundles.append(all_shots[i:i + BUNDLE_MAX_SHOTS])

        logger.info("shot_cinematography: %d selected shots → %d bundles", len(all_shots), len(bundles))

        results_by_idx = {}
        with ThreadPoolExecutor(max_workers=MAX_WORKERS) as pool:
            futures = {
                pool.submit(
                    self._call_bundle, b, i + 1, system, user_template, schema, shot_types_block
                ): i + 1
                for i, b in enumerate(bundles)
            }
            for fut in as_completed(futures):
                idx, shots, err = fut.result()
                results_by_idx[idx] = shots

        all_results = []
        for i in sorted(results_by_idx):
            all_results.extend(results_by_idx[i])

        logger.info("shot_cinematography: %d shots processed", len(all_results))

        return {
            "completed_count": len(all_results),
            "applicable_count": len(all_shots),
            "failed_count": max(0, len(all_shots) - len(all_results)),
            "data": {"shots": all_results, "total_shots": len(all_results)},
        }

    def _call_bundle(self, bundle, call_idx, system, user_template, schema, shot_types_block):
        shots_lines = []
        for sh in bundle:
            chars = ", ".join(sh["characters"])
            beat = f"beat:{sh['based_on_beat']}" if sh["based_on_beat"] else "원문"
            shots_lines.append(
                f"Scene {sh['scene_index']} Shot {sh['shot_index']} ({beat}): "
                f"{sh['description']} [{chars}]"
            )

        user_prompt = user_template.format(
            shot_types_block=shot_types_block,
            shots_block="\n".join(shots_lines),
        )
        # 같은 씬의 연속 shot에 기법 다양성 확보
        user_prompt += (
            "\n[다양성 규칙] 같은 씬의 연속 shot에 동일한 촬영 기법을 배정하지 마세요. "
            "시각적 다양성을 위해 서로 다른 기법(category 포함)을 선택하세요.\n"
        )

        scene_range = f"S{bundle[0]['scene_index']}-S{bundle[-1]['scene_index']}"
        max_retry = 5
        for attempt in range(max_retry + 1):
            try:
                result = call_structured(
                    step="shot_cinematography",
                    system_prompt=system,
                    user_prompt=user_prompt,
                    response_schema=schema,
                    project_config=self.project_config,
                    schema_name=f"shot_cine_{call_idx}",
                    opik_metadata=self.build_opik_metadata(),
                )
                shots = result.get("shots", [])
                logger.info("shot_cinematography call %d (%s, %d shots): %d results",
                            call_idx, scene_range, len(bundle), len(shots))
                return call_idx, shots, None
            except Exception as exc:
                if attempt < max_retry:
                    logger.warning("shot_cinematography call %d (%s) retry %d/%d: %s",
                                   call_idx, scene_range, attempt + 1, max_retry, exc)
                    import time
                    time.sleep(2)
                    continue
                logger.warning("shot_cinematography call %d (%s) FAILED after %d retries: %s",
                               call_idx, scene_range, max_retry, exc)
                return call_idx, [], str(exc)
