"""Helpers for integrating MCP (Model Context Protocol) SDK types with the Anthropic SDK.

These helpers reduce boilerplate when converting between MCP types and Anthropic API types.

Usage::

    from anthropic.lib.tools.mcp import mcp_tool, async_mcp_tool, mcp_message

This module requires the ``mcp`` package to be installed.
"""
# pyright: reportUnknownArgumentType=false, reportUnknownMemberType=false, reportUnknownVariableType=false, reportMissingImports=false, reportUnknownParameterType=false

from __future__ import annotations

import json
import base64
from typing import Any, Iterable
from urllib.parse import urlparse
from typing_extensions import Literal

try:
    from mcp.types import (  # type: ignore[import-not-found]
        Tool,
        TextContent,
        ContentBlock,
        ImageContent,
        PromptMessage,
        CallToolResult,
        EmbeddedResource,
        ReadResourceResult,
        BlobResourceContents,
        TextResourceContents,
    )
    from mcp.client.session import ClientSession  # type: ignore[import-not-found]
except ImportError as _err:
    raise ImportError(
        "The `mcp` package is required to use MCP helpers. Install it with: pip install anthropic[mcp]. Requires Python 3.10 or higher."
    ) from _err

from ...types.beta import (
    BetaBase64PDFSourceParam,
    BetaPlainTextSourceParam,
    BetaBase64ImageSourceParam,
    BetaCacheControlEphemeralParam,
)
from ._beta_functions import (
    ToolError,
    BetaFunctionTool,
    BetaAsyncFunctionTool,
    BetaFunctionToolResultType,
    beta_tool,
    beta_async_tool,
)
from .._stainless_helpers import tag_helper
from ...types.beta.beta_tool_result_block_param import Content as BetaContent

__all__ = [
    "mcp_tool",
    "async_mcp_tool",
    "mcp_content",
    "mcp_message",
    "mcp_resource_to_content",
    "mcp_resource_to_file",
    "UnsupportedMCPValueError",
]

# -----------------------------------------------------------------------
# mcp version compatibility
# -----------------------------------------------------------------------

# mcp<2 exposes camelCase model fields (`tool.inputSchema`); mcp>=2 exposes
# snake_case (`tool.input_schema`). Read through this helper to support both.

_MCP_V1_NAMES = {
    "input_schema": "inputSchema",
    "mime_type": "mimeType",
    "is_error": "isError",
    "structured_content": "structuredContent",
}


def _mcp_field_v1_or_v2(obj: Any, name: str) -> Any:
    try:
        return getattr(obj, name)
    except AttributeError:
        return getattr(obj, _MCP_V1_NAMES[name])


# -----------------------------------------------------------------------
# Supported MIME types
# -----------------------------------------------------------------------

_SUPPORTED_IMAGE_TYPES = frozenset({"image/jpeg", "image/png", "image/gif", "image/webp"})


class _TaggedDict(dict):  # type: ignore[type-arg]
    """A dict subclass that can carry a ``_stainless_helper`` attribute.

    Behaves identically to a regular dict for serialization and isinstance checks,
    but allows attaching tracking metadata that won't appear in JSON output.
    """


class _TaggedTuple(tuple):  # type: ignore[type-arg]
    """A tuple subclass that can carry a ``_stainless_helper`` attribute."""


def _is_supported_image_type(mime_type: str) -> bool:
    return mime_type in _SUPPORTED_IMAGE_TYPES


def _is_supported_resource_mime_type(mime_type: str | None) -> bool:
    return (
        mime_type is None
        or mime_type.startswith("text/")
        or mime_type == "application/pdf"
        or _is_supported_image_type(mime_type)
    )


# -----------------------------------------------------------------------
# Errors
# -----------------------------------------------------------------------


class UnsupportedMCPValueError(Exception):
    """Raised when an MCP value cannot be converted to a format supported by the Claude API."""


# -----------------------------------------------------------------------
# Content conversion
# -----------------------------------------------------------------------


def mcp_content(
    content: ContentBlock,
    *,
    cache_control: BetaCacheControlEphemeralParam | None = None,
) -> BetaContent:
    """Convert a single MCP content block to an Anthropic content block.

    Handles text, image, and embedded resource content types.
    Raises :class:`UnsupportedMCPValueError` for audio and resource_link types.
    """
    if isinstance(content, TextContent):
        block = _TaggedDict({"type": "text", "text": content.text})
        if cache_control is not None:
            block["cache_control"] = cache_control
        tag_helper(block, "mcp_content")
        return block  # type: ignore[return-value]

    if isinstance(content, ImageContent):
        mime_type = _mcp_field_v1_or_v2(content, "mime_type")
        if not _is_supported_image_type(mime_type):
            raise UnsupportedMCPValueError(f"Unsupported image MIME type: {mime_type}")
        image_block = _TaggedDict(
            {
                "type": "image",
                "source": BetaBase64ImageSourceParam(
                    type="base64",
                    data=content.data,
                    media_type=mime_type,  # type: ignore[typeddict-item]
                ),
            }
        )
        if cache_control is not None:
            image_block["cache_control"] = cache_control
        tag_helper(image_block, "mcp_content")
        return image_block  # type: ignore[return-value]

    if isinstance(content, EmbeddedResource):
        return _resource_contents_to_block(content.resource, cache_control=cache_control)

    # audio, resource_link, or unknown
    content_type = getattr(content, "type", type(content).__name__)
    raise UnsupportedMCPValueError(f"Unsupported MCP content type: {content_type}")


def _resource_contents_to_block(
    resource: TextResourceContents | BlobResourceContents,
    *,
    cache_control: BetaCacheControlEphemeralParam | None = None,
) -> BetaContent:
    """Convert MCP resource contents to an Anthropic content block."""
    mime_type = _mcp_field_v1_or_v2(resource, "mime_type")

    # Images
    if mime_type is not None and _is_supported_image_type(mime_type):
        if not isinstance(resource, BlobResourceContents):
            raise UnsupportedMCPValueError(f"Image resource must have blob data, not text. URI: {resource.uri}")
        image_block = _TaggedDict(
            {
                "type": "image",
                "source": BetaBase64ImageSourceParam(
                    type="base64",
                    data=resource.blob,
                    media_type=mime_type,  # type: ignore[typeddict-item]
                ),
            }
        )
        if cache_control is not None:
            image_block["cache_control"] = cache_control
        tag_helper(image_block, "mcp_resource_to_content")
        return image_block  # type: ignore[return-value]

    # PDFs
    if mime_type == "application/pdf":
        if not isinstance(resource, BlobResourceContents):
            raise UnsupportedMCPValueError(f"PDF resource must have blob data, not text. URI: {resource.uri}")
        pdf_block = _TaggedDict(
            {
                "type": "document",
                "source": BetaBase64PDFSourceParam(
                    type="base64",
                    data=resource.blob,
                    media_type="application/pdf",
                ),
            }
        )
        if cache_control is not None:
            pdf_block["cache_control"] = cache_control
        tag_helper(pdf_block, "mcp_resource_to_content")
        return pdf_block  # type: ignore[return-value]

    # Text (text/*, or no MIME type)
    if mime_type is None or mime_type.startswith("text/"):
        if isinstance(resource, TextResourceContents):
            data = resource.text
        else:
            data = base64.b64decode(resource.blob).decode("utf-8")
        text_block = _TaggedDict(
            {
                "type": "document",
                "source": BetaPlainTextSourceParam(
                    type="text",
                    data=data,
                    media_type="text/plain",
                ),
            }
        )
        if cache_control is not None:
            text_block["cache_control"] = cache_control
        tag_helper(text_block, "mcp_resource_to_content")
        return text_block  # type: ignore[return-value]

    raise UnsupportedMCPValueError(f'Unsupported MIME type "{mime_type}" for resource: {resource.uri}')


# -----------------------------------------------------------------------
# Message conversion
# -----------------------------------------------------------------------


def mcp_message(
    message: PromptMessage,
    *,
    cache_control: BetaCacheControlEphemeralParam | None = None,
) -> dict[str, Any]:
    """Convert an MCP prompt message to an Anthropic ``BetaMessageParam``."""
    result = _TaggedDict(
        {
            "role": message.role,
            "content": [mcp_content(message.content, cache_control=cache_control)],
        }
    )
    tag_helper(result, "mcp_message")
    return result


# -----------------------------------------------------------------------
# Resource conversion
# -----------------------------------------------------------------------


def mcp_resource_to_content(
    result: ReadResourceResult,
    *,
    cache_control: BetaCacheControlEphemeralParam | None = None,
) -> BetaContent:
    """Convert MCP resource contents to an Anthropic content block.

    Finds the first resource with a supported MIME type from the result's
    ``contents`` list.
    """
    if not result.contents:
        raise UnsupportedMCPValueError("Resource contents array must contain at least one item")

    mime_types = [_mcp_field_v1_or_v2(c, "mime_type") for c in result.contents]
    supported = next(
        (c for c, mime_type in zip(result.contents, mime_types) if _is_supported_resource_mime_type(mime_type)),
        None,
    )
    if supported is None:
        mime_types = [m for m in mime_types if m is not None]
        raise UnsupportedMCPValueError(
            f"No supported MIME type found in resource contents. Available: {', '.join(mime_types)}"
        )

    return _resource_contents_to_block(supported, cache_control=cache_control)


def mcp_resource_to_file(
    result: ReadResourceResult,
) -> tuple[str | None, bytes, str | None]:
    """Convert MCP resource contents to a file tuple for ``files.upload()``.

    Returns a ``(filename, content_bytes, mime_type)`` tuple compatible with
    the SDK's ``FileTypes``.
    """
    if not result.contents:
        raise UnsupportedMCPValueError("Resource contents array must contain at least one item")

    resource = result.contents[0]
    uri_str = str(resource.uri)

    # Extract filename from URI
    path = urlparse(uri_str).path
    name = path.rsplit("/", 1)[-1] if path else None

    # Get bytes
    if isinstance(resource, BlobResourceContents):
        content_bytes = base64.b64decode(resource.blob)
    else:
        content_bytes = resource.text.encode("utf-8")

    file_tuple = _TaggedTuple((name, content_bytes, _mcp_field_v1_or_v2(resource, "mime_type")))
    tag_helper(file_tuple, "mcp_resource_to_file")
    return file_tuple


# -----------------------------------------------------------------------
# Tool result conversion (used by tool call handlers)
# -----------------------------------------------------------------------


def _convert_tool_result(result: CallToolResult) -> BetaFunctionToolResultType:
    """Convert MCP ``CallToolResult`` to a value suitable for returning from ``call()``."""
    if _mcp_field_v1_or_v2(result, "is_error"):
        raise ToolError([mcp_content(item) for item in result.content])

    # If content is empty but structuredContent is present, JSON-encode it
    structured_content = _mcp_field_v1_or_v2(result, "structured_content")
    if not result.content and structured_content is not None:
        return json.dumps(structured_content)

    return [mcp_content(item) for item in result.content]


# -----------------------------------------------------------------------
# Public factory functions
# -----------------------------------------------------------------------


def mcp_tool(
    tool: Tool,
    client: ClientSession,
    *,
    cache_control: BetaCacheControlEphemeralParam | None = None,
    defer_loading: bool | None = None,
    allowed_callers: list[
        Literal["direct", "code_execution_20250825", "code_execution_20260120", "code_execution_20260521"]
    ]
    | None = None,
    eager_input_streaming: bool | None = None,
    input_examples: Iterable[dict[str, object]] | None = None,
    strict: bool | None = None,
) -> BetaFunctionTool[Any]:
    """Convert an MCP tool to a sync runnable tool for ``tool_runner()``.

    Example::

        from anthropic.lib.tools.mcp import mcp_tool

        tools_result = await mcp_client.list_tools()
        runner = client.beta.messages.tool_runner(
            model="claude-sonnet-4-20250514",
            max_tokens=1024,
            tools=[mcp_tool(t, mcp_client) for t in tools_result.tools],
            messages=[{"role": "user", "content": "Use the available tools"}],
        )

    Args:
        tool: An MCP tool definition from ``client.list_tools()``.
        client: The MCP ``ClientSession`` used to call the tool.
        cache_control: Cache control configuration.
        defer_loading: If true, tool will not be included in initial system prompt.
        allowed_callers: Which callers may use this tool.
        eager_input_streaming: Enable eager input streaming for this tool.
        input_examples: Example inputs for the tool.
        strict: When true, guarantees schema validation on tool names and inputs.
    """
    import anyio.from_thread

    tool_name = tool.name

    def call_mcp(**kwargs: Any) -> BetaFunctionToolResultType:
        result = anyio.from_thread.run(client.call_tool, tool_name, kwargs)
        return _convert_tool_result(result)

    result = beta_tool(
        call_mcp,
        name=tool_name,
        description=tool.description,
        input_schema=_mcp_field_v1_or_v2(tool, "input_schema"),
        cache_control=cache_control,
        defer_loading=defer_loading,
        allowed_callers=allowed_callers,
        eager_input_streaming=eager_input_streaming,
        input_examples=input_examples,
        strict=strict,
    )
    tag_helper(result, "mcp_tool")
    return result


def async_mcp_tool(
    tool: Tool,
    client: ClientSession,
    *,
    cache_control: BetaCacheControlEphemeralParam | None = None,
    defer_loading: bool | None = None,
    allowed_callers: list[
        Literal["direct", "code_execution_20250825", "code_execution_20260120", "code_execution_20260521"]
    ]
    | None = None,
    eager_input_streaming: bool | None = None,
    input_examples: Iterable[dict[str, object]] | None = None,
    strict: bool | None = None,
) -> BetaAsyncFunctionTool[Any]:
    """Convert an MCP tool to an async runnable tool for ``tool_runner()``.

    Example::

        from anthropic.lib.tools.mcp import async_mcp_tool

        tools_result = await mcp_client.list_tools()
        runner = await client.beta.messages.tool_runner(
            model="claude-sonnet-4-20250514",
            max_tokens=1024,
            tools=[async_mcp_tool(t, mcp_client) for t in tools_result.tools],
            messages=[{"role": "user", "content": "Use the available tools"}],
        )

    Args:
        tool: An MCP tool definition from ``client.list_tools()``.
        client: The MCP ``ClientSession`` used to call the tool.
        cache_control: Cache control configuration.
        defer_loading: If true, tool will not be included in initial system prompt.
        allowed_callers: Which callers may use this tool.
        eager_input_streaming: Enable eager input streaming for this tool.
        input_examples: Example inputs for the tool.
        strict: When true, guarantees schema validation on tool names and inputs.
    """
    tool_name = tool.name

    async def call_mcp(**kwargs: Any) -> BetaFunctionToolResultType:
        result = await client.call_tool(name=tool_name, arguments=kwargs)
        return _convert_tool_result(result)

    result = beta_async_tool(
        call_mcp,
        name=tool_name,
        description=tool.description,
        input_schema=_mcp_field_v1_or_v2(tool, "input_schema"),
        cache_control=cache_control,
        defer_loading=defer_loading,
        allowed_callers=allowed_callers,
        eager_input_streaming=eager_input_streaming,
        input_examples=input_examples,
        strict=strict,
    )
    tag_helper(result, "mcp_tool")
    return result
