"""MCP tool wiring utilities.

This module provides a decorator for MCP tool functions to standardize:
- rate limiting
- input validation
- consistent error mapping
- structured security logging hooks

It is intentionally framework-agnostic: it does not import FastMCP directly.
The decorator is meant to be applied to functions already registered via
`@mcp.tool(...)` in `registry/server.py`.

Design notes:
- The decorator takes callables for rate limiting, validation, and error mapping.
- It keeps the wrapped function signature compatible with MCP tool calling.
- There is no async support here (current tools are sync). If you add async tools,
  we can extend this with an async-aware wrapper.
"""

from __future__ import annotations

from dataclasses import dataclass
from functools import wraps
from typing import Any, Callable, Dict, Optional, TypeVar

from ...logging_config import get_logger

logger = get_logger(__name__)

F = TypeVar("F", bound=Callable[..., Any])


@dataclass(frozen=True)
class ToolErrorPayload:
    """Normalized error payload returned to MCP client.

    MCP tools often return structured output; we keep this minimal and stable.
    """

    error: str
    error_type: str
    details: Dict[str, Any]

    def to_dict(self) -> Dict[str, Any]:
        return {
            "error": self.error,
            "type": self.error_type,
            "details": self.details,
        }


def _default_error_mapper(exc: Exception) -> ToolErrorPayload:
    """Fallback error mapper."""
    return ToolErrorPayload(
        error=str(exc) or "unknown error",
        error_type=type(exc).__name__,
        details={},
    )


def mcp_tool_wrapper(
    *,
    tool_name: str,
    rate_limit_key: Callable[..., str],
    check_rate_limit: Callable[[str], None],
    validate: Optional[Callable[..., None]] = None,
    error_mapper: Optional[Callable[[Exception], ToolErrorPayload]] = None,
    on_error: Optional[Callable[[Exception, Dict[str, Any]], None]] = None,
) -> Callable[[F], F]:
    """Decorator to standardize MCP tool behavior.

    Args:
        tool_name: Human-readable tool name (used in error payload metadata).
        rate_limit_key: Callable that builds a rate limit bucket key from args/kwargs.
        check_rate_limit: Callable that enforces rate limit for the computed key.
                          Should raise (e.g. RateLimitExceeded) when exceeded.
        validate: Optional callable to validate inputs. Should raise ValueError on invalid input.
                  Signature should match the wrapped tool function.
        error_mapper: Optional callable mapping Exception -> ToolErrorPayload.
                      If omitted, a minimal default is used.
        on_error: Optional hook called on exception with (exc, context_dict).

    Returns:
        Decorated function.
    """
    mapper = error_mapper or _default_error_mapper

    def decorator(func: F) -> F:
        @wraps(func)
        def wrapped(*args: Any, **kwargs: Any) -> Any:
            # Rate limit first (cheapest check) to reduce abuse surface.
            key = rate_limit_key(*args, **kwargs)
            check_rate_limit(key)

            # Validate inputs if provided.
            if validate is not None:
                validate(*args, **kwargs)

            try:
                return func(*args, **kwargs)
            except Exception as exc:
                # Optional error hook (e.g. security auditing).
                if on_error is not None:
                    try:
                        on_error(
                            exc,
                            {
                                "tool": tool_name,
                                "rate_limit_key": key,
                                "args_count": len(args),
                                "kwargs_keys": list(kwargs.keys()),
                            },
                        )
                    except (TypeError, ValueError, RuntimeError) as hook_err:
                        # Never let the error hook override the original failure.
                        # Log but don't propagate hook errors.
                        logger.debug(
                            "error_hook_failed",
                            tool=tool_name,
                            hook_error=str(hook_err),
                        )

                payload = mapper(exc)
                # Return a stable, tool-friendly dict. MCP will surface this as tool error.
                return payload.to_dict()

        return wrapped  # type: ignore[misc]

    return decorator


def key_global(*_: Any, **__: Any) -> str:
    """Rate limit key for globally-scoped tools."""
    return "global"


def key_per_provider(provider: str, *_: Any, **__: Any) -> str:
    """Rate limit key scoped per provider."""
    return f"provider:{provider}"


def key_registry_invoke(provider: str, tool: str, *_: Any, **__: Any) -> str:
    """Rate limit key specialized for tool invocation (per provider)."""
    # Keep it coarse by default to avoid key explosion; include tool name if desired.
    return f"registry_invoke:{provider}"


def chain_validators(*validators: Callable[..., None]) -> Callable[..., None]:
    """Combine multiple validators into a single callable.

    Each validator is called in order. First exception stops the chain.
    """

    def _combined(*args: Any, **kwargs: Any) -> None:
        for v in validators:
            v(*args, **kwargs)

    return _combined
