#!/usr/bin/env python3
# File: src/noveler/infrastructure/persistence/plot_viewpoint_repository.py
# Purpose: Provide a resilient repository for reading viewpoint plots from YAML sources.
# Context: Infrastructure layer component consumed by quality evaluators and CLI tooling.

"""Plot viewpoint repository with comprehensive error handling."""

from __future__ import annotations

import shutil
from pathlib import Path
from typing import Any

import yaml

from noveler.domain.exceptions.viewpoint_exceptions import (
    ViewpointDataInvalidError,
    ViewpointFileNotFoundError,
    ViewpointRepositoryError,
    ViewpointYAMLParseError,
)
from noveler.domain.quality.viewpoint_entities import ComplexityLevel, ViewpointInfo, ViewpointType
from noveler.infrastructure.logging.unified_logger import get_logger

VIEWPOINT_TYPE_ALIASES: dict[str, ViewpointType] = {
    "single_introspective": ViewpointType.SINGLE_INTROSPECTIVE,
    "introspective": ViewpointType.SINGLE_INTROSPECTIVE,
    "内省": ViewpointType.SINGLE_INTROSPECTIVE,
    "single_interactive": ViewpointType.SINGLE_INTERACTIVE,
    "interactive": ViewpointType.SINGLE_INTERACTIVE,
    "対話": ViewpointType.SINGLE_INTERACTIVE,
    "multiple": ViewpointType.MULTIPLE_PERSPECTIVE,
    "multiple_perspective": ViewpointType.MULTIPLE_PERSPECTIVE,
    "multi": ViewpointType.MULTIPLE_PERSPECTIVE,
    "複数視点": ViewpointType.MULTIPLE_PERSPECTIVE,
    "body_swap": ViewpointType.BODY_SWAP,
    "switch": ViewpointType.BODY_SWAP,
    "入れ替わり": ViewpointType.BODY_SWAP,
}

COMPLEXITY_ALIASES: dict[str, ComplexityLevel] = {
    "low": ComplexityLevel.LOW,
    "medium": ComplexityLevel.MEDIUM,
    "high": ComplexityLevel.HIGH,
    "低": ComplexityLevel.LOW,
    "中": ComplexityLevel.MEDIUM,
    "高": ComplexityLevel.HIGH,
}

EPISODE_EXPECTED_TYPE = "正の整数または数値文字列であること"


class PlotViewpointRepository:
    """Persist and retrieve viewpoint plot information from YAML files."""

    def __init__(self, project_path: Path, enable_backup: bool) -> None:
        """Initialise the repository with a project root and backup preference."""
        self.project_path = Path(project_path)
        if not self.project_path.exists():
            raise ViewpointFileNotFoundError(str(self.project_path), project_name=self.project_path.name)

        self.plot_dir = self.project_path / "20_プロット" / "視点プロット"
        self.enable_backup = enable_backup
        self.logger = get_logger(__name__)

    def get_episode_viewpoint_info(self, episode_number: str) -> ViewpointInfo | None:
        """Return viewpoint information for the specified episode or None when missing."""
        episode_index = self._parse_episode_number(episode_number)
        candidates = self._candidate_episode_keys(episode_number, episode_index)

        match = self._find_episode_in_plot_files(candidates)
        if match is None:
            return None

        episode_data, chapter_data = match
        return self._convert_to_viewpoint_info(episode_data, chapter_data)

    # ------------------------------------------------------------------ helpers
    def _parse_episode_number(self, value: str) -> int:
        suffix = value
        if value.startswith("episode"):
            suffix = value[len("episode") :]
        try:
            episode_num = int(suffix)
        except (TypeError, ValueError) as exc:
            raise ViewpointDataInvalidError(
                field_name="episode_number",
                expected_type=EPISODE_EXPECTED_TYPE,
                actual_value=value,
            ) from exc

        if episode_num <= 0:
            raise ViewpointDataInvalidError(
                field_name="episode_number",
                expected_type=EPISODE_EXPECTED_TYPE,
                actual_value=value,
            )
        return episode_num

    def _candidate_episode_keys(self, original: str, episode_num: int) -> list[Any]:
        padded = f"{episode_num:03d}"
        return [
            f"episode{padded}",
            original,
            padded,
            f"ep{episode_num}",
            str(episode_num),
            episode_num,
        ]

    def _find_episode_in_plot_files(
        self,
        candidate_keys: list[Any],
    ) -> tuple[dict[str, Any], dict[str, Any]] | None:
        if not self.plot_dir.exists():
            self.logger.debug("Plot directory does not exist: %s", self.plot_dir)
            return None

        plot_files = sorted(self.plot_dir.glob("*.yaml"))
        if not plot_files:
            self.logger.debug("No plot YAML files found in %s", self.plot_dir)
            return None

        for plot_file in plot_files:
            data = self._load_plot_file(plot_file)
            if not isinstance(data, dict):
                continue

            breakdown = data.get("episode_breakdown")
            if breakdown is None:
                self.logger.debug("episode_breakdown missing in %s", plot_file)
                continue
            if not isinstance(breakdown, dict):
                raise ViewpointDataInvalidError(
                    field_name="episode_breakdown",
                    expected_type="dict",
                    actual_value=type(breakdown).__name__,
                    file_path=str(plot_file),
                )

            for key in candidate_keys:
                if key in breakdown:
                    episode_data = breakdown[key]
                    chapter_data = data.get("chapter_info", {})
                    if not isinstance(chapter_data, dict):
                        raise ViewpointRepositoryError(
                            operation="get_episode_viewpoint_info",
                            reason="chapter_info must be dict",
                            file_path=str(plot_file),
                        )
                    return episode_data, chapter_data

        return None

    def _load_plot_file(self, plot_file: Path) -> dict[str, Any] | Any:
        if self.enable_backup:
            self._create_backup(plot_file)

        try:
            with plot_file.open("r", encoding="utf-8") as handle:
                return yaml.safe_load(handle) or {}
        except yaml.YAMLError as exc:
            parse_error = self._build_yaml_parse_error(plot_file, exc)
            raise ViewpointRepositoryError(
                operation="load_plot_yaml",
                reason=str(parse_error),
                file_path=str(plot_file),
            ) from parse_error
        except UnicodeDecodeError as exc:
            raise ViewpointRepositoryError(
                operation="load_plot_yaml",
                reason=f"UnicodeDecodeError: {exc}",
                file_path=str(plot_file),
            ) from exc
        except OSError as exc:
            raise ViewpointRepositoryError(
                operation="load_plot_yaml",
                reason=str(exc),
                file_path=str(plot_file),
            ) from exc

    def _build_yaml_parse_error(self, plot_file: Path, error: yaml.YAMLError) -> ViewpointYAMLParseError:
        mark = getattr(error, "problem_mark", None)
        line_number = getattr(mark, "line", None)
        column_number = getattr(mark, "column", None)
        if line_number is not None:
            line_number += 1
        if column_number is not None:
            column_number += 1
        return ViewpointYAMLParseError(
            file_path=str(plot_file),
            line_number=line_number,
            column_number=column_number,
            original_error=str(error),
        )

    def _create_backup(self, plot_file: Path) -> None:
        backup_path = plot_file.with_suffix(plot_file.suffix + ".bak")
        try:
            shutil.copy2(plot_file, backup_path)
        except Exception as exc:  # pragma: no cover - defensive
            self.logger.warning("Failed to create backup for %s: %s", plot_file, exc)

    def _convert_to_viewpoint_info(
        self,
        episode_data: Any,
        chapter_data: Any,
    ) -> ViewpointInfo:
        if not isinstance(episode_data, dict):
            raise ViewpointRepositoryError(
                operation="convert_to_viewpoint_info",
                reason="episode_data must be dict",
                episode_data_type=type(episode_data).__name__,
            )
        if not isinstance(chapter_data, dict):
            raise ViewpointRepositoryError(
                operation="convert_to_viewpoint_info",
                reason="chapter_data must be dict",
                chapter_data_type=type(chapter_data).__name__,
            )

        try:
            character = str(
                episode_data.get("character")
                or episode_data.get("viewpoint_character")
                or chapter_data.get("default_viewpoint_character")
                or "Unnamed Character"
            )
            viewpoint_type_value = episode_data.get("viewpoint_type") or chapter_data.get("default_viewpoint_type")
            viewpoint_type = self._resolve_viewpoint_type(viewpoint_type_value)

            complexity_value = episode_data.get("complexity_level") or episode_data.get("complexity")
            complexity_level = self._resolve_complexity_level(complexity_value)

            special_conditions = self._to_string_list(episode_data.get("special_conditions"))
            narrative_focus = str(
                episode_data.get("narrative_focus")
                or chapter_data.get("narrative_focus")
                or ""
            )

            return ViewpointInfo(
                character=character,
                viewpoint_type=viewpoint_type,
                complexity_level=complexity_level,
                special_conditions=special_conditions,
                narrative_focus=narrative_focus,
            )
        except ViewpointRepositoryError:
            raise
        except Exception as exc:  # pragma: no cover - safety net
            raise ViewpointRepositoryError(
                operation="convert_to_viewpoint_info",
                reason=f"ViewpointInfo 変換エラー: {exc}",
                episode_data_type=type(episode_data).__name__,
            ) from exc

    def _resolve_viewpoint_type(self, value: Any) -> ViewpointType:
        if isinstance(value, ViewpointType):
            return value
        if isinstance(value, str):
            key = value.strip().lower()
            if key in VIEWPOINT_TYPE_ALIASES:
                return VIEWPOINT_TYPE_ALIASES[key]
        return ViewpointType.SINGLE_INTROSPECTIVE

    def _resolve_complexity_level(self, value: Any) -> ComplexityLevel:
        if isinstance(value, ComplexityLevel):
            return value
        if isinstance(value, str):
            key = value.strip().lower()
            if key in COMPLEXITY_ALIASES:
                return COMPLEXITY_ALIASES[key]
        return ComplexityLevel.MEDIUM

    def _to_string_list(self, value: Any) -> list[str]:
        if isinstance(value, list):
            return [str(item) for item in value]
        if value is None:
            return []
        return [str(value)]


__all__ = ["PlotViewpointRepository"]
