"""Lifespan and Docket task infrastructure for FastMCP Server."""

from __future__ import annotations

import asyncio
import weakref
from collections.abc import AsyncIterator
from contextlib import AsyncExitStack, asynccontextmanager, suppress
from contextvars import ContextVar
from typing import TYPE_CHECKING, Any

import anyio
from uncalled_for import SharedContext

import fastmcp
from fastmcp.utilities.logging import get_logger

if TYPE_CHECKING:
    from docket import Docket

    from fastmcp.server.server import FastMCP

logger = get_logger(__name__)


# Set True by `FastMCPProvider.lifespan` immediately before it enters the
# wrapped (mounted) server's `_lifespan_manager`, and reset on exit. The
# mounted server's `_docket_lifespan` reads this and becomes a no-op so that
# Docket / Worker / SharedContext are not re-initialized — there's one set
# per runtime tree, owned by the root.
#
# Independent servers entered as siblings (e.g. via `AsyncExitStack` in the
# same async context) are NOT in a parent/child relationship; the flag is not
# set in that case, so each independently establishes its own Docket and
# server context.
_lifespan_root_active: ContextVar[bool] = ContextVar(
    "fastmcp_lifespan_root_active", default=False
)


class LifespanMixin:
    """Mixin providing lifespan and Docket task infrastructure for FastMCP."""

    @property
    def docket(self: FastMCP) -> Docket | None:
        """The Docket instance owned by this server.

        Returns the Docket that this server initialized as the root of a
        runtime tree. Mounted children do not own their own Docket — they
        share the root's via ``_current_docket`` ContextVar inheritance —
        so accessing ``.docket`` on a mounted child returns None even while
        its tasks run on the root's Docket. For "the Docket in scope right
        now," prefer reading ``_current_docket`` directly or use the
        ``CurrentDocket`` dependency injection.
        """
        return self._docket

    @asynccontextmanager
    async def _docket_lifespan(self: FastMCP) -> AsyncIterator[None]:
        """Manage Docket instance and Worker for background task execution.

        Docket is process-level, not server-level: only the first server in a
        runtime tree starts Docket and the Worker. Mounted children entered
        via ``FastMCPProvider.lifespan`` see ``_lifespan_root_active=True``
        (set by the provider before delegating to ``_lifespan_manager``) and
        become no-ops, sharing the root's Docket via ``_current_docket``.

        Independent servers entered as siblings — for example two unrelated
        ``FastMCP`` instances each entered through ``AsyncExitStack`` in the
        same async context — are not in a parent/child relationship; no
        provider has set the flag for them, so each runs the full root setup.

        Docket infrastructure is only initialized at the root if:
        1. pydocket is installed (fastmcp[tasks] extra)
        2. There are task-enabled components (task_config.mode != 'forbidden')

        Users with pydocket installed but no task-enabled components won't spin
        up Docket / Worker infrastructure even at the root.
        """
        # Nested entry: a parent in this runtime tree already owns Docket and
        # SharedContext (the FastMCPProvider that mounted us set the flag).
        # Stay out of their way and inherit via ContextVars.
        if _lifespan_root_active.get():
            yield
            return

        async with self._docket_lifespan_root():
            yield

    @asynccontextmanager
    async def _docket_lifespan_root(self: FastMCP) -> AsyncIterator[None]:
        """Root-only Docket lifecycle. See _docket_lifespan for the dispatch."""
        from fastmcp.server.dependencies import _current_server, is_docket_available

        # Set FastMCP server in ContextVar so CurrentFastMCP can access it
        # (use weakref to avoid reference cycles)
        server_token = _current_server.set(weakref.ref(self))

        try:
            # If docket is not available, skip task infrastructure but still
            # set up SharedContext so Shared() dependencies work.
            if not is_docket_available():
                async with SharedContext():
                    yield
                return

            # Collect task-enabled components at startup with all transforms applied.
            # Components must be available now to be registered with Docket workers;
            # dynamically added components after startup won't be registered.
            try:
                task_components = list(await self.get_tasks())
            except Exception as e:
                logger.warning(f"Failed to get tasks: {e}")
                if fastmcp.settings.mounted_components_raise_on_load_error:
                    raise
                task_components = []

            # If no task-enabled components, skip Docket infrastructure but still
            # set up SharedContext so Shared() dependencies work.
            if not task_components:
                async with SharedContext():
                    yield
                return

            # Docket is available AND there are task-enabled components
            from docket import Depends, Docket, Worker

            from fastmcp import settings
            from fastmcp.server.dependencies import (
                _current_docket,
                _current_worker,
            )
            from fastmcp.server.tasks.context import restore_task_snapshot

            # Create Docket instance using configured name and URL
            async with Docket(
                name=settings.docket.name,
                url=settings.docket.url,
            ) as docket:
                self._docket = docket

                # Register task-enabled components with Docket
                for component in task_components:
                    component.register_with_docket(docket)

                docket_token = _current_docket.set(docket)
                try:
                    # Build worker kwargs from settings
                    worker_kwargs: dict[str, Any] = {
                        "concurrency": settings.docket.concurrency,
                        "redelivery_timeout": settings.docket.redelivery_timeout,
                        "reconnection_delay": settings.docket.reconnection_delay,
                        "minimum_check_interval": settings.docket.minimum_check_interval,
                    }
                    if settings.docket.worker_name:
                        worker_kwargs["name"] = settings.docket.worker_name

                    # Create and start Worker.  The restore_task_snapshot
                    # worker-level dependency runs before every task so the
                    # per-task snapshot ContextVar is populated before user
                    # code or task-scoped dependencies observe it.
                    async with Worker(
                        docket,
                        dependencies=[Depends(restore_task_snapshot)],
                        **worker_kwargs,
                    ) as worker:
                        self._worker = worker
                        worker_token = _current_worker.set(worker)
                        try:
                            worker_task = asyncio.create_task(worker.run_forever())
                            try:
                                yield
                            finally:
                                worker_task.cancel()
                                with suppress(asyncio.CancelledError):
                                    await worker_task
                        finally:
                            _current_worker.reset(worker_token)
                            self._worker = None
                finally:
                    _current_docket.reset(docket_token)
                    self._docket = None
        finally:
            # Reset server ContextVar
            _current_server.reset(server_token)

    @asynccontextmanager
    async def _lifespan_manager(self: FastMCP) -> AsyncIterator[None]:
        async with self._lifespan_lock:
            if self._lifespan_result_set:
                self._lifespan_ref_count += 1
                should_enter_lifespan = False
            else:
                self._lifespan_ref_count = 1
                should_enter_lifespan = True

        if not should_enter_lifespan:
            try:
                yield
            finally:
                async with self._lifespan_lock:
                    self._lifespan_ref_count -= 1
                    if self._lifespan_ref_count == 0:
                        self._lifespan_result_set = False
                        self._lifespan_result = None
            return

        # Use an explicit AsyncExitStack so we can shield teardown from
        # cancellation. Without this, Ctrl-C causes CancelledError to
        # propagate into lifespan finally blocks, preventing any async
        # cleanup (e.g. closing DB connections, flushing buffers).
        stack = AsyncExitStack()
        try:
            user_lifespan_result = await stack.enter_async_context(self._lifespan(self))
            await stack.enter_async_context(self._docket_lifespan())

            self._lifespan_result = user_lifespan_result
            self._lifespan_result_set = True

            # Start lifespans for all providers
            for provider in self.providers:
                await stack.enter_async_context(provider.lifespan())

            self._started.set()
            try:
                yield
            finally:
                self._started.clear()
        finally:
            try:
                with anyio.CancelScope(shield=True):
                    await stack.aclose()
            finally:
                async with self._lifespan_lock:
                    self._lifespan_ref_count -= 1
                    if self._lifespan_ref_count == 0:
                        self._lifespan_result_set = False
                        self._lifespan_result = None

    def _setup_task_protocol_handlers(self: FastMCP) -> None:
        """Register SEP-1686 task protocol handlers with SDK.

        Only registers handlers if docket is installed. Without docket,
        task protocol requests will return "method not found" errors.
        """
        from fastmcp.server.dependencies import is_docket_available

        if not is_docket_available():
            return

        from mcp.types import (
            CancelTaskRequest,
            GetTaskPayloadRequest,
            GetTaskRequest,
            ListTasksRequest,
            ServerResult,
        )

        from fastmcp.server.tasks.requests import (
            tasks_cancel_handler,
            tasks_get_handler,
            tasks_list_handler,
            tasks_result_handler,
        )

        # Manually register handlers (SDK decorators fail with locally-defined functions)
        # SDK expects handlers that receive Request objects and return ServerResult

        async def handle_get_task(req: GetTaskRequest) -> ServerResult:
            params = req.params.model_dump(by_alias=True, exclude_none=True)
            result = await tasks_get_handler(self, params)
            return ServerResult(result)

        async def handle_get_task_result(req: GetTaskPayloadRequest) -> ServerResult:
            params = req.params.model_dump(by_alias=True, exclude_none=True)
            result = await tasks_result_handler(self, params)
            return ServerResult(result)

        async def handle_list_tasks(req: ListTasksRequest) -> ServerResult:
            params = (
                req.params.model_dump(by_alias=True, exclude_none=True)
                if req.params
                else {}
            )
            result = await tasks_list_handler(self, params)
            return ServerResult(result)

        async def handle_cancel_task(req: CancelTaskRequest) -> ServerResult:
            params = req.params.model_dump(by_alias=True, exclude_none=True)
            result = await tasks_cancel_handler(self, params)
            return ServerResult(result)

        # Register directly with SDK (same as what decorators do internally)
        self._mcp_server.request_handlers[GetTaskRequest] = handle_get_task
        self._mcp_server.request_handlers[GetTaskPayloadRequest] = (
            handle_get_task_result
        )
        self._mcp_server.request_handlers[ListTasksRequest] = handle_list_tasks
        self._mcp_server.request_handlers[CancelTaskRequest] = handle_cancel_task
