#!/usr/bin/env python3

"""Application.validators.plot_adherence_validator
Where: Application validator verifying plot adherence rules.
What: Evaluates manuscripts against expected plot beats and constraints.
Why: Helps authors avoid deviations from planned plot structure.
"""

from __future__ import annotations

"""プロット準拠検証バリデーター

執筆原稿とプロットの準拠性を検証するアプリケーション層バリデーター
SPEC-PLOT-ADHERENCE-001準拠実装
"""


from dataclasses import dataclass, field
from enum import Enum
from typing import TYPE_CHECKING, Any

from noveler.application.adapters.cli_adapter_factory import CliAdapterFactory

if TYPE_CHECKING:
    from pathlib import Path

logger = CliAdapterFactory.create_logger(__name__)


class AdherenceElementType(Enum):
    """プロット要素タイプ"""

    KEY_EVENT = "key_event"
    CHARACTER_DEVELOPMENT = "character_development"
    WORLD_BUILDING = "world_building"
    FORESHADOWING = "foreshadowing"


@dataclass
class PlotElement:
    """プロット要素値オブジェクト"""

    element_type: AdherenceElementType
    description: str
    required: bool = True
    weight: float = 1.0
    metadata: dict[str, Any] = field(default_factory=dict)

    def __hash__(self) -> int:
        """ハッシュ値を計算（辞書のキーとして使用可能にする）

        metadata は除外してハッシュ化（mutable なため）
        """
        return hash((self.element_type, self.description, self.required, self.weight))

    def __eq__(self, other: object) -> bool:
        """等価性比較（metadata は除外）"""
        if not isinstance(other, PlotElement):
            return False
        return (
            self.element_type == other.element_type
            and self.description == other.description
            and self.required == other.required
            and self.weight == other.weight
        )


@dataclass
class AdherenceScore:
    """準拠率値オブジェクト"""

    total_score: float  # 0-100
    element_scores: dict[AdherenceElementType, float] = field(default_factory=dict)
    implemented_count: int = 0
    total_count: int = 0

    @property
    def is_excellent(self) -> bool:
        """優秀評価判定（95%以上）"""
        return self.total_score >= 95.0

    @property
    def is_acceptable(self) -> bool:
        """許容評価判定（80%以上）"""
        return self.total_score >= 80.0


@dataclass
class PlotAdherenceResult:
    """プロット準拠検証結果"""

    episode_number: int
    adherence_score: AdherenceScore
    missing_elements: list[PlotElement] = field(default_factory=list)
    extra_elements: list[PlotElement] = field(default_factory=list)
    improvement_suggestions: list[str] = field(default_factory=list)
    validation_timestamp: str | None = None


class PlotAdherenceValidator:
    """プロット準拠検証バリデーター

    執筆完了後の原稿がプロット要件を満たしているかを検証する。
    SPEC-PLOT-ADHERENCE-001の中核コンポーネント。
    """

    def __init__(
        self,
        llm_plot_verifier: Any | None = None,
        llm_improvement_suggestion_service: Any | None = None,
    ) -> None:
        """初期化

        Args:
            llm_plot_verifier: LLMPlotVerifier（任意、未指定時はフォールバック）
            llm_improvement_suggestion_service: LLMImprovement SuggestionService（任意）
        """
        self.logger = logger
        self._llm_plot_verifier = llm_plot_verifier
        self._llm_improvement_suggestion_service = llm_improvement_suggestion_service
        # 要素別重み設定（仕様書準拠）
        self._element_weights = {
            AdherenceElementType.KEY_EVENT: 0.4,
            AdherenceElementType.CHARACTER_DEVELOPMENT: 0.3,
            AdherenceElementType.WORLD_BUILDING: 0.2,
            AdherenceElementType.FORESHADOWING: 0.1,
        }

    async def validate_adherence(
        self, episode_number: int, manuscript_content: str, plot_data: dict[str, Any], project_root: Path | None = None
    ) -> PlotAdherenceResult:
        """プロット準拠性検証メイン処理

        Args:
            episode_number: エピソード番号
            manuscript_content: 執筆完了原稿
            plot_data: プロットデータ（YAML形式）
            project_root: プロジェクトルートパス

        Returns:
            PlotAdherenceResult: 検証結果
        """
        self.logger.info(
            "プロット準拠検証開始: 第%03d話",
            episode_number,
        )

        try:
            # プロット要素抽出
            plot_elements = await self._extract_plot_elements(plot_data)

            # 原稿内容分析（LLMベース検証）
            if self._llm_plot_verifier:
                # LLM検証を使用
                verification_results = await self.verify_plot_elements_with_llm(manuscript_content, plot_elements)
                manuscript_elements = self._convert_verification_results_to_elements(verification_results, plot_elements)
            else:
                # フォールバック: 従来の単語マッチング
                self.logger.warning("LLMPlotVerifierが未設定のため、単語マッチングで検証します")
                manuscript_elements = await self._analyze_manuscript_content(manuscript_content)

            # 準拠性評価
            adherence_score = await self._calculate_adherence_score(plot_elements, manuscript_elements)

            # 不足・過剰要素検出
            missing_elements = await self._detect_missing_elements(plot_elements, manuscript_elements)
            extra_elements = await self._detect_extra_elements(plot_elements, manuscript_elements)

            # 改善提案生成
            improvement_suggestions = await self._generate_improvement_suggestions(
                missing_elements,
                extra_elements,
                adherence_score,
                manuscript_content=manuscript_content,
            )

            result = PlotAdherenceResult(
                episode_number=episode_number,
                adherence_score=adherence_score,
                missing_elements=missing_elements,
                extra_elements=extra_elements,
                improvement_suggestions=improvement_suggestions,
            )

            self.logger.info(
                "プロット準拠検証完了: 第%03d話 準拠率%.1f%%",
                episode_number,
                adherence_score.total_score,
            )

            return result

        except Exception as e:
            self.logger.exception(
                "プロット準拠検証エラー: %s",
                e,
            )
            # フォールバック結果を返す
            return PlotAdherenceResult(
                episode_number=episode_number,
                adherence_score=AdherenceScore(total_score=0.0),
                improvement_suggestions=[f"検証エラー: {e!s}"],
            )

    async def _extract_plot_elements(self, plot_data: dict[str, Any]) -> list[PlotElement]:
        """プロットデータから検証要素を抽出

        Args:
            plot_data: プロットYAMLデータ

        Returns:
            list[PlotElement]: プロット要素リスト
        """
        elements = []

        # キーイベント抽出
        elements.extend(
            PlotElement(
                element_type=AdherenceElementType.KEY_EVENT, description=str(event), required=True
            )
            for event in plot_data.get("key_events", [])
        )

        # キャラクター要素抽出
        elements.extend(
            PlotElement(
                element_type=AdherenceElementType.CHARACTER_DEVELOPMENT,
                description=str(char_element),
                required=True,
            )
            for char_element in plot_data.get("character_development", [])
        )

        # 世界観要素抽出
        elements.extend(
            PlotElement(
                element_type=AdherenceElementType.WORLD_BUILDING,
                description=str(world_element),
                required=True,
            )
            for world_element in plot_data.get("world_building", [])
        )

        # 伏線要素抽出
        elements.extend(
            PlotElement(
                element_type=AdherenceElementType.FORESHADOWING,
                description=str(foreshadow_element),
                required=True,
            )
            for foreshadow_element in plot_data.get("foreshadowing", [])
        )

        self.logger.debug(
            "抽出されたプロット要素: %s個",
            len(elements),
        )
        return elements

    async def _analyze_manuscript_content(self, manuscript: str) -> list[PlotElement]:
        """原稿内容を分析して実装要素を検出（フォールバック専用）

        このメソッドはLLMPlotVerifierが利用不可の場合のフォールバック処理。
        通常はvalidate_adherence内でLLM検証が使用される。

        Args:
            manuscript: 原稿テキスト

        Returns:
            list[PlotElement]: 実装されている要素リスト（単語マッチングベース）
        """
        # フォールバック: 従来の単語マッチング
        self.logger.info("フォールバック: 単語マッチングで原稿を分析します")
        return self._fallback_keyword_analysis(manuscript)

    def _fallback_keyword_analysis(self, manuscript: str) -> list[PlotElement]:
        """フォールバック: 単語マッチング分析

        Args:
            manuscript: 原稿テキスト

        Returns:
            list[PlotElement]: 検出された要素リスト
        """
        elements = []

        # キーワードベースの簡易分析（従来実装）
        if "ギルド" in manuscript:
            elements.append(
                PlotElement(element_type=AdherenceElementType.WORLD_BUILDING, description="ギルドの描写", required=True)
            )

        if "主人公" in manuscript or "彼" in manuscript:
            elements.append(
                PlotElement(
                    element_type=AdherenceElementType.CHARACTER_DEVELOPMENT, description="主人公の描写", required=True
                )
            )

        self.logger.debug(
            "分析された原稿要素: %s個（フォールバック）",
            len(elements),
        )
        return elements

    async def _calculate_adherence_score(
        self, plot_elements: list[PlotElement], manuscript_elements: list[PlotElement]
    ) -> AdherenceScore:
        """準拠率を計算

        Args:
            plot_elements: プロット要素
            manuscript_elements: 原稿実装要素

        Returns:
            AdherenceScore: 算出された準拠率
        """
        element_scores = {}
        total_weighted_score = 0.0

        for element_type in AdherenceElementType:
            plot_count = len([e for e in plot_elements if e.element_type == element_type])
            impl_count = len([e for e in manuscript_elements if e.element_type == element_type])

            if plot_count > 0:
                type_score = min(impl_count / plot_count, 1.0) * 100
            else:
                type_score = 100.0  # 要求されていない要素は満点

            element_scores[element_type] = type_score
            weight = self._element_weights.get(element_type, 0.0)
            total_weighted_score += type_score * weight

        return AdherenceScore(
            total_score=total_weighted_score,
            element_scores=element_scores,
            implemented_count=len(manuscript_elements),
            total_count=len(plot_elements),
        )

    async def _detect_missing_elements(
        self, plot_elements: list[PlotElement], manuscript_elements: list[PlotElement]
    ) -> list[PlotElement]:
        """不足要素を検出

        Args:
            plot_elements: プロット要素
            manuscript_elements: 原稿実装要素

        Returns:
            list[PlotElement]: 不足している要素
        """
        # TODO: より精密な要素マッチング実装
        # 現在は簡易実装
        missing = []

        for plot_element in plot_elements:
            # 原稿に対応する要素があるかチェック（簡易版）
            has_matching = any(m.element_type == plot_element.element_type for m in manuscript_elements)

            if not has_matching:
                missing.append(plot_element)

        return missing

    async def verify_plot_elements_with_llm(
        self,
        manuscript: str,
        plot_elements: list[PlotElement],
    ) -> dict[PlotElement, Any]:
        """LLMを使ってプロット要素を検証（新規メソッド）

        Args:
            manuscript: 原稿テキスト
            plot_elements: 検証対象プロット要素リスト

        Returns:
            dict: プロット要素 → ElementVerificationResult のマッピング
        """
        if self._llm_plot_verifier is None:
            self.logger.warning("LLMPlotVerifierが未設定のため、検証をスキップします")
            return {}

        results = {}
        for element in plot_elements:
            try:
                verification_result = await self._llm_plot_verifier.verify_element(
                    manuscript=manuscript,
                    plot_element=element,
                )
                results[element] = verification_result
            except Exception as e:
                self.logger.exception("プロット要素検証エラー: %s", e)
                # エラー時は低信頼度のMISSING扱い
                from noveler.domain.value_objects.element_verification_result import ElementVerificationResult

                results[element] = ElementVerificationResult.create_missing(
                    reason=f"検証エラー: {e!s}",
                    confidence=0.0,
                )

        return results

    def _convert_verification_results_to_elements(
        self,
        verification_results: dict[PlotElement, Any],
        original_elements: list[PlotElement],
    ) -> list[PlotElement]:
        """ElementVerificationResult を PlotElement リストに変換

        Args:
            verification_results: LLM検証結果（PlotElement → ElementVerificationResult）
            original_elements: 元のプロット要素リスト

        Returns:
            list[PlotElement]: 実装されている要素のリスト
        """
        implemented_elements = []

        for element, verification_result in verification_results.items():
            # IMPLEMENTED または PARTIAL の場合は実装されていると見なす
            if verification_result.is_implemented() or verification_result.is_partial():
                # 信頼度が高い場合のみ追加
                if verification_result.has_high_confidence(threshold=0.5):
                    implemented_elements.append(element)

        return implemented_elements

    async def _detect_extra_elements(
        self, plot_elements: list[PlotElement], manuscript_elements: list[PlotElement]
    ) -> list[PlotElement]:
        """過剰要素を検出

        Args:
            plot_elements: プロット要素
            manuscript_elements: 原稿実装要素

        Returns:
            list[PlotElement]: 過剰な要素
        """
        # TODO: 過剰要素の検出実装
        # 現在は空リストを返す
        return []

    async def _generate_improvement_suggestions(
        self,
        missing_elements: list[PlotElement],
        extra_elements: list[PlotElement],
        adherence_score: AdherenceScore,
        manuscript_content: str | None = None,
    ) -> list[str]:
        """改善提案を生成（LLMベースまたはテンプレート）

        Args:
            missing_elements: 不足要素
            extra_elements: 過剰要素
            adherence_score: 準拠率
            manuscript_content: 原稿内容（LLM使用時は必須）

        Returns:
            list[str]: 改善提案リスト
        """
        # LLMサービスが設定されており、原稿が利用可能な場合はLLMを使用
        if (
            self._llm_improvement_suggestion_service
            and manuscript_content
            and missing_elements
        ):
            try:
                self.logger.info("LLMベース改善提案生成を開始します")
                suggestions = await self._llm_improvement_suggestion_service.generate_suggestions(
                    manuscript=manuscript_content,
                    missing_elements=missing_elements,
                    extra_elements=extra_elements,
                    adherence_score=adherence_score.total_score,
                )
                return suggestions
            except Exception as e:
                self.logger.warning("LLM改善提案生成に失敗、テンプレートを使用します: %s", e)

        # テンプレートベースの提案（フォールバック）
        suggestions: list[str] = []

        for missing in missing_elements:
            if missing.element_type == AdherenceElementType.KEY_EVENT:
                suggestions.append(f"重要イベントの描写を追加: {missing.description}")
            elif missing.element_type == AdherenceElementType.CHARACTER_DEVELOPMENT:
                suggestions.append(f"キャラクター描写を強化: {missing.description}")
            elif missing.element_type == AdherenceElementType.WORLD_BUILDING:
                suggestions.append(f"世界観描写を追加: {missing.description}")
            elif missing.element_type == AdherenceElementType.FORESHADOWING:
                suggestions.append(f"伏線の設置を検討: {missing.description}")

        # 準拠率に基づく全体提案
        if adherence_score.total_score < 80.0:
            suggestions.append("プロットの見直しと原稿の大幅な改善が推奨されます")
        elif adherence_score.total_score < 95.0:
            suggestions.append("細部の調整でより高品質な原稿になります")
        else:
            suggestions.append("優秀な準拠率です。このまま公開品質を維持してください")

        return suggestions
