# File: src/mcp_servers/noveler/domain/entities/mcp_tool_base.py
# Purpose: Define core request/response data structures and the abstract base
#          class for Noveler MCP tools, including shared helpers for
#          normalised responses and PathService fallback metadata.
# Context: Imported by individual MCP tool implementations under
#          src/mcp_servers/noveler/tools. Depends on noveler.infrastructure
#          (path service, cache) but performs no I/O at import time.
"""Core data structures and base classes for Noveler MCP tools.

Purpose:
    Define serialisable request/response payloads and provide a shared base
    class that handles validation, timing, and metadata enrichment for MCP tools.
Context:
    Imported by tool implementations under `src/mcp_servers/noveler/tools`
    to ensure consistent behaviour across FastMCP adapters.
Side Effects:
    Imports infrastructure services at runtime for manuscript loading when
    `_load_episode_content` is invoked.
"""
import time
from abc import ABC, abstractmethod
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Protocol

from noveler.infrastructure.adapters.path_service_adapter import create_path_service
from noveler.infrastructure.caching.file_cache_service import get_file_cache_service


class SupportsFallbackEvents(Protocol):
    """Protocol capturing path services that expose fallback event buffers.

    Purpose:
        Allow `_ps_collect_fallback` to operate on any path-service-like object
        that implements the `get_and_clear_fallback_events` method.

    Args:
        None.

    Returns:
        None.
    Side Effects:
        None.
    """

    def get_and_clear_fallback_events(self) -> list[dict[str, Any]] | None:
        """Return and clear stored fallback events."""
        ...


@dataclass(frozen=True)
class ToolIssue:
    """Describe a single issue detected during tool execution.

    Purpose:
        Provide structured metadata for quality or validation issues reported by tools.

    Args:
        type: Machine-friendly issue category (e.g. ``typo``).
        severity: Qualitative severity indicator.
        message: Human-readable explanation of the issue.
        line_number: Optional primary line number associated with the issue.
        suggestion: Optional remediation guidance.
        end_line_number: Optional end line for ranged issues.
        file_path: Optional file path context.
        line_hash: Optional hash for deduplicating line-level issues.
        block_hash: Optional hash covering a multi-line block.
        reason_code: Stable issue identifier for downstream automation.
        details: Additional structured data (e.g. metrics, thresholds).
        issue_id: Stable identifier derived from issue attributes.
    Side Effects:
        None.
    """

    type: str  # 問題のタイプ（例：typo, long_sentence, etc.）
    severity: str  # 重要度：low, medium, high, critical
    message: str  # 問題の説明
    line_number: int | None = None  # 問題が発生した行番号
    suggestion: str | None = None  # 修正提案
    # 後続修正作業向けの識別子/付加情報（任意）
    end_line_number: int | None = None  # 連続範囲などの終了行
    file_path: str | None = None  # 対象ファイルパス
    line_hash: str | None = None  # 対象行のハッシュ（正規化行のSHA256など）
    block_hash: str | None = None  # 範囲ブロックのハッシュ
    reason_code: str | None = None  # 機械可読な理由コード（例: LINE_WIDTH_OVERFLOW）
    details: dict[str, Any] | None = None  # 測定値・閾値などの詳細
    issue_id: str | None = None  # 安定同定子（ファイルハッシュ/行/タイプに基づく）


@dataclass(frozen=True)
class ToolResponse:
    """Aggregate the result of a tool execution.

    Purpose:
        Provide a uniform container for tool success state, issues, and metadata.

    Args:
        success: Whether the tool completed successfully.
        score: Quality score on a 0–100 scale.
        issues: Collection of issues detected.
        execution_time_ms: Runtime in milliseconds.
        metadata: Arbitrary extra data linked to the execution.
    Side Effects:
        None.
    """

    success: bool
    score: float  # 0-100の品質スコア
    issues: list[ToolIssue]
    execution_time_ms: float
    metadata: dict[str, Any]


@dataclass(frozen=True)
class ToolRequest:
    """Describe the request payload forwarded to an MCP tool.

    Purpose:
        Encapsulate input parameters shared across multiple tools.

    Args:
        episode_number: Target episode number for manuscript operations.
        project_name: Optional project identifier used for path resolution.
        additional_params: Tool-specific parameters forwarded to executors.
    Side Effects:
        None.
    """

    episode_number: int | None = None
    project_name: str | None = None
    additional_params: dict[str, Any] | None = None


class MCPToolBase(ABC):
    """Abstract base class for FastMCP tool implementations.

    Purpose:
        Provide shared validation, response building, and path-service helpers.

    Args:
        tool_name: Human-readable name exposed to MCP clients.
        tool_description: Description displayed alongside the tool.
    Side Effects:
        Initialises fallback event buffers for path-service metadata.
    """

    def __init__(self, tool_name: str, tool_description: str) -> None:
        """Initialise shared tool metadata and state buffers.

        Purpose:
            Capture immutable name/description metadata and prepare fallback storage.

        Args:
            tool_name: Human-readable name exposed externally.
            tool_description: Description used in tool listings and docs.

        Returns:
            None.
        Side Effects:
            Allocates the fallback event buffer.
        """
        self._tool_name = tool_name
        self._tool_description = tool_description
        self._version = "1.0.0"
        # PathServiceのフォールバックイベントを各ツールで一貫して扱うための共通バッファ
        self._fallback_events: list[dict[str, Any]] = []

    @property
    def tool_name(self) -> str:
        """Return the public tool name exported to MCP clients.

        Purpose:
            Expose the registered name for introspection.

        Returns:
            str: Tool name.
        Side Effects:
            None.
        """
        return self._tool_name

    @property
    def tool_description(self) -> str:
        """Return the user-facing description of the tool.

        Purpose:
            Provide documentation text for UIs and logs.

        Returns:
            str: Tool description.
        Side Effects:
            None.
        """
        return self._tool_description

    @property
    def version(self) -> str:
        """Return the semantic version string advertised to clients.

        Purpose:
            Surface compatibility information for introspection.

        Returns:
            str: Version identifier.
        Side Effects:
            None.
        """
        return self._version

    @abstractmethod
    def get_input_schema(self) -> dict[str, Any]:
        """Return the JSON schema describing the tool input.

        Purpose:
            Allow MCP runtimes to validate arguments prior to execution.

        Args:
            None.

        Returns:
            dict[str, Any]: JSON schema definition.
        Side Effects:
            None.
        """

    @abstractmethod
    def execute(self, request: ToolRequest) -> ToolResponse:
        """Execute the tool logic and return a :class:`ToolResponse`.

        Purpose:
            Implement the concrete tool behaviour.

        Args:
            request: ToolRequest containing user-supplied parameters.

        Returns:
            ToolResponse: Result of the tool execution.
        Side Effects:
            Depends on the subclass implementation.
        """

    def _create_response(
        self,
        success: bool,
        score: float,
        issues: list[ToolIssue],
        start_time: float,
        error_message: str | None = None
    ) -> ToolResponse:
        """Build a :class:`ToolResponse` with common metadata.

        Purpose:
            Consolidate response attributes used across multiple tools.

        Args:
            success: Whether the tool completed successfully.
            score: Score assigned by the tool (0–100 scale).
            issues: Issues reported by the tool.
            start_time: Execution start timestamp (``time.time()``).
            error_message: Optional error message to record in metadata.

        Returns:
            ToolResponse: Normalised response object.
        Side Effects:
            Adds error metadata when provided.
        """
        execution_time = (time.time() - start_time) * 1000  # ms

        metadata = {
            "tool_name": self._tool_name,
            "tool_version": self._version,
            "check_timestamp": time.strftime("%Y-%m-%d %H:%M:%S")
        }

        if error_message:
            metadata["error_message"] = error_message

        return ToolResponse(
            success=success,
            score=score,
            issues=issues,
            execution_time_ms=execution_time,
            metadata=metadata
        )

    # ---- PathService fallback helpers (共通ユーティリティ) ----
    def _ps_collect_fallback(self, *services: SupportsFallbackEvents) -> None:
        """Collect fallback events emitted by path service instances.

        Purpose:
            Aggregate fallback diagnostics so they can be attached to responses.

        Args:
            *services: Path service instances providing fallback event buffers.

        Returns:
            None.
        Side Effects:
            Mutates the internal `_fallback_events` buffer.
        """
        for ps in services:
            try:
                ev = ps.get_and_clear_fallback_events() or []
                if ev:
                    self._fallback_events.extend(ev)
            except Exception:
                # 収集失敗はツールの挙動に影響させない
                pass

    def _apply_fallback_metadata(self, response: "ToolResponse") -> None:
        """Attach collected fallback events to the response metadata.

        Purpose:
            Record path-service fallback usage in the response payload.

        Args:
            response: ToolResponse being enriched.

        Returns:
            None.
        Side Effects:
            Mutates response metadata and clears the fallback buffer.
        """
        if getattr(self, "_fallback_events", None):
            response.metadata["path_fallback_used"] = True
            response.metadata["path_fallback_events"] = list(self._fallback_events)
            self._fallback_events.clear()

    def _validate_request(self, request: ToolRequest, *, require_episode_number: bool = True) -> None:
        """Perform basic validation on the tool request.

        Purpose:
            Guard against missing or invalid core parameters.

        Args:
            request: Incoming request payload.
            require_episode_number: When ``True`` ensure episode_number is positive.

        Returns:
            None.
        Side Effects:
            Raises `ValueError` on invalid inputs.

        Raises:
            ValueError: If the episode number requirement is violated.
        """
        if require_episode_number:
            if request.episode_number is None or request.episode_number <= 0:
                msg = f"Invalid episode number: {request.episode_number}"
                raise ValueError(msg)

    def _get_common_input_schema(self) -> dict[str, Any]:
        """Return the common subset of the JSON input schema.

        Purpose:
            Provide reusable schema fragments for derived tools.

        Args:
            None.

        Returns:
            dict[str, Any]: Schema with episode/project fields.
        Side Effects:
            None.
        """
        return {
            "type": "object",
            "properties": {
                "episode_number": {
                    "type": "integer",
                    "description": "対象エピソード番号",
                    "minimum": 1
                },
                "project_name": {
                    "type": "string",
                    "description": "プロジェクト名（省略時は環境変数から取得）"
                }
            },
            "required": ["episode_number"]
        }

    def _load_episode_content(self, episode_number: int, project_name: str | None) -> str:
        """Load manuscript content for the requested episode.

        Purpose:
            Retrieve manuscript text via the path service and cache utilities.

        Args:
            episode_number: Episode identifier to load.
            project_name: Optional project override for path resolution.

        Returns:
            str: Manuscript content as UTF-8 text.
        Side Effects:
            Reads from disk and may use caching infrastructure.

        Raises:
            FileNotFoundError: If the episode file cannot be located or read.
        """
        try:
            # DDD準拠: Infrastructure層のパスサービスとキャッシュサービスを使用
            # プロジェクト指定があれば使用、なければ現在のプロジェクト
            path_service = create_path_service(Path(project_name)) if project_name else create_path_service()

            # 高速化: ファイルキャッシュサービスを活用
            cache_service = get_file_cache_service()
            manuscript_dir = path_service.get_manuscript_dir()

            # キャッシュ最適化されたエピソードファイル取得
            episode_file = cache_service.get_episode_file_cached(manuscript_dir, episode_number)

            if not episode_file or not episode_file.exists():
                msg = f"Episode {episode_number} file not found"
                raise FileNotFoundError(msg)

            try:
                from noveler.infrastructure.caching.file_cache_service import read_text_cached
                return read_text_cached(episode_file)
            except Exception:
                return episode_file.read_text(encoding="utf-8")

        except FileNotFoundError:
            raise
        except Exception as e:
            msg = f"Failed to load episode {episode_number}: {e!s}"
            raise FileNotFoundError(msg) from e
