"""SEP-1686 task execution handlers.

Handles queuing tool/prompt/resource executions to Docket as background tasks.
"""

from __future__ import annotations

import uuid
from contextlib import suppress
from datetime import datetime, timezone
from typing import TYPE_CHECKING, Any, Literal

import mcp.types
from mcp.shared.exceptions import McpError
from mcp.types import INTERNAL_ERROR, ErrorData

from fastmcp.server.dependencies import (
    _current_docket,
    get_context,
)
from fastmcp.server.tasks.config import TaskMeta
from fastmcp.server.tasks.context import (
    TaskContextSnapshot,
    get_task_scope,
    register_task_server,
    register_task_session,
)
from fastmcp.server.tasks.keys import build_task_key, task_redis_prefix
from fastmcp.utilities.logging import get_logger

if TYPE_CHECKING:
    from fastmcp.prompts.base import Prompt
    from fastmcp.resources.base import Resource
    from fastmcp.resources.template import ResourceTemplate
    from fastmcp.tools.base import Tool

logger = get_logger(__name__)

# Redis mapping TTL buffer: Add 15 minutes to Docket's execution_ttl
TASK_MAPPING_TTL_BUFFER_SECONDS = 15 * 60


async def submit_to_docket(
    task_type: Literal["tool", "resource", "template", "prompt"],
    key: str,
    component: Tool | Resource | ResourceTemplate | Prompt,
    arguments: dict[str, Any] | None = None,
    task_meta: TaskMeta | None = None,
) -> mcp.types.CreateTaskResult:
    """Submit any component to Docket for background execution (SEP-1686).

    Unified handler for all component types. Called by component's internal
    methods (_run, _read, _render) when task metadata is present and mode allows.

    Queues the component's method to Docket, stores raw return values,
    and converts to MCP types on retrieval.

    Args:
        task_type: Component type for task key construction
        key: The component key as seen by MCP layer (with namespace prefix)
        component: The component instance (Tool, Resource, ResourceTemplate, Prompt)
        arguments: Arguments/params (None for Resource which has no args)
        task_meta: Task execution metadata. If task_meta.ttl is provided, it
            overrides the server default (docket.execution_ttl).

    Returns:
        CreateTaskResult: Task stub with proper Task object
    """
    # Validate and coerce arguments before creating any task state. A failure
    # here must surface before the Redis metadata and initial "working"
    # notification below are written, otherwise an invalid input would orphan a
    # task the client has already observed (#4349).
    if arguments is not None:
        arguments = component.coerce_task_arguments(arguments)

    # Generate server-side task ID per SEP-1686 final spec (line 375-377)
    # Server MUST generate task IDs, clients no longer provide them
    server_task_id = str(uuid.uuid4())

    # Record creation timestamp per SEP-1686 final spec (line 430)
    created_at = datetime.now(timezone.utc)

    ctx = get_context()

    # Authorization scope for task isolation (auth identity, or None for anonymous)
    task_scope = get_task_scope()

    # Transport session ID for notification delivery
    try:
        session_id = ctx.session_id
    except RuntimeError:
        session_id = None

    # Try the server's own Docket first; fall back to the ContextVar for
    # mounted children (whose parent server owns the Docket instance).
    docket = ctx.fastmcp._docket or _current_docket.get()
    if docket is None:
        raise McpError(
            ErrorData(
                code=INTERNAL_ERROR,
                message="Background tasks require a running FastMCP server context",
            )
        )

    # Register the current server so background workers resolve
    # CurrentFastMCP() / ctx.fastmcp to the correct (child) server
    # for mounted tasks. At this point ctx.fastmcp is the child because
    # we're inside the child's call_tool dispatch.
    register_task_server(server_task_id, ctx.fastmcp)

    # Build full task key with embedded metadata
    task_key = build_task_key(task_scope, server_task_id, task_type, key)

    # Determine TTL: use task_meta.ttl if provided, else docket default
    if task_meta is not None and task_meta.ttl is not None:
        ttl_ms = task_meta.ttl
    else:
        ttl_ms = int(docket.execution_ttl.total_seconds() * 1000)
    ttl_seconds = int(ttl_ms / 1000) + TASK_MAPPING_TTL_BUFFER_SECONDS

    # Store task metadata in Redis for protocol handlers
    prefix = task_redis_prefix(task_scope)
    task_meta_key = docket.key(f"{prefix}:{server_task_id}")
    created_at_key = docket.key(f"{prefix}:{server_task_id}:created_at")
    poll_interval_key = docket.key(f"{prefix}:{server_task_id}:poll_interval")
    poll_interval_ms = int(component.task_config.poll_interval.total_seconds() * 1000)

    # Snapshot all context (access token, headers, origin request ID,
    # and session_id for notification delivery in background workers)
    snapshot = TaskContextSnapshot.capture()

    async with docket.redis() as redis:
        await redis.set(task_meta_key, task_key, ex=ttl_seconds)
        await redis.set(created_at_key, created_at.isoformat(), ex=ttl_seconds)
        await redis.set(poll_interval_key, str(poll_interval_ms), ex=ttl_seconds)

    await snapshot.save(docket, task_scope, server_task_id, ttl_seconds)

    # Register session for Context access in background workers (SEP-1686)
    # This enables elicitation/sampling from background tasks via weakref
    # Skip when there is no session (programmatic calls without MCP session)
    if session_id is not None:
        register_task_session(session_id, ctx.session)

    # Send an initial tasks/status notification before queueing.
    # This guarantees clients can observe task creation immediately.
    notification = mcp.types.TaskStatusNotification.model_validate(
        {
            "method": "notifications/tasks/status",
            "params": {
                "taskId": server_task_id,
                "status": "working",
                "statusMessage": "Task submitted",
                "createdAt": created_at,
                "lastUpdatedAt": created_at,
                "ttl": ttl_ms,
                "pollInterval": poll_interval_ms,
            },
            "_meta": {
                "io.modelcontextprotocol/related-task": {
                    "taskId": server_task_id,
                }
            },
        }
    )
    server_notification = mcp.types.ServerNotification(notification)
    with suppress(Exception):
        # Don't let notification failures break task creation
        await ctx.session.send_notification(server_notification)

    # Queue function to Docket by key (result storage via execution_ttl)
    # Use component.add_to_docket() which handles calling conventions
    # `fn_key` is the function lookup key (e.g., "child_multiply")
    # `task_key` is the task result key (e.g., "fastmcp:task:{task_scope}:{task_id}:tool:child_multiply")
    # Resources don't take arguments; tools/prompts/templates always pass arguments (even if None/empty)
    if task_type == "resource":
        await component.add_to_docket(docket, fn_key=key, task_key=task_key)  # type: ignore[call-arg]  # ty:ignore[missing-argument]
    else:
        await component.add_to_docket(docket, arguments, fn_key=key, task_key=task_key)  # type: ignore[call-arg]  # ty:ignore[invalid-argument-type, too-many-positional-arguments]

    # Spawn subscription task to send status notifications (SEP-1686 optional feature)
    # Start subscription in session's task group (persists for connection lifetime)
    # Deferred: subscriptions and notifications depend on docket at import time
    from fastmcp.server.tasks.subscriptions import subscribe_to_task_updates

    if hasattr(ctx.session, "_subscription_task_group"):
        tg = ctx.session._subscription_task_group
        if tg:
            tg.start_soon(  # type: ignore[union-attr]  # ty:ignore[unresolved-attribute]
                subscribe_to_task_updates,
                server_task_id,
                task_key,
                ctx.session,
                docket,
                poll_interval_ms,
            )

    # Deferred: notifications depends on docket at import time
    from fastmcp.server.tasks.notifications import (
        ensure_subscriber_running,
        stop_subscriber,
    )

    if session_id is not None:
        try:
            await ensure_subscriber_running(
                session_id, ctx.session, docket, ctx.fastmcp
            )

            # Register cleanup callback on session exit (once per session)
            # This ensures subscriber is stopped when the session disconnects
            if (
                hasattr(ctx.session, "_exit_stack")
                and ctx.session._exit_stack is not None
                and not getattr(ctx.session, "_notification_cleanup_registered", False)
            ):

                async def _cleanup_subscriber() -> None:
                    await stop_subscriber(session_id)  # type: ignore[arg-type]

                ctx.session._exit_stack.push_async_callback(_cleanup_subscriber)
                ctx.session._notification_cleanup_registered = True  # type: ignore[attr-defined]  # ty:ignore[unresolved-attribute]
        except Exception as e:
            # Non-fatal: elicitation will still work via polling fallback
            logger.debug("Failed to start notification subscriber: %s", e)

    # Return CreateTaskResult with proper Task object
    # Tasks MUST begin in "working" status per SEP-1686 final spec (line 381)
    return mcp.types.CreateTaskResult(
        task=mcp.types.Task(
            taskId=server_task_id,
            status="working",
            createdAt=created_at,
            lastUpdatedAt=created_at,
            ttl=ttl_ms,
            pollInterval=poll_interval_ms,
        )
    )
