"""Response limiting middleware for controlling tool response sizes."""

from __future__ import annotations

import logging
from typing import Any

import mcp.types as mt
import pydantic_core
from mcp.types import TextContent

from fastmcp.tools.base import ToolResult

from .middleware import CallNext, Middleware, MiddlewareContext

__all__ = ["ResponseLimitingMiddleware"]

logger = logging.getLogger(__name__)


class ResponseLimitingMiddleware(Middleware):
    """Middleware that limits the response size of tool calls.

    Intercepts tool call responses and enforces size limits. If a response
    exceeds the limit, it extracts text content, truncates it, and returns
    a single TextContent block.

    Example:
        ```python
        from fastmcp import FastMCP
        from fastmcp.server.middleware.response_limiting import (
            ResponseLimitingMiddleware,
        )

        mcp = FastMCP("MyServer")

        # Limit all tool responses to 500KB
        mcp.add_middleware(ResponseLimitingMiddleware(max_size=500_000))

        # Limit only specific tools
        mcp.add_middleware(
            ResponseLimitingMiddleware(
                max_size=100_000,
                tools=["search", "fetch_data"],
            )
        )
        ```
    """

    def __init__(
        self,
        *,
        max_size: int = 1_000_000,
        truncation_suffix: str = "\n\n[Response truncated due to size limit]",
        tools: list[str] | None = None,
    ) -> None:
        """Initialize response limiting middleware.

        Args:
            max_size: Maximum response size in bytes. Defaults to 1MB (1,000,000).
            truncation_suffix: Suffix to append when truncating responses.
                Defaults to "\\n\\n[Response truncated due to size limit]".
            tools: List of tool names to apply limiting to. If None, applies to all.
        """
        if max_size <= 0:
            raise ValueError(f"max_size must be positive, got {max_size}")
        self.max_size = max_size
        self.truncation_suffix = truncation_suffix
        self.tools = set(tools) if tools is not None else None

    def _truncate_to_result(
        self,
        text: str,
        meta: dict[str, Any] | None = None,
    ) -> ToolResult:
        """Truncate text to fit within max_size and wrap in ToolResult."""
        suffix_bytes = len(self.truncation_suffix.encode("utf-8"))
        # Account for JSON wrapper overhead: {"content":[{"type":"text","text":"..."}]}
        overhead = 50
        target_size = self.max_size - suffix_bytes - overhead

        if target_size <= 0:
            # Edge case: max_size too small for even the suffix
            truncated = self.truncation_suffix
        else:
            # Truncate to target size, preserving UTF-8 boundaries
            encoded = text.encode("utf-8")
            if len(encoded) <= target_size:
                truncated = text + self.truncation_suffix
            else:
                truncated = (
                    encoded[:target_size].decode("utf-8", errors="ignore")
                    + self.truncation_suffix
                )

        # Preserve original meta, falling back to {} when absent. Having
        # meta set ensures to_mcp_result() returns a CallToolResult, which
        # bypasses MCP SDK outputSchema validation — a truncated response
        # is no longer valid structured output.
        return ToolResult(
            content=[TextContent(type="text", text=truncated)],
            meta=meta if meta is not None else {},
        )

    async def on_call_tool(
        self,
        context: MiddlewareContext[mt.CallToolRequestParams],
        call_next: CallNext[mt.CallToolRequestParams, ToolResult],
    ) -> ToolResult:
        """Intercept tool calls and limit response size."""
        result = await call_next(context)

        # Check if we should limit this tool
        if self.tools is not None and context.message.name not in self.tools:
            return result

        # Measure serialized size
        serialized = pydantic_core.to_json(result, fallback=str)
        if len(serialized) <= self.max_size:
            return result

        # Over limit: extract text, truncate, return single TextContent
        logger.warning(
            "Tool %r response exceeds size limit: %d bytes > %d bytes, truncating",
            context.message.name,
            len(serialized),
            self.max_size,
        )

        texts = [b.text for b in result.content if isinstance(b, TextContent)]
        text = (
            "\n\n".join(texts)
            if texts
            else serialized.decode("utf-8", errors="replace")
        )

        return self._truncate_to_result(text, meta=result.meta)
