#!/usr/bin/env python3
# File: src/noveler/domain/services/llm_plot_verifier.py
# Purpose: LLMを使用したプロット要素検証ドメインサービス
# Context: SPEC-PLOT-ADHERENCE-001 改善版（単語マッチング→意味的検証）

"""LLMベースプロット要素検証サービス

プロット要素が原稿内で適切に実装されているかを、LLMを使って意味的に検証。
従来の単語マッチングから、文脈理解による高精度検証への改善。

Purpose:
    プロット要素（キーイベント、キャラクター描写等）の実装状況を、
    LLMの自然言語理解能力を活用して検証。エビデンス（引用箇所）と
    信頼度スコアを提供し、説明可能性を向上。

Args:
    session_executor: Claude LLM実行インターフェース
    environment_detector: 実行環境検出インターフェース

Returns:
    ElementVerificationResult: 検証結果（status, evidence, confidence）

Side Effects:
    LLM APIへのHTTPリクエスト（session_executor経由）
"""

from __future__ import annotations

import json
import sys
from typing import TYPE_CHECKING, Any

from noveler.domain.value_objects.element_verification_result import ElementVerificationResult

if TYPE_CHECKING:
    from noveler.application.validators.plot_adherence_validator import PlotElement
    from noveler.domain.interfaces.claude_session_interface import (
        ClaudeSessionExecutorInterface,
        EnvironmentDetectorInterface,
    )

# 定数定義
DEFAULT_MAX_MANUSCRIPT_LENGTH = 3000  # LLMトークン制限に基づく推奨値
DEFAULT_VERIFICATION_TIMEOUT = 30  # LLM検証のデフォルトタイムアウト（秒）


class PlotVerificationError(Exception):
    """プロット検証エラー"""


class LLMPlotVerifier:
    """LLMベースプロット要素検証サービス

    ClaudeSessionExecutorInterfaceを使用してLLMにプロンプトを送信し、
    プロット要素が原稿内で実装されているかを意味的に検証。
    """

    def __init__(
        self,
        session_executor: ClaudeSessionExecutorInterface,
        environment_detector: EnvironmentDetectorInterface,
        max_manuscript_length: int = DEFAULT_MAX_MANUSCRIPT_LENGTH,
        verification_timeout: int = DEFAULT_VERIFICATION_TIMEOUT,
    ) -> None:
        """初期化

        Args:
            session_executor: LLMセッション実行インターフェース
            environment_detector: 環境検出インターフェース
            max_manuscript_length: LLMに送信する原稿の最大長（デフォルト: 3000文字）
            verification_timeout: LLM検証のタイムアウト秒数（デフォルト: 30秒）
        """
        self._session_executor = session_executor
        self._environment_detector = environment_detector
        self._max_manuscript_length = max_manuscript_length
        self._verification_timeout = verification_timeout

    async def verify_element(
        self,
        manuscript: str,
        plot_element: PlotElement,
    ) -> ElementVerificationResult:
        """プロット要素1件の検証

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

        Returns:
            ElementVerificationResult: 検証結果

        Raises:
            PlotVerificationError: 検証処理でエラーが発生した場合
        """
        # LLM利用可否チェック
        if not self._session_executor.is_available():
            msg = "LLMセッションが利用できません"
            raise PlotVerificationError(msg)

        # LLM検証プロンプト構築
        prompt = self._build_verification_prompt(manuscript, plot_element)

        # LLM実行
        try:
            response = self._session_executor.execute_prompt(
                prompt=prompt,
                response_format="json",
                timeout=self._verification_timeout,
            )
        except Exception as e:
            msg = f"LLMプロンプト実行に失敗: {e}"
            raise PlotVerificationError(msg) from e

        # レスポンスパース
        return self._parse_llm_response(response, plot_element)

    async def verify_elements_batch(
        self,
        manuscript: str,
        plot_elements: list[PlotElement],
    ) -> list[ElementVerificationResult]:
        """複数プロット要素の一括検証（効率化）

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

        Returns:
            list[ElementVerificationResult]: 検証結果リスト
        """
        # 将来的にバッチ処理最適化（1リクエストで複数要素検証）
        # 現状は順次実行
        results = []
        for element in plot_elements:
            result = await self.verify_element(manuscript, element)
            results.append(result)
        return results

    def _build_verification_prompt(
        self,
        manuscript: str,
        plot_element: PlotElement,
    ) -> str:
        """LLM検証用プロンプト生成

        Args:
            manuscript: 原稿テキスト
            plot_element: プロット要素

        Returns:
            str: LLM検証プロンプト
        """
        # 原稿が長すぎる場合は制限
        manuscript_excerpt = (
            manuscript[: self._max_manuscript_length]
            if len(manuscript) > self._max_manuscript_length
            else manuscript
        )

        element_type_jp = {
            "key_event": "重要イベント",
            "character_development": "キャラクター描写",
            "world_building": "世界観設定",
            "foreshadowing": "伏線設置",
        }.get(plot_element.element_type.value, "プロット要素")

        return f"""あなたは小説原稿のプロット準拠性を検証する専門家です。

【原稿】
{manuscript_excerpt}

【検証項目】
カテゴリ: {element_type_jp}
内容: {plot_element.description}

【タスク】
上記の検証項目が原稿内で適切に描写・実装されているかを判定してください。

【判定基準】
- IMPLEMENTED: 検証項目が明確に描写されている
- PARTIAL: 間接的な言及や部分的な実装がある
- MISSING: 検証項目に関する記述が見当たらない

【出力形式（JSON）】
以下のJSON形式で回答してください（他のテキストは含めないこと）：

{{
  "status": "IMPLEMENTED" | "PARTIAL" | "MISSING",
  "evidence": "根拠となる原稿の引用（100文字以内）",
  "confidence": 0.0～1.0の信頼度スコア,
  "reason": "判定理由の簡潔な説明（50文字以内）"
}}

重要: 回答は必ず上記のJSON形式のみとし、追加の説明や```json```マーカーは不要です。
"""

    def _parse_llm_response(
        self,
        response: dict[str, Any],
        plot_element: PlotElement,
    ) -> ElementVerificationResult:
        """LLMレスポンスのパース

        Args:
            response: LLMレスポンス
            plot_element: プロット要素

        Returns:
            ElementVerificationResult: 検証結果

        Raises:
            PlotVerificationError: パースに失敗した場合
        """
        try:
            # レスポンス構造の確認
            if not response.get("success", True):
                error_msg = response.get("error", "LLM実行失敗")
                raise PlotVerificationError(error_msg)

            # データ抽出
            data = response.get("data", response)

            # 文字列の場合はJSON解析
            if isinstance(data, str):
                try:
                    data = json.loads(data)
                except json.JSONDecodeError as e:
                    raise PlotVerificationError(f"JSONパースエラー: {e}") from e

            # 必須フィールド検証
            if not isinstance(data, dict):
                raise PlotVerificationError(f"LLMレスポンスが辞書型ではありません: {type(data)}")

            status = data.get("status")
            if status not in ("IMPLEMENTED", "PARTIAL", "MISSING"):
                raise PlotVerificationError(f"無効なstatus値: {status}")

            confidence = float(data.get("confidence", 0.5))
            reason = str(data.get("reason", "LLM判定"))
            evidence = data.get("evidence")

            # ElementVerificationResult生成
            if status == "IMPLEMENTED":
                return ElementVerificationResult.create_implemented(
                    evidence=evidence or "",
                    confidence=confidence,
                    reason=reason,
                )
            if status == "PARTIAL":
                return ElementVerificationResult.create_partial(
                    reason=reason,
                    confidence=confidence,
                    evidence=evidence,
                )
            # MISSING
            return ElementVerificationResult.create_missing(
                reason=reason,
                confidence=confidence,
            )

        except PlotVerificationError:
            raise
        except Exception as e:
            msg = f"LLMレスポンスのパースに失敗: {e}"
            raise PlotVerificationError(msg) from e
