"""Base classes and interfaces for FastMCP resources."""

from __future__ import annotations

import base64
import json
from collections.abc import Callable
from typing import TYPE_CHECKING, Annotated, Any, ClassVar, overload

import mcp.types

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

    from fastmcp.resources.function_resource import FunctionResource

import pydantic
import pydantic_core
from mcp.types import Annotations, Icon
from mcp.types import Resource as SDKResource
from pydantic import (
    AnyUrl,
    ConfigDict,
    Field,
    UrlConstraints,
    field_validator,
    model_validator,
)
from pydantic.json_schema import SkipJsonSchema
from typing_extensions import Self

from fastmcp.exceptions import FastMCPDeprecationWarning
from fastmcp.server.auth.authorization import AuthCheck
from fastmcp.server.tasks.config import TaskConfig, TaskMeta
from fastmcp.utilities.components import FastMCPComponent


class ResourceContent(pydantic.BaseModel):
    """Wrapper for resource content with optional MIME type and metadata.

    Accepts any value for content - strings and bytes pass through directly,
    other types (dict, list, BaseModel, etc.) are automatically JSON-serialized.

    Example:
        ```python
        from fastmcp.resources import ResourceContent

        # String content
        ResourceContent("plain text")

        # Binary content
        ResourceContent(b"binary data", mime_type="application/octet-stream")

        # Auto-serialized to JSON
        ResourceContent({"key": "value"})
        ResourceContent(["a", "b", "c"])
        ```
    """

    content: str | bytes
    mime_type: str | None = None
    meta: dict[str, Any] | None = None

    def __init__(
        self,
        content: Any,
        mime_type: str | None = None,
        meta: dict[str, Any] | None = None,
    ):
        """Create ResourceContent with automatic serialization.

        Args:
            content: The content value. str and bytes pass through directly.
                     Other types (dict, list, BaseModel) are JSON-serialized.
            mime_type: Optional MIME type. Defaults based on content type:
                       str → "text/plain", bytes → "application/octet-stream",
                       other → "application/json"
            meta: Optional metadata dictionary.
        """
        if isinstance(content, str):
            normalized_content: str | bytes = content
            mime_type = mime_type or "text/plain"
        elif isinstance(content, bytes):
            normalized_content = content
            mime_type = mime_type or "application/octet-stream"
        else:
            # dict, list, BaseModel, etc → JSON
            normalized_content = pydantic_core.to_json(content, fallback=str).decode()
            mime_type = mime_type or "application/json"

        super().__init__(content=normalized_content, mime_type=mime_type, meta=meta)

    def to_mcp_resource_contents(
        self, uri: AnyUrl | str
    ) -> mcp.types.TextResourceContents | mcp.types.BlobResourceContents:
        """Convert to MCP resource contents type.

        Args:
            uri: The URI of the resource (required by MCP types)

        Returns:
            TextResourceContents for str content, BlobResourceContents for bytes
        """
        if isinstance(self.content, str):
            return mcp.types.TextResourceContents(
                uri=AnyUrl(uri) if isinstance(uri, str) else uri,
                text=self.content,
                mimeType=self.mime_type or "text/plain",
                _meta=self.meta,  # type: ignore[call-arg]  # _meta is Pydantic alias for meta field  # ty:ignore[unknown-argument]
            )
        else:
            return mcp.types.BlobResourceContents(
                uri=AnyUrl(uri) if isinstance(uri, str) else uri,
                blob=base64.b64encode(self.content).decode(),
                mimeType=self.mime_type or "application/octet-stream",
                _meta=self.meta,  # type: ignore[call-arg]  # _meta is Pydantic alias for meta field  # ty:ignore[unknown-argument]
            )


class ResourceResult(pydantic.BaseModel):
    """Canonical result type for resource reads.

    Provides explicit control over resource responses: multiple content items,
    per-item MIME types, and metadata at both the item and result level.

    Accepts:
        - str: Wrapped as single ResourceContent (text/plain)
        - bytes: Wrapped as single ResourceContent (application/octet-stream)
        - list[ResourceContent]: Used directly for multiple items or custom MIME types

    Example:
        ```python
        from fastmcp import FastMCP
        from fastmcp.resources import ResourceResult, ResourceContent

        mcp = FastMCP()

        # Simple string content
        @mcp.resource("data://simple")
        def get_simple() -> ResourceResult:
            return ResourceResult("hello world")

        # Multiple items with custom MIME types
        @mcp.resource("data://items")
        def get_items() -> ResourceResult:
            return ResourceResult(
                contents=[
                    ResourceContent({"key": "value"}),  # auto-serialized to JSON
                    ResourceContent(b"binary data"),
                ],
                meta={"count": 2}
            )
        ```
    """

    contents: list[ResourceContent]
    meta: dict[str, Any] | None = None

    def __init__(
        self,
        contents: str | bytes | list[ResourceContent],
        meta: dict[str, Any] | None = None,
    ):
        """Create ResourceResult.

        Args:
            contents: String, bytes, or list of ResourceContent objects.
            meta: Optional metadata about the resource result.
        """
        normalized = self._normalize_contents(contents)
        super().__init__(contents=normalized, meta=meta)

    @staticmethod
    def _normalize_contents(
        contents: str | bytes | list[ResourceContent],
    ) -> list[ResourceContent]:
        """Normalize input to list[ResourceContent]."""
        if isinstance(contents, str):
            return [ResourceContent(contents)]
        if isinstance(contents, bytes):
            return [ResourceContent(contents)]
        if isinstance(contents, list):
            # Validate all items are ResourceContent
            for i, item in enumerate(contents):
                if not isinstance(item, ResourceContent):
                    raise TypeError(
                        f"contents[{i}] must be ResourceContent, got {type(item).__name__}. "
                        f"Use ResourceContent({item!r}) to wrap the value."
                    )
            return contents
        # Auto-serialize JSON-native types to JSON text
        if (
            isinstance(contents, dict | list | tuple | int | float | bool)
            or contents is None
        ):
            return [ResourceContent(json.dumps(contents), mime_type="application/json")]
        raise TypeError(
            f"contents must be str, bytes, or list[ResourceContent], got {type(contents).__name__}"
        )

    def to_mcp_result(self, uri: AnyUrl | str) -> mcp.types.ReadResourceResult:
        """Convert to MCP ReadResourceResult.

        Args:
            uri: The URI of the resource (required by MCP types)

        Returns:
            MCP ReadResourceResult with converted contents
        """
        mcp_contents = [item.to_mcp_resource_contents(uri) for item in self.contents]
        return mcp.types.ReadResourceResult(
            contents=mcp_contents,
            _meta=self.meta,  # type: ignore[call-arg]  # _meta is Pydantic alias for meta field  # ty:ignore[unknown-argument]
        )


class Resource(FastMCPComponent):
    """Base class for all resources."""

    KEY_PREFIX: ClassVar[str] = "resource"

    model_config = ConfigDict(validate_default=True)

    uri: Annotated[AnyUrl, UrlConstraints(host_required=False)] = Field(
        default=..., description="URI of the resource"
    )
    name: str = Field(default="", description="Name of the resource")
    mime_type: str = Field(
        default="text/plain",
        description="MIME type of the resource content",
    )
    annotations: Annotated[
        Annotations | None,
        Field(description="Optional annotations about the resource's behavior"),
    ] = None
    auth: Annotated[
        SkipJsonSchema[AuthCheck | list[AuthCheck] | None],
        Field(description="Authorization checks for this resource", exclude=True),
    ] = None

    @classmethod
    def from_function(
        cls,
        fn: Callable[..., Any],
        uri: str | AnyUrl,
        *,
        name: str | None = None,
        version: str | int | None = None,
        title: str | None = None,
        description: str | None = None,
        icons: list[Icon] | None = None,
        mime_type: str | None = None,
        tags: set[str] | None = None,
        annotations: Annotations | None = None,
        meta: dict[str, Any] | None = None,
        task: bool | TaskConfig | None = None,
        auth: AuthCheck | list[AuthCheck] | None = None,
    ) -> FunctionResource:
        from fastmcp.resources.function_resource import (
            FunctionResource,
        )

        return FunctionResource.from_function(
            fn=fn,
            uri=uri,
            name=name,
            version=version,
            title=title,
            description=description,
            icons=icons,
            mime_type=mime_type,
            tags=tags,
            annotations=annotations,
            meta=meta,
            task=task,
            auth=auth,
        )

    @field_validator("mime_type", mode="before")
    @classmethod
    def set_default_mime_type(cls, mime_type: str | None) -> str:
        """Set default MIME type if not provided."""
        if mime_type:
            return mime_type
        return "text/plain"

    @model_validator(mode="after")
    def set_default_name(self) -> Self:
        """Set default name from URI if not provided."""
        if self.name:
            pass
        elif self.uri:
            self.name = str(self.uri)
        else:
            raise ValueError("Either name or uri must be provided")
        return self

    async def read(
        self,
    ) -> str | bytes | ResourceResult:
        """Read the resource content.

        Subclasses implement this to return resource data. Supported return types:
            - str: Text content
            - bytes: Binary content
            - ResourceResult: Full control over contents and result-level meta
        """
        raise NotImplementedError("Subclasses must implement read()")

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

        This is used in two contexts:
        1. In _read() to convert user function return values to ResourceResult
        2. In tasks_result_handler() to convert Docket task results to ResourceResult

        Handles ResourceResult passthrough and converts raw values using
        ResourceResult's normalization.  When the raw value is a plain
        string or bytes, the resource's own ``mime_type`` is forwarded so
        that ``ui://`` resources (and others with non-default MIME types)
        don't fall back to ``text/plain``.

        The resource's component-level ``meta`` (e.g. ``ui`` metadata for
        MCP Apps CSP/permissions) is propagated to each content item so
        that hosts can read it from the ``resources/read`` response.
        """
        if isinstance(raw_value, ResourceResult):
            return raw_value

        # For plain str/bytes returns, wrap in ResourceContent with the
        # resource's MIME type and component meta so the wire response
        # carries the correct type and metadata (e.g. CSP for MCP Apps).
        if isinstance(raw_value, (str, bytes)):
            return ResourceResult(
                [ResourceContent(raw_value, mime_type=self.mime_type, meta=self.meta)]
            )

        # For JSON-native types (dict, list, tuple, int, float, bool, None),
        # serialize and wrap in ResourceContent with the component's meta,
        # matching the str/bytes path above so CSP/permissions propagate.
        # Exclude list[ResourceContent] which should go through ResourceResult
        # normalization below.
        if (
            isinstance(raw_value, dict | list | tuple | int | float | bool)
            or raw_value is None
        ) and not (
            isinstance(raw_value, list)
            and raw_value
            and isinstance(raw_value[0], ResourceContent)
        ):
            return ResourceResult(
                [
                    ResourceContent(
                        json.dumps(raw_value),
                        mime_type=self.mime_type or "application/json",
                        meta=self.meta,
                    )
                ]
            )

        # All other types fall through to ResourceResult for error handling
        return ResourceResult(raw_value)

    @overload
    async def _read(self, task_meta: None = None) -> ResourceResult: ...

    @overload
    async def _read(self, task_meta: TaskMeta) -> mcp.types.CreateTaskResult: ...

    async def _read(
        self, task_meta: TaskMeta | None = None
    ) -> ResourceResult | mcp.types.CreateTaskResult:
        """Server entry point that handles task routing.

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

        Args:
            task_meta: If provided, execute as a background task and return
                CreateTaskResult. If None (default), execute synchronously and
                return ResourceResult.

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

        Subclasses can override this to customize task routing behavior.
        For example, FastMCPProviderResource 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="resource", arguments=None, task_meta=task_meta
        )
        if task_result:
            return task_result

        # Synchronous execution - convert result to ResourceResult
        result = await self.read()
        return self.convert_result(result)

    def to_mcp_resource(
        self,
        **overrides: Any,
    ) -> SDKResource:
        """Convert the resource to an SDKResource."""

        return SDKResource(
            name=overrides.get("name", self.name),
            uri=overrides.get("uri", self.uri),
            description=overrides.get("description", self.description),
            mimeType=overrides.get("mimeType", self.mime_type),
            title=overrides.get("title", self.title),
            icons=overrides.get("icons", self.icons),
            annotations=overrides.get("annotations", self.annotations),
            _meta=overrides.get(  # type: ignore[call-arg]  # _meta is Pydantic alias for meta field
                "_meta", self.get_meta()
            ),  # ty:ignore[unknown-argument]
        )

    def __repr__(self) -> str:
        return f"{self.__class__.__name__}(uri={self.uri!r}, name={self.name!r}, description={self.description!r}, tags={self.tags})"

    @property
    def key(self) -> str:
        """The globally unique lookup key for this resource."""
        base_key = self.make_key(str(self.uri))
        return f"{base_key}@{self.version or ''}"

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

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

        Args:
            docket: The Docket instance
            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)()

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


__all__ = [
    "Resource",
    "ResourceContent",
    "ResourceResult",
]


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

    if name in deprecated_exports:
        import warnings

        import fastmcp

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

        return getattr(function_resource, name)

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