from __future__ import annotations

import warnings
from collections.abc import Callable
from typing import (
    TYPE_CHECKING,
    Annotated,
    Any,
    ClassVar,
    TypeAlias,
    overload,
)

import mcp.types
import pydantic_core
from mcp.shared.tool_name_validation import validate_and_warn_tool_name
from mcp.types import (
    CallToolResult,
    ContentBlock,
    Icon,
    TextContent,
    ToolAnnotations,
    ToolExecution,
)
from mcp.types import Tool as MCPTool
from pydantic import BaseModel, Field, model_validator
from pydantic.json_schema import SkipJsonSchema

from fastmcp.exceptions import FastMCPDeprecationWarning
from fastmcp.utilities.authorization import AuthCheck
from fastmcp.utilities.components import FastMCPComponent
from fastmcp.utilities.logging import get_logger
from fastmcp.utilities.tasks import TaskConfig, TaskMeta
from fastmcp.utilities.types import (
    Audio,
    File,
    Image,
    NotSet,
    NotSetT,
)

try:
    from prefab_ui.app import PrefabApp as _PrefabApp
    from prefab_ui.components.base import Component as _PrefabComponent

    _HAS_PREFAB = True
except ImportError:
    _HAS_PREFAB = False

if TYPE_CHECKING:
    from docket import Docket
    from docket.execution import Execution

    from fastmcp.tools.function_tool import FunctionTool
    from fastmcp.tools.tool_transform import ArgTransform, TransformedTool

# Re-export from function_tool module

logger = get_logger(__name__)


ToolResultSerializerType: TypeAlias = Callable[[Any], str]


def resolve_serialize_by_alias(value: Any) -> bool:
    """Resolve the effective ``by_alias`` setting for serializing *value*.

    Pydantic's low-level serialization helpers (``to_json``,
    ``to_jsonable_python``) default ``by_alias`` to ``True``, which silently
    ignores a model's ``serialize_by_alias`` config. When *value* is a Pydantic
    model we consult that config instead, falling back to ``True`` to preserve
    FastMCP's longstanding default of emitting aliases when no preference is
    declared.
    """
    if isinstance(value, type):
        model = value if issubclass(value, BaseModel) else None
    elif isinstance(value, BaseModel):
        model = type(value)
    else:
        model = None

    if model is None:
        return True

    configured = model.model_config.get("serialize_by_alias")
    return True if configured is None else configured


def default_serializer(data: Any) -> str:
    return pydantic_core.to_json(
        data, fallback=str, by_alias=resolve_serialize_by_alias(data)
    ).decode()


class ToolResult(BaseModel):
    content: list[ContentBlock] = Field(
        description="List of content blocks for the tool result"
    )
    structured_content: dict[str, Any] | None = Field(
        default=None, description="Structured content matching the tool's output schema"
    )
    meta: dict[str, Any] | None = Field(
        default=None, description="Runtime metadata about the tool execution"
    )
    is_error: bool = Field(
        default=False,
        description="Whether this result represents a tool execution error. "
        "When True, it maps to CallToolResult.isError so the error is returned "
        "to the client rather than raised.",
    )

    def __init__(
        self,
        content: list[ContentBlock] | Any | None = None,
        structured_content: dict[str, Any] | Any | None = None,
        meta: dict[str, Any] | None = None,
        is_error: bool = False,
    ):
        if content is None and structured_content is None:
            raise ValueError("Either content or structured_content must be provided")
        elif content is None:
            content = structured_content

        converted_content: list[ContentBlock] = _convert_to_content(result=content)

        if structured_content is not None:
            # Convert Prefab types to their wire-format envelope before
            # generic serialization, so the renderer gets the right shape.
            if _HAS_PREFAB:
                if isinstance(structured_content, _PrefabApp):
                    structured_content = _prefab_to_json(structured_content)
                elif isinstance(structured_content, _PrefabComponent):
                    structured_content = _prefab_to_json(
                        _PrefabApp(view=structured_content)
                    )

            try:
                structured_content = pydantic_core.to_jsonable_python(
                    value=structured_content,
                    by_alias=resolve_serialize_by_alias(structured_content),
                )
            except pydantic_core.PydanticSerializationError as e:
                logger.error(
                    f"Could not serialize structured content. If this is unexpected, set your tool's output_schema to None to disable automatic serialization: {e}"
                )
                raise
            if not isinstance(structured_content, dict):
                raise ValueError(
                    "structured_content must be a dict or None. "
                    f"Got {type(structured_content).__name__}: {structured_content!r}. "
                    "Tools should wrap non-dict values based on their output_schema."
                )

        super().__init__(
            content=converted_content,
            structured_content=structured_content,
            meta=meta,
            is_error=is_error,
        )

    def to_mcp_result(
        self,
    ) -> (
        list[ContentBlock] | tuple[list[ContentBlock], dict[str, Any]] | CallToolResult
    ):
        # An error result must round-trip through CallToolResult so isError
        # reaches the client; the plain content/tuple returns can't carry it.
        if self.meta is not None or self.is_error:
            return CallToolResult(
                structuredContent=self.structured_content,
                content=self.content,
                isError=self.is_error,
                _meta=self.meta,  # type: ignore[call-arg]  # _meta is Pydantic alias for meta field
            )
        if self.structured_content is None:
            return self.content
        return self.content, self.structured_content


class Tool(FastMCPComponent):
    """Internal tool registration info."""

    KEY_PREFIX: ClassVar[str] = "tool"

    parameters: Annotated[
        dict[str, Any], Field(description="JSON schema for tool parameters")
    ]
    output_schema: Annotated[
        dict[str, Any] | None, Field(description="JSON schema for tool output")
    ] = None
    annotations: Annotated[
        ToolAnnotations | None,
        Field(description="Additional annotations about the tool"),
    ] = None
    execution: Annotated[
        ToolExecution | None,
        Field(description="Task execution configuration (SEP-1686)"),
    ] = None
    serializer: Annotated[
        SkipJsonSchema[ToolResultSerializerType | None],
        Field(
            description="Deprecated. Return ToolResult from your tools for full control over serialization."
        ),
    ] = None
    auth: Annotated[
        SkipJsonSchema[AuthCheck | list[AuthCheck] | None],
        Field(description="Authorization checks for this tool", exclude=True),
    ] = None
    timeout: Annotated[
        float | None,
        Field(
            description="Execution timeout in seconds. If None, no timeout is applied."
        ),
    ] = None

    @model_validator(mode="after")
    def _validate_tool_name(self) -> Tool:
        """Validate tool name according to MCP specification (SEP-986)."""
        validate_and_warn_tool_name(self.name)
        return self

    def to_mcp_tool(
        self,
        **overrides: Any,
    ) -> MCPTool:
        """Convert the FastMCP tool to an MCP tool."""
        title = None

        if self.title:
            title = self.title
        elif self.annotations and self.annotations.title:
            title = self.annotations.title

        mcp_tool = MCPTool(
            name=overrides.get("name", self.name),
            title=overrides.get("title", title),
            description=overrides.get("description", self.description),
            inputSchema=overrides.get("inputSchema", self.parameters),
            outputSchema=overrides.get("outputSchema", self.output_schema),
            icons=overrides.get("icons", self.icons),
            annotations=overrides.get("annotations", self.annotations),
            execution=overrides.get("execution", self.execution),
            _meta=overrides.get(  # type: ignore[call-arg]  # _meta is Pydantic alias for meta field
                "_meta", self.get_meta()
            ),
        )

        if (
            self.task_config.supports_tasks()
            and "execution" not in overrides
            and not self.execution
        ):
            mcp_tool.execution = ToolExecution(taskSupport=self.task_config.mode)

        return mcp_tool

    @classmethod
    def from_function(
        cls,
        fn: Callable[..., Any],
        *,
        name: str | None = None,
        version: str | int | None = None,
        title: str | None = None,
        description: str | None = None,
        icons: list[Icon] | None = None,
        tags: set[str] | None = None,
        annotations: ToolAnnotations | None = None,
        exclude_args: list[str] | None = None,
        output_schema: dict[str, Any] | NotSetT | None = NotSet,
        serializer: ToolResultSerializerType | None = None,  # Deprecated
        meta: dict[str, Any] | None = None,
        task: bool | TaskConfig | None = None,
        timeout: float | None = None,
        auth: AuthCheck | list[AuthCheck] | None = None,
        run_in_thread: bool | None = None,
    ) -> FunctionTool:
        """Create a Tool from a function."""
        from fastmcp.tools.function_tool import FunctionTool

        return FunctionTool.from_function(
            fn=fn,
            name=name,
            version=version,
            title=title,
            description=description,
            icons=icons,
            tags=tags,
            annotations=annotations,
            exclude_args=exclude_args,
            output_schema=output_schema,
            serializer=serializer,
            meta=meta,
            task=task,
            timeout=timeout,
            auth=auth,
            run_in_thread=run_in_thread,
        )

    async def run(self, arguments: dict[str, Any]) -> ToolResult:
        """
        Run the tool with arguments.

        This method is not implemented in the base Tool class and must be
        implemented by subclasses.

        `run()` can EITHER return a list of ContentBlocks, or a tuple of
        (list of ContentBlocks, dict of structured output).
        """
        raise NotImplementedError("Subclasses must implement run()")

    def convert_result(self, raw_value: Any) -> ToolResult:
        """Convert a raw result to ToolResult.

        Handles ToolResult passthrough and converts raw values using the tool's
        attributes (serializer, output_schema) for proper conversion.
        """
        if isinstance(raw_value, ToolResult):
            return raw_value

        if _HAS_PREFAB:
            if isinstance(raw_value, _PrefabApp):
                return _prefab_to_tool_result(
                    raw_value,
                    fastmcp_app_name=_get_fastmcp_app_name(self),
                )
            if isinstance(raw_value, _PrefabComponent):
                return _prefab_to_tool_result(
                    _PrefabApp(view=raw_value),
                    fastmcp_app_name=_get_fastmcp_app_name(self),
                )

        content = _convert_to_content(raw_value, serializer=self.serializer)

        # Bytes can't be represented as structured JSON content
        if isinstance(raw_value, bytes):
            return ToolResult(content=content)

        # Skip structured content for ContentBlock types only if no output_schema
        # (if output_schema exists, MCP SDK requires structured_content)
        if self.output_schema is None and (
            isinstance(raw_value, ContentBlock | Audio | Image | File)
            or (
                isinstance(raw_value, list | tuple)
                and any(isinstance(item, ContentBlock) for item in raw_value)
            )
        ):
            return ToolResult(content=content)

        try:
            structured = pydantic_core.to_jsonable_python(
                raw_value, by_alias=resolve_serialize_by_alias(raw_value)
            )
        except (pydantic_core.PydanticSerializationError, UnicodeDecodeError):
            return ToolResult(content=content)

        if self.output_schema is None:
            # No schema - only use structured_content for dicts
            if isinstance(structured, dict):
                return ToolResult(content=content, structured_content=structured)
            return ToolResult(content=content)

        # Has output_schema - wrap if x-fastmcp-wrap-result is set
        wrap_result = self.output_schema.get("x-fastmcp-wrap-result")
        return ToolResult(
            content=content,
            structured_content={"result": structured} if wrap_result else structured,
            meta={"fastmcp": {"wrap_result": True}} if wrap_result else None,
        )

    @overload
    async def _run(
        self,
        arguments: dict[str, Any],
        task_meta: None = None,
    ) -> ToolResult: ...

    @overload
    async def _run(
        self,
        arguments: dict[str, Any],
        task_meta: TaskMeta,
    ) -> mcp.types.CreateTaskResult: ...

    async def _run(
        self,
        arguments: dict[str, Any],
        task_meta: TaskMeta | None = None,
    ) -> ToolResult | mcp.types.CreateTaskResult:
        """Server entry point that handles task routing.

        This allows ANY Tool subclass to support background execution by setting
        task_config.mode to "supported" or "required". The server calls this
        method instead of run() directly.

        Args:
            arguments: Tool arguments
            task_meta: If provided, execute as background task and return
                CreateTaskResult. If None (default), execute synchronously and
                return ToolResult.

        Returns:
            ToolResult when task_meta is None.
            CreateTaskResult when task_meta is provided.

        Subclasses can override this to customize task routing behavior.
        For example, FastMCPProviderTool overrides to delegate to child
        middleware without submitting to Docket.
        """
        from fastmcp.server.tasks.routing import check_background_task

        task_result = await check_background_task(
            component=self,
            task_type="tool",
            arguments=arguments,
            task_meta=task_meta,
        )
        if task_result:
            return task_result

        return await self.run(arguments)

    def register_with_docket(self, docket: Docket) -> None:
        """Register this tool with docket for background execution."""
        if not self.task_config.supports_tasks():
            return
        docket.register(self.run, names=[self.key])

    async def add_to_docket(  # type: ignore[override]
        self,
        docket: Docket,
        arguments: dict[str, Any],
        *,
        fn_key: str | None = None,
        task_key: str | None = None,
        **kwargs: Any,
    ) -> Execution:
        """Schedule this tool for background execution via docket.

        Args:
            docket: The Docket instance
            arguments: Tool arguments
            fn_key: Function lookup key in Docket registry (defaults to self.key)
            task_key: Redis storage key for the result
            **kwargs: Additional kwargs passed to docket.add()
        """
        lookup_key = fn_key or self.key
        if task_key:
            kwargs["key"] = task_key
        return await docket.add(lookup_key, **kwargs)(arguments)

    @classmethod
    def from_tool(
        cls,
        tool: Tool | Callable[..., Any],
        *,
        name: str | None = None,
        title: str | NotSetT | None = NotSet,
        description: str | NotSetT | None = NotSet,
        tags: set[str] | None = None,
        annotations: ToolAnnotations | NotSetT | None = NotSet,
        output_schema: dict[str, Any] | NotSetT | None = NotSet,
        serializer: ToolResultSerializerType | None = None,  # Deprecated
        meta: dict[str, Any] | NotSetT | None = NotSet,
        transform_args: dict[str, ArgTransform] | None = None,
        transform_fn: Callable[..., Any] | None = None,
    ) -> TransformedTool:
        from fastmcp.tools.tool_transform import TransformedTool

        tool = cls._ensure_tool(tool)

        return TransformedTool.from_tool(
            tool=tool,
            transform_fn=transform_fn,
            name=name,
            title=title,
            transform_args=transform_args,
            description=description,
            tags=tags,
            annotations=annotations,
            output_schema=output_schema,
            serializer=serializer,
            meta=meta,
        )

    @classmethod
    def _ensure_tool(cls, tool: Tool | Callable[..., Any]) -> Tool:
        """Coerce a callable into a Tool, respecting @tool decorator metadata."""
        if isinstance(tool, Tool):
            return tool

        from fastmcp.decorators import get_fastmcp_meta
        from fastmcp.tools.function_tool import FunctionTool, ToolMeta

        fmeta = get_fastmcp_meta(tool)
        if isinstance(fmeta, ToolMeta):
            return FunctionTool.from_function(tool, metadata=fmeta)

        return cls.from_function(tool)

    def get_span_attributes(self) -> dict[str, Any]:
        return super().get_span_attributes() | {
            "fastmcp.component.type": "tool",
            "fastmcp.provider.type": "LocalProvider",
        }


def _serialize_with_fallback(
    result: Any, serializer: ToolResultSerializerType | None = None
) -> str:
    if serializer is not None:
        try:
            return serializer(result)
        except Exception as e:
            logger.warning(
                "Error serializing tool result: %s",
                e,
                exc_info=True,
            )

    return default_serializer(result)


def _convert_to_single_content_block(
    item: Any,
    serializer: ToolResultSerializerType | None = None,
) -> ContentBlock:
    if isinstance(item, ContentBlock):
        return item

    if isinstance(item, Image):
        return item.to_image_content()

    if isinstance(item, Audio):
        return item.to_audio_content()

    if isinstance(item, File):
        return item.to_resource_content()

    if isinstance(item, str):
        return TextContent(type="text", text=item)

    if isinstance(item, bytes):
        try:
            return TextContent(type="text", text=item.decode("utf-8"))
        except UnicodeDecodeError:
            import base64

            return TextContent(type="text", text=base64.b64encode(item).decode("ascii"))

    return TextContent(type="text", text=_serialize_with_fallback(item, serializer))


_PREFAB_TEXT_FALLBACK = "[Rendered Prefab UI]"


def _get_tool_resolver(app_name: str | None = None) -> Callable[..., str] | None:
    """Get the Prefab peer-reference resolver bound to an app name."""
    try:
        from fastmcp.apps.app import _make_resolver

        return _make_resolver(app_name)
    except ImportError:
        return None


def _prefab_to_json(app: Any, fastmcp_app_name: str | None = None) -> dict[str, Any]:
    """Call PrefabApp.to_json() with the hash-based resolver.

    The resolver prefixes peer-tool references with a deterministic hash
    derived from the app name + tool name. The dispatcher recognizes that
    format and routes calls via ``get_tool_by_hash`` which walks the
    provider tree recursively — same pattern as the old ``get_app_tool``.
    """
    data = app.to_json(tool_resolver=_get_tool_resolver(fastmcp_app_name))
    return data


def _get_fastmcp_app_name(tool: Tool) -> str | None:
    """Read the FastMCPApp name from a tool's metadata, if present."""
    meta = tool.meta
    if not meta:
        return None
    fastmcp_meta = meta.get("fastmcp")
    if isinstance(fastmcp_meta, dict):
        app = fastmcp_meta.get("app")
        if isinstance(app, str):
            return app
    return None


def _prefab_to_tool_result(app: Any, fastmcp_app_name: str | None = None) -> ToolResult:
    """Convert a PrefabApp to a FastMCP ToolResult."""
    return ToolResult(
        content=[TextContent(type="text", text=_PREFAB_TEXT_FALLBACK)],
        structured_content=_prefab_to_json(app, fastmcp_app_name=fastmcp_app_name),
    )


def _convert_to_content(
    result: Any,
    serializer: ToolResultSerializerType | None = None,
) -> list[ContentBlock]:
    """Convert a result to a sequence of content objects."""

    if result is None:
        return []

    if not isinstance(result, (list | tuple)):
        return [_convert_to_single_content_block(result, serializer)]

    # If all items are ContentBlocks, return them as is
    if all(isinstance(item, ContentBlock) for item in result):
        return result

    # If any item is a ContentBlock, convert non-ContentBlock items to TextContent
    # without aggregating them
    if any(isinstance(item, ContentBlock | Image | Audio | File) for item in result):
        return [
            _convert_to_single_content_block(item, serializer)
            if not isinstance(item, ContentBlock)
            else item
            for item in result
        ]
    # If none of the items are ContentBlocks, aggregate all items into a single TextContent
    return [TextContent(type="text", text=_serialize_with_fallback(result, serializer))]


__all__ = ["Tool", "ToolResult"]


def __getattr__(name: str) -> Any:
    """Deprecated re-exports for backwards compatibility."""
    deprecated_exports = {
        "FunctionTool": "FunctionTool",
        "ParsedFunction": "ParsedFunction",
        "tool": "tool",
    }

    if name in deprecated_exports:
        import fastmcp

        if fastmcp.settings.deprecation_warnings:
            warnings.warn(
                f"Importing {name} from fastmcp.tools.tool is deprecated. "
                f"Import from fastmcp.tools.function_tool instead.",
                FastMCPDeprecationWarning,
                stacklevel=2,
            )
        from fastmcp.tools import function_tool

        return getattr(function_tool, name)

    raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
