"""인물 리스트 사전 추출 — beat/shot 전에 인물 목록을 확정."""
import json
import logging
from typing import Any, Dict, Optional
from pathlib import Path

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__)


class EntityCharacterListStep(StepRunner):
    """Step 6.5: 인물 리스트 사전 추출.

    씬 텍스트에서 인물 리스트를 1회 호출로 추출.
    beat_extract, shot_extract에서 이 리스트를 참조하여 인물명을 제한한다.
    """

    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]:
        # segments (text 포함)
        save_cp = self._load_prev_checkpoint("scene_save")
        if not save_cp or not save_cp.get("data", {}).get("segments"):
            from app.core.errors import AppError
            raise AppError(code="step.no_input", message="scene_save 결과 없음", status_code=400)
        segments = save_cp["data"]["segments"]

        # 씬 텍스트 조합 (text 필드 없는 구 체크포인트 fallback)
        has_empty = any(not seg.get("text") for seg in segments)
        fulltext = ""
        if has_empty:
            from app.models.project import Episode
            from sqlalchemy.orm import undefer
            ep = self.db.query(Episode).options(undefer(Episode.fulltext)).filter(Episode.id == self.episode_id).first()
            fulltext = ep.fulltext if ep else ""
            logger.warning("entity_character_list: %d segments missing text, using fulltext fallback",
                           sum(1 for s in segments if not s.get("text")))

        parts = []
        for seg in segments:
            text = seg.get("text") or fulltext[seg.get("start_char", 0):seg.get("end_char", 0)]
            parts.append(f"[씬 {seg['scene_index']}]\n{text}")
        scene_block = "\n\n".join(parts)

        # visual_world_rules (있으면 추가)
        rules_cp = self._load_prev_checkpoint("visual_world_rules")
        visual_rules = ""
        if rules_cp and rules_cp.get("data"):
            rules_data = rules_cp["data"]
            notes = rules_data.get("director_notes", [])
            if notes:
                visual_rules = "\n".join(f"- {n}" for n in notes)

        system = load_prompt("entity_character_list", "system", db=self.db)
        schema = load_schema("entity_character_list", "character_list_schema", db=self.db)

        user_prompt = scene_block
        if visual_rules:
            user_prompt = f"[시각적 규칙]\n{visual_rules}\n\n{user_prompt}"

        import time
        max_retry = 2
        last_err = None
        for attempt in range(max_retry + 1):
            try:
                result = call_structured(
                    step="entity_character_list",
                    system_prompt=system,
                    user_prompt=user_prompt,
                    response_schema=schema,
                    project_config=self.project_config,
                    schema_name="entity_character_list",
                    opik_metadata=self.build_opik_metadata(),
                )
                break
            except Exception as exc:
                last_err = exc
                if attempt < max_retry:
                    logger.warning("entity_character_list retry %d/%d: %s", attempt + 1, max_retry, exc)
                    time.sleep(2)
                else:
                    logger.error("entity_character_list FAILED after %d retries: %s", max_retry, exc)
                    raise

        characters = result.get("characters", [])
        characters.sort(key=lambda c: -c.get("appearance_count", 0))

        logger.info("entity_character_list: %d characters extracted", len(characters))

        return {
            "completed_count": len(characters),
            "applicable_count": len(characters),
            "failed_count": 0,
            "data": {"characters": characters},
        }
