"""Base classes for tool handlers.

This module provides the foundational classes for the MCP tool handler
architecture, including the abstract ToolHandler base class and
EndpointMapping for HTTP routing.
"""

from __future__ import annotations

import json
import logging
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any

from mcp.types import ImageContent, TextContent, Tool

if TYPE_CHECKING:
    from ..client import FastAPIClient

__all__ = [
    "EndpointMapping",
    "ToolHandler",
    "PROJECT_PARAM_SCHEMA",
    "add_project_param",
]

logger = logging.getLogger(__name__)

# Common schema components for project routing
PROJECT_PARAM_SCHEMA = {
    "project": {
        "type": "string",
        "description": (
            "Optional project name or path to route this request to a specific server. "
            "If not provided, uses the first available server from the registry or "
            "GFP_API_URL if set. Use list_projects to see available projects."
        ),
    },
}


def add_project_param(schema: dict) -> dict:
    """Add optional project parameter to a tool schema.

    Args:
        schema: Tool input schema dict

    Returns:
        Schema with project parameter added to properties
    """
    if "properties" not in schema:
        schema["properties"] = {}
    schema["properties"].update(PROJECT_PARAM_SCHEMA)
    return schema


@dataclass
class EndpointMapping:
    """Configuration for mapping an MCP tool to a FastAPI endpoint.

    This class defines the HTTP method and path for a tool's backend endpoint.
    Unlike the previous implementation in mappings.py, request and response
    transformers are now methods on the ToolHandler class.

    Attributes:
        method: HTTP method (GET, POST, etc.)
        path: API endpoint path (e.g., "/api/build-cells")
    """

    method: str
    path: str


class ToolHandler(ABC):
    """Abstract base class for MCP tool handlers.

    Each tool handler encapsulates:
    - The MCP Tool definition (name, description, input schema)
    - The HTTP endpoint mapping (method, path)
    - Request transformation (MCP args -> HTTP params/body)
    - Response transformation (HTTP response -> MCP format)
    - Execution logic (handle method)

    Subclasses must implement the `name` and `definition` properties.
    Other methods have sensible defaults but can be overridden.
    """

    @property
    @abstractmethod
    def name(self) -> str:
        """Tool name identifier.

        This must match the name in the Tool definition and is used
        for routing in the server's call_tool handler.
        """
        ...

    @property
    @abstractmethod
    def definition(self) -> Tool:
        """MCP Tool definition.

        Returns the complete Tool object with name, description,
        and input schema for the MCP protocol.
        """
        ...

    @property
    def mapping(self) -> EndpointMapping | None:
        """Endpoint mapping for HTTP-backed tools.

        Returns None for registry-only tools (like list_projects)
        that don't make HTTP requests to the backend.
        """
        return None

    def transform_request(self, args: dict[str, Any]) -> dict[str, Any]:
        """Transform MCP tool arguments to HTTP request parameters.

        Override this method to customize how tool arguments are
        converted to HTTP request format.

        Args:
            args: MCP tool arguments from the client

        Returns:
            Dict that may contain:
            - "params": Query parameters for GET requests
            - "json_data": JSON body for POST requests
            - "data": Form data for POST requests
            - "path": Dynamic path override (e.g., "/api/cell/{name}")
            - "fallback_path": Alternative endpoint for 404 fallback
            - "fallback_json_data": Alternative body for fallback request
        """
        return {}

    def transform_response(self, response: Any) -> Any:
        """Transform HTTP response to MCP-friendly format.

        Override this method to customize how backend responses are
        formatted for the AI assistant.

        Args:
            response: Raw response from the FastAPI backend

        Returns:
            Transformed response (typically a dict or the original response)
        """
        return response

    async def handle(
        self,
        arguments: dict[str, Any],
        client: FastAPIClient,
    ) -> list[TextContent | ImageContent]:
        """Execute the tool and return results.

        This is the main entry point called by the server's call_tool handler.
        The default implementation uses the endpoint mapping and transformers.
        Override for custom execution logic (e.g., registry-only tools).

        Args:
            arguments: MCP tool arguments from the client
            client: FastAPI client for making HTTP requests

        Returns:
            List of TextContent and/or ImageContent responses
        """
        import httpx

        mapping = self.mapping
        if mapping is None:
            error_msg = f"Tool {self.name} has no endpoint mapping"
            logger.error(error_msg)
            return [TextContent(type="text", text=json.dumps({"error": error_msg}))]

        try:
            project = arguments.get("project")
            transformed = self.transform_request(arguments)

            method = mapping.method
            path = transformed.get("path", mapping.path)
            params = transformed.get("params")
            json_data = transformed.get("json_data")
            data = transformed.get("data")
            fallback_path = transformed.get("fallback_path")
            fallback_json_data = transformed.get("fallback_json_data")

            try:
                response = await client.request(
                    method=method,
                    path=path,
                    params=params,
                    json_data=json_data,
                    data=data,
                    project=project,
                )
            except httpx.HTTPStatusError as e:
                if e.response.status_code == 404 and fallback_path:
                    logger.info(
                        "Endpoint %s returned 404, trying fallback: %s",
                        path,
                        fallback_path,
                    )
                    response = await client.request(
                        method=method,
                        path=fallback_path,
                        params=params,
                        json_data=fallback_json_data,
                        data=data,
                        project=project,
                    )
                else:
                    raise

            result = self.transform_response(response)
            logger.debug("Tool %s result: %s", self.name, result)

            return [
                TextContent(
                    type="text",
                    text=json.dumps(result, indent=2),
                )
            ]

        except Exception as e:
            error_msg = f"Tool execution failed: {e!s}"
            logger.exception(error_msg)
            return [
                TextContent(
                    type="text",
                    text=json.dumps({"error": error_msg}),
                )
            ]
