"""Anthropic sampling handler for FastMCP."""

from collections.abc import Iterator, Sequence
from typing import Any

from mcp.types import (
    AudioContent,
    CreateMessageResult,
    CreateMessageResultWithTools,
    ImageContent,
    ModelPreferences,
    SamplingMessage,
    SamplingMessageContentBlock,
    StopReason,
    TextContent,
    Tool,
    ToolChoice,
    ToolResultContent,
    ToolUseContent,
)
from mcp.types import CreateMessageRequestParams as SamplingParams

try:
    from anthropic import AsyncAnthropic
    from anthropic.types import (
        Base64ImageSourceParam,
        ImageBlockParam,
        Message,
        MessageParam,
        TextBlock,
        TextBlockParam,
        ToolParam,
        ToolResultBlockParam,
        ToolUseBlock,
        ToolUseBlockParam,
    )
    from anthropic.types.model_param import ModelParam
    from anthropic.types.tool_choice_any_param import ToolChoiceAnyParam
    from anthropic.types.tool_choice_auto_param import ToolChoiceAutoParam
    from anthropic.types.tool_choice_param import ToolChoiceParam
except ImportError as e:
    raise ImportError(
        "The `anthropic` package is not installed. "
        "Install it with `pip install fastmcp-slim[anthropic]` or add `anthropic` to your dependencies."
    ) from e

__all__ = ["AnthropicSamplingHandler"]

# Anthropic supports these image MIME types
_ANTHROPIC_IMAGE_MEDIA_TYPES = frozenset(
    {"image/jpeg", "image/png", "image/gif", "image/webp"}
)


def _image_content_to_anthropic_block(content: ImageContent) -> ImageBlockParam:
    """Convert MCP ImageContent to Anthropic ImageBlockParam."""
    if content.mimeType not in _ANTHROPIC_IMAGE_MEDIA_TYPES:
        raise ValueError(
            f"Unsupported image MIME type for Anthropic: {content.mimeType!r}. "
            f"Supported types: {', '.join(sorted(_ANTHROPIC_IMAGE_MEDIA_TYPES))}"
        )
    return ImageBlockParam(
        type="image",
        source=Base64ImageSourceParam(
            type="base64",
            media_type=content.mimeType,  # type: ignore[arg-type]  # ty:ignore[invalid-argument-type]
            data=content.data,
        ),
    )


class AnthropicSamplingHandler:
    """Sampling handler that uses the Anthropic API.

    Example:
        ```python
        from anthropic import AsyncAnthropic
        from fastmcp import FastMCP
        from fastmcp.client.sampling.handlers.anthropic import AnthropicSamplingHandler

        handler = AnthropicSamplingHandler(
            default_model="claude-sonnet-4-5",
            client=AsyncAnthropic(),
        )

        server = FastMCP(sampling_handler=handler)
        ```
    """

    def __init__(
        self, default_model: ModelParam, client: AsyncAnthropic | None = None
    ) -> None:
        self.client: AsyncAnthropic = client or AsyncAnthropic()
        self.default_model: ModelParam = default_model

    async def __call__(
        self,
        messages: list[SamplingMessage],
        params: SamplingParams,
        context: Any,
    ) -> CreateMessageResult | CreateMessageResultWithTools:
        anthropic_messages: list[MessageParam] = self._convert_to_anthropic_messages(
            messages=messages,
        )

        model: ModelParam = self._select_model_from_preferences(params.modelPreferences)

        # Convert MCP tools to Anthropic format
        anthropic_tools: list[ToolParam] | None = None
        if params.tools:
            anthropic_tools = self._convert_tools_to_anthropic(params.tools)

        # Convert tool_choice to Anthropic format
        # Returns None if mode is "none", signaling tools should be omitted
        anthropic_tool_choice: ToolChoiceParam | None = None
        if params.toolChoice:
            converted = self._convert_tool_choice_to_anthropic(params.toolChoice)
            if converted is None:
                # tool_choice="none" means don't use tools
                anthropic_tools = None
            else:
                anthropic_tool_choice = converted

        # Build kwargs to avoid sentinel type compatibility issues across
        # anthropic SDK versions (NotGiven vs Omit)
        kwargs: dict[str, Any] = {
            "model": model,
            "messages": anthropic_messages,
            "max_tokens": params.maxTokens,
        }
        if params.systemPrompt is not None:
            kwargs["system"] = params.systemPrompt
        if params.temperature is not None:
            kwargs["temperature"] = params.temperature
        if params.stopSequences is not None:
            kwargs["stop_sequences"] = params.stopSequences
        if anthropic_tools is not None:
            kwargs["tools"] = anthropic_tools
        if anthropic_tool_choice is not None:
            kwargs["tool_choice"] = anthropic_tool_choice

        response = await self.client.messages.create(**kwargs)

        # Return appropriate result type based on whether tools were provided
        if params.tools:
            return self._message_to_result_with_tools(response)
        return self._message_to_create_message_result(response)

    @staticmethod
    def _iter_models_from_preferences(
        model_preferences: ModelPreferences | str | list[str] | None,
    ) -> Iterator[str]:
        if model_preferences is None:
            return

        if isinstance(model_preferences, str):
            yield model_preferences

        elif isinstance(model_preferences, list):
            yield from model_preferences

        elif isinstance(model_preferences, ModelPreferences):
            if not (hints := model_preferences.hints):
                return

            for hint in hints:
                if not (name := hint.name):
                    continue

                yield name

    @staticmethod
    def _convert_to_anthropic_messages(
        messages: Sequence[SamplingMessage],
    ) -> list[MessageParam]:
        anthropic_messages: list[MessageParam] = []

        for message in messages:
            content = message.content

            # Handle list content (from CreateMessageResultWithTools)
            if isinstance(content, list):
                content_blocks: list[
                    TextBlockParam
                    | ImageBlockParam
                    | ToolUseBlockParam
                    | ToolResultBlockParam
                ] = []

                for item in content:
                    if isinstance(item, ToolUseContent):
                        content_blocks.append(
                            ToolUseBlockParam(
                                type="tool_use",
                                id=item.id,
                                name=item.name,
                                input=item.input,
                            )
                        )
                    elif isinstance(item, TextContent):
                        content_blocks.append(
                            TextBlockParam(type="text", text=item.text)
                        )
                    elif isinstance(item, ImageContent):
                        if message.role != "user":
                            raise ValueError(
                                "ImageContent is only supported in user messages "
                                "for Anthropic"
                            )
                        content_blocks.append(_image_content_to_anthropic_block(item))
                    elif isinstance(item, AudioContent):
                        raise ValueError(
                            "AudioContent is not supported by the Anthropic API"
                        )
                    elif isinstance(item, ToolResultContent):
                        # Extract text content from the result
                        result_content: str | list[TextBlockParam] = ""
                        if item.content:
                            text_blocks: list[TextBlockParam] = [
                                TextBlockParam(type="text", text=sub_item.text)
                                for sub_item in item.content
                                if isinstance(sub_item, TextContent)
                            ]
                            if len(text_blocks) == 1:
                                result_content = text_blocks[0]["text"]
                            elif text_blocks:
                                result_content = text_blocks

                        content_blocks.append(
                            ToolResultBlockParam(
                                type="tool_result",
                                tool_use_id=item.toolUseId,
                                content=result_content,
                                is_error=item.isError if item.isError else False,
                            )
                        )
                    else:
                        raise ValueError(
                            f"Unsupported content type for Anthropic: {type(item).__name__}"
                        )

                if content_blocks:
                    anthropic_messages.append(
                        MessageParam(
                            role=message.role,
                            content=content_blocks,
                        )
                    )
                continue

            # Handle ToolUseContent (assistant's tool calls)
            if isinstance(content, ToolUseContent):
                anthropic_messages.append(
                    MessageParam(
                        role="assistant",
                        content=[
                            ToolUseBlockParam(
                                type="tool_use",
                                id=content.id,
                                name=content.name,
                                input=content.input,
                            )
                        ],
                    )
                )
                continue

            # Handle ToolResultContent (user's tool results)
            if isinstance(content, ToolResultContent):
                result_content_str: str | list[TextBlockParam] = ""
                if content.content:
                    text_parts: list[TextBlockParam] = [
                        TextBlockParam(type="text", text=item.text)
                        for item in content.content
                        if isinstance(item, TextContent)
                    ]
                    if len(text_parts) == 1:
                        result_content_str = text_parts[0]["text"]
                    elif text_parts:
                        result_content_str = text_parts

                anthropic_messages.append(
                    MessageParam(
                        role="user",
                        content=[
                            ToolResultBlockParam(
                                type="tool_result",
                                tool_use_id=content.toolUseId,
                                content=result_content_str,
                                is_error=content.isError if content.isError else False,
                            )
                        ],
                    )
                )
                continue

            # Handle TextContent
            if isinstance(content, TextContent):
                anthropic_messages.append(
                    MessageParam(
                        role=message.role,
                        content=content.text,
                    )
                )
                continue

            # Handle ImageContent
            if isinstance(content, ImageContent):
                if message.role != "user":
                    raise ValueError(
                        "ImageContent is only supported in user messages for Anthropic"
                    )
                anthropic_messages.append(
                    MessageParam(
                        role="user",
                        content=[_image_content_to_anthropic_block(content)],
                    )
                )
                continue

            # Handle AudioContent - not supported by Anthropic
            if isinstance(content, AudioContent):
                raise ValueError("AudioContent is not supported by the Anthropic API")

            raise ValueError(f"Unsupported content type: {type(content)}")

        return anthropic_messages

    @staticmethod
    def _message_to_create_message_result(
        message: Message,
    ) -> CreateMessageResult:
        if len(message.content) == 0:
            raise ValueError("No content in response from Anthropic")

        # Join all text blocks to avoid dropping content
        text = "".join(
            block.text for block in message.content if isinstance(block, TextBlock)
        )
        if text:
            return CreateMessageResult(
                content=TextContent(type="text", text=text),
                role="assistant",
                model=message.model,
            )

        raise ValueError(
            f"No text content in response from Anthropic: {[type(b).__name__ for b in message.content]}"
        )

    def _select_model_from_preferences(
        self, model_preferences: ModelPreferences | str | list[str] | None
    ) -> ModelParam:
        for model_option in self._iter_models_from_preferences(model_preferences):
            # Accept any model that starts with "claude"
            if model_option.startswith("claude"):
                return model_option

        return self.default_model

    @staticmethod
    def _convert_tools_to_anthropic(tools: list[Tool]) -> list[ToolParam]:
        """Convert MCP tools to Anthropic tool format."""
        anthropic_tools: list[ToolParam] = []
        for tool in tools:
            # Build input_schema dict, ensuring required fields
            input_schema: dict[str, Any] = dict(tool.inputSchema)
            if "type" not in input_schema:
                input_schema["type"] = "object"

            anthropic_tools.append(
                ToolParam(
                    name=tool.name,
                    description=tool.description or "",
                    input_schema=input_schema,
                )
            )
        return anthropic_tools

    @staticmethod
    def _convert_tool_choice_to_anthropic(
        tool_choice: ToolChoice,
    ) -> ToolChoiceParam | None:
        """Convert MCP tool_choice to Anthropic format.

        Returns None for "none" mode, signaling that tools should be omitted
        from the request entirely (Anthropic doesn't have an explicit "none" option).
        """
        if tool_choice.mode == "auto":
            return ToolChoiceAutoParam(type="auto")
        elif tool_choice.mode == "required":
            return ToolChoiceAnyParam(type="any")
        elif tool_choice.mode == "none":
            # Anthropic doesn't have a "none" option - return None to signal
            # that tools should be omitted from the request entirely
            return None
        else:
            raise ValueError(f"Unsupported tool_choice mode: {tool_choice.mode!r}")

    @staticmethod
    def _message_to_result_with_tools(
        message: Message,
    ) -> CreateMessageResultWithTools:
        """Convert Anthropic response to CreateMessageResultWithTools."""
        if len(message.content) == 0:
            raise ValueError("No content in response from Anthropic")

        # Determine stop reason
        stop_reason: StopReason
        if message.stop_reason == "tool_use":
            stop_reason = "toolUse"
        elif message.stop_reason == "end_turn":
            stop_reason = "endTurn"
        elif message.stop_reason == "max_tokens":
            stop_reason = "maxTokens"
        elif message.stop_reason == "stop_sequence":
            stop_reason = "endTurn"
        else:
            stop_reason = "endTurn"

        # Build content list
        content: list[SamplingMessageContentBlock] = []

        for block in message.content:
            if isinstance(block, TextBlock):
                content.append(TextContent(type="text", text=block.text))
            elif isinstance(block, ToolUseBlock):
                # Anthropic returns input as dict directly
                arguments = block.input if isinstance(block.input, dict) else {}

                content.append(
                    ToolUseContent(
                        type="tool_use",
                        id=block.id,
                        name=block.name,
                        input=arguments,
                    )
                )

        # Must have at least some content
        if not content:
            raise ValueError("No content in response from Anthropic")

        return CreateMessageResultWithTools(
            content=content,
            role="assistant",
            model=message.model,
            stopReason=stop_reason,
        )
