from __future__ import annotations

from typing import Any

from mcp_types import INVALID_PARAMS, CallToolResult
from opentelemetry.trace import SpanKind, StatusCode
from pydantic import ValidationError

from mcp.server.context import CallNext, HandlerResult, ServerMiddleware, ServerRequestContext
from mcp.shared._otel import extract_trace_context, otel_span
from mcp.shared.exceptions import MCPError


class OpenTelemetryMiddleware(ServerMiddleware[Any]):
    """Context-tier middleware that wraps each inbound message in an OpenTelemetry span."""

    async def __call__(self, ctx: ServerRequestContext[Any, Any], call_next: CallNext) -> HandlerResult:
        name = ctx.params.get("name") if ctx.params else None
        target = name if isinstance(name, str) else None

        attributes: dict[str, Any] = {
            "mcp.method.name": ctx.method,
            "mcp.protocol.version": ctx.protocol_version,
        }
        if ctx.request_id is not None:
            attributes["jsonrpc.request.id"] = str(ctx.request_id)

        if ctx.method == "tools/call":
            attributes["gen_ai.operation.name"] = "execute_tool"
            if target is not None:
                attributes["gen_ai.tool.name"] = target
        elif ctx.method == "prompts/get" and target is not None:
            attributes["gen_ai.prompt.name"] = target

        with otel_span(
            name=f"{ctx.method}{f' {target}' if target else ''}",
            kind=SpanKind.SERVER,
            attributes=attributes,
            context=extract_trace_context(ctx.meta),
            record_exception=False,
            set_status_on_exception=False,
        ) as span:
            try:
                result = await call_next(ctx)
            except MCPError as e:
                code = str(e.error.code)
                span.set_attributes({"error.type": code, "rpc.response.status_code": code})
                span.set_status(StatusCode.ERROR, e.error.message)
                raise
            except ValidationError:
                # Mirror the sanitized wire response; pydantic messages carry client input.
                code = str(INVALID_PARAMS)
                span.set_attributes({"error.type": code, "rpc.response.status_code": code})
                span.set_status(StatusCode.ERROR, "Invalid request parameters")
                raise
            except Exception as e:
                span.set_attribute("error.type", type(e).__qualname__)
                span.record_exception(e)
                span.set_status(StatusCode.ERROR, str(e))
                raise
            if ctx.method == "tools/call":
                # Tool errors are detected pre-serialization, so only shapes that reach the wire as an error
                # count: the model, or the camelCase alias (`is_error` is dropped by the alias-only wire
                # validation). A raw-dict `isError` is matched as a literal bool only - non-bool coercible
                # values (1, "true") would serialize to an error but are rare enough to leave undetected.
                match result:
                    case CallToolResult(is_error=True) | {"isError": True}:
                        span.set_attribute("error.type", "tool_error")
                        span.set_status(StatusCode.ERROR)
                    case _:
                        pass
            return result
