"""`ServerSession`: server-to-client requests and notifications.

A per-request proxy built by the kernel for each inbound request. Exposes the
request-scoped outbound channel and the connection's standalone channel.
Handlers reach it as `ctx.session` and use the typed helpers (`elicit_form`,
`send_log_message`, ...) to call back to the client.
"""

import logging
from typing import Any, TypeVar, overload

import mcp_types as types
from mcp_types import methods as _methods
from mcp_types.version import MODERN_PROTOCOL_VERSIONS
from pydantic import AnyUrl, BaseModel
from typing_extensions import deprecated

from mcp.server.connection import Connection, allowed_log_levels
from mcp.server.validation import validate_sampling_tools, validate_tool_use_result_messages, wants_sampling_tools
from mcp.shared.dispatcher import CallOptions, DispatchContext, ProgressFnT
from mcp.shared.exceptions import MCPDeprecationWarning
from mcp.shared.message import ServerMessageMetadata

__all__ = ["ServerSession"]

logger = logging.getLogger(__name__)
# `send_log_message`'s `logger` parameter (public API, the spec's logger-name
# field) shadows the module logger inside that method; this alias keeps it
# reachable there.
_logger = logger

ResultT = TypeVar("ResultT", bound=BaseModel)


class ServerSession:
    """Per-request proxy for server-to-client requests and notifications.

    Built once per inbound request by the kernel's `_make_context`. Holds two
    `Outbound` channels: the request-scoped one (the per-request
    `DispatchContext`, which on streamable HTTP routes onto the originating
    POST's response stream) and the connection's standalone channel
    (`connection.outbound`). `related_request_id` on the public methods is the
    selector — present means request-scoped, absent means standalone — and
    never crosses the `Outbound` Protocol.
    """

    def __init__(
        self,
        request_outbound: DispatchContext[Any],
        connection: Connection,
        *,
        request_meta: types.RequestParamsMeta | None = None,
    ) -> None:
        self._request_outbound = request_outbound
        self._connection = connection
        # The per-request log-delivery contract, fixed at construction: on
        # 2026-07-28+ the inbound request's `_meta` log-level opt-in decides
        # which `notifications/message` levels may be sent for this request
        # (and they ride this request's stream only); on handshake versions
        # every level may be sent (`logging/setLevel`-era semantics).
        self._log_is_request_scoped = connection.protocol_version in MODERN_PROTOCOL_VERSIONS
        self._allowed_log_levels = allowed_log_levels(connection.protocol_version, request_meta)

    @property
    def client_params(self) -> types.InitializeRequestParams | None:
        """The client's `initialize` request params; `None` when no client info was supplied."""
        return self._connection.client_params

    @property
    def client_capabilities(self) -> types.ClientCapabilities | None:
        """The capabilities the client declared; `None` when none were declared.

        Prefer this over `client_params.capabilities`: on 2026-07-28+ the
        request envelope declares capabilities while client info stays
        optional, so capabilities can be present without `client_params`.
        """
        return self._connection.client_capabilities

    @property
    def can_send_request(self) -> bool:
        """Whether this request's channel can currently deliver a server-initiated request."""
        return self._request_outbound.can_send_request

    @property
    def protocol_version(self) -> str:
        """The protocol version this connection speaks.

        Populated at `Connection` construction and overwritten once the
        handshake commits on the loop path; never `None`.
        """
        return self._connection.protocol_version

    async def send_request(
        self,
        request: types.ServerRequest,
        result_type: type[ResultT],
        request_read_timeout_seconds: float | None = None,
        metadata: ServerMessageMetadata | None = None,
        progress_callback: ProgressFnT | None = None,
    ) -> ResultT:
        """Send a typed server-to-client request and validate the result.

        Raises:
            MCPError: The peer responded with an error.
            NoBackChannelError: The connection has no back-channel for
                server-initiated requests (raised by the held `Outbound`).
            pydantic.ValidationError: The peer's result does not match `result_type`.
        """
        related = metadata.related_request_id if metadata is not None else None
        channel = self._request_outbound if related is not None else self._connection.outbound
        data = request.model_dump(by_alias=True, mode="json", exclude_none=True)
        opts: CallOptions = {}
        if request_read_timeout_seconds is not None:
            opts["timeout"] = request_read_timeout_seconds
        if progress_callback is not None:
            opts["on_progress"] = progress_callback
        result = await channel.send_raw_request(data["method"], data.get("params"), opts or None)
        try:
            _methods.validate_client_result(request.method, self.protocol_version, result)
        except KeyError:
            pass
        return result_type.model_validate(result, by_name=False)

    async def send_notification(
        self,
        notification: types.ServerNotification,
        related_request_id: types.RequestId | None = None,
    ) -> None:
        """Send a typed server-to-client notification."""
        await self._notify(notification, request_scoped=related_request_id is not None)

    async def _notify(self, notification: types.ServerNotification, *, request_scoped: bool) -> None:
        channel = self._request_outbound if request_scoped else self._connection.outbound
        data = notification.model_dump(by_alias=True, mode="json", exclude_none=True)
        await channel.notify(data["method"], data.get("params"))

    def check_client_capability(self, capability: types.ClientCapabilities) -> bool:
        """Check if the client supports a specific capability."""
        return self._connection.check_capability(capability)

    @deprecated("The logging capability is deprecated as of 2026-07-28 (SEP-2577).", category=MCPDeprecationWarning)
    async def send_log_message(
        self,
        level: types.LoggingLevel,
        data: Any,
        logger: str | None = None,
        related_request_id: types.RequestId | None = None,
    ) -> None:
        """Send a log message notification.

        On 2026-07-28+ delivery is a per-request opt-in: nothing is sent
        unless this request's `_meta` carried the reserved log-level key, and
        entries below the requested level are dropped (debug-logged). What is
        sent rides this request's stream regardless of `related_request_id` -
        the spec forbids `notifications/message` on any stream but the one
        carrying the response. Handshake versions send unconditionally on the
        channel `related_request_id` selects, as before.
        """
        if level not in self._allowed_log_levels:
            _logger.debug("dropped notifications/message at %r: not opted in at that level on this request", level)
            return
        await self._notify(
            types.LoggingMessageNotification(
                params=types.LoggingMessageNotificationParams(
                    level=level,
                    data=data,
                    logger=logger,
                ),
            ),
            request_scoped=self._log_is_request_scoped or related_request_id is not None,
        )

    async def send_resource_updated(self, uri: str | AnyUrl) -> None:
        """Send a resource updated notification."""
        await self.send_notification(
            types.ResourceUpdatedNotification(
                params=types.ResourceUpdatedNotificationParams(uri=str(uri)),
            )
        )

    @overload
    @deprecated("The sampling capability is deprecated as of 2026-07-28 (SEP-2577).", category=MCPDeprecationWarning)
    async def create_message(
        self,
        messages: list[types.SamplingMessage],
        *,
        max_tokens: int,
        system_prompt: str | None = None,
        include_context: types.IncludeContext | None = None,
        temperature: float | None = None,
        stop_sequences: list[str] | None = None,
        metadata: dict[str, Any] | None = None,
        model_preferences: types.ModelPreferences | None = None,
        tools: None = None,
        tool_choice: None = None,
        related_request_id: types.RequestId | None = None,
    ) -> types.CreateMessageResult:
        """Overload: Without tools or tool_choice, returns single content."""
        ...

    @overload
    @deprecated("The sampling capability is deprecated as of 2026-07-28 (SEP-2577).", category=MCPDeprecationWarning)
    async def create_message(
        self,
        messages: list[types.SamplingMessage],
        *,
        max_tokens: int,
        system_prompt: str | None = None,
        include_context: types.IncludeContext | None = None,
        temperature: float | None = None,
        stop_sequences: list[str] | None = None,
        metadata: dict[str, Any] | None = None,
        model_preferences: types.ModelPreferences | None = None,
        tools: list[types.Tool],
        tool_choice: types.ToolChoice | None = None,
        related_request_id: types.RequestId | None = None,
    ) -> types.CreateMessageResultWithTools:
        """Overload: With tools, returns array-capable content."""
        ...

    @overload
    @deprecated("The sampling capability is deprecated as of 2026-07-28 (SEP-2577).", category=MCPDeprecationWarning)
    async def create_message(
        self,
        messages: list[types.SamplingMessage],
        *,
        max_tokens: int,
        system_prompt: str | None = None,
        include_context: types.IncludeContext | None = None,
        temperature: float | None = None,
        stop_sequences: list[str] | None = None,
        metadata: dict[str, Any] | None = None,
        model_preferences: types.ModelPreferences | None = None,
        tools: list[types.Tool] | None = None,
        tool_choice: types.ToolChoice,
        related_request_id: types.RequestId | None = None,
    ) -> types.CreateMessageResultWithTools:
        """Overload: With tool_choice, returns array-capable content."""
        ...

    @deprecated("The sampling capability is deprecated as of 2026-07-28 (SEP-2577).", category=MCPDeprecationWarning)
    async def create_message(
        self,
        messages: list[types.SamplingMessage],
        *,
        max_tokens: int,
        system_prompt: str | None = None,
        include_context: types.IncludeContext | None = None,
        temperature: float | None = None,
        stop_sequences: list[str] | None = None,
        metadata: dict[str, Any] | None = None,
        model_preferences: types.ModelPreferences | None = None,
        tools: list[types.Tool] | None = None,
        tool_choice: types.ToolChoice | None = None,
        related_request_id: types.RequestId | None = None,
    ) -> types.CreateMessageResult | types.CreateMessageResultWithTools:
        """Send a sampling/create_message request.

        Args:
            messages: The conversation messages to send.
            max_tokens: Maximum number of tokens to generate.
            system_prompt: Optional system prompt.
            include_context: Optional context inclusion setting.
                Should only be set to "thisServer" or "allServers"
                if the client has sampling.context capability.
            temperature: Optional sampling temperature.
            stop_sequences: Optional stop sequences.
            metadata: Optional metadata to pass through to the LLM provider.
            model_preferences: Optional model selection preferences.
            tools: Optional list of tools the LLM can use during sampling.
                Requires client to have sampling.tools capability.
            tool_choice: Optional control over tool usage behavior.
                Requires client to have sampling.tools capability.
            related_request_id: Optional ID of a related request.

        Returns:
            The sampling result from the client.

        Raises:
            MCPError: If tools are provided but client doesn't support them.
            ValueError: If tool_use or tool_result message structure is invalid.
            NoBackChannelError: The connection has no back-channel for
                server-initiated requests.
        """
        validate_sampling_tools(self.client_capabilities, tools, tool_choice)
        validate_tool_use_result_messages(messages)

        request = types.CreateMessageRequest(
            params=types.CreateMessageRequestParams(
                messages=messages,
                system_prompt=system_prompt,
                include_context=include_context,
                temperature=temperature,
                max_tokens=max_tokens,
                stop_sequences=stop_sequences,
                metadata=metadata,
                model_preferences=model_preferences,
                tools=tools,
                tool_choice=tool_choice,
            ),
        )
        metadata_obj = ServerMessageMetadata(related_request_id=related_request_id)

        if wants_sampling_tools(tools, tool_choice):
            return await self.send_request(
                request=request,
                result_type=types.CreateMessageResultWithTools,
                metadata=metadata_obj,
            )
        return await self.send_request(
            request=request,
            result_type=types.CreateMessageResult,
            metadata=metadata_obj,
        )

    @deprecated("The roots capability is deprecated as of 2026-07-28 (SEP-2577).", category=MCPDeprecationWarning)
    async def list_roots(self) -> types.ListRootsResult:
        """Send a roots/list request.

        Raises:
            NoBackChannelError: The connection has no back-channel for
                server-initiated requests.
        """
        return await self.send_request(
            types.ListRootsRequest(),
            types.ListRootsResult,
        )

    async def elicit(
        self,
        message: str,
        requested_schema: types.ElicitRequestedSchema,
        related_request_id: types.RequestId | None = None,
    ) -> types.ElicitResult:
        """Send a form mode elicitation/create request.

        Args:
            message: The message to present to the user.
            requested_schema: Schema defining the expected response structure.
            related_request_id: Optional ID of the request that triggered this elicitation.

        Returns:
            The client's response.

        Note:
            This method is deprecated in favor of elicit_form(). It remains for
            backward compatibility but new code should use elicit_form().
        """
        return await self.elicit_form(message, requested_schema, related_request_id)

    async def elicit_form(
        self,
        message: str,
        requested_schema: types.ElicitRequestedSchema,
        related_request_id: types.RequestId | None = None,
    ) -> types.ElicitResult:
        """Send a form mode elicitation/create request.

        Args:
            message: The message to present to the user.
            requested_schema: Schema defining the expected response structure.
            related_request_id: Optional ID of the request that triggered this elicitation.

        Returns:
            The client's response with form data.

        Raises:
            NoBackChannelError: The connection has no back-channel for
                server-initiated requests.
        """
        return await self.send_request(
            types.ElicitRequest(
                params=types.ElicitRequestFormParams(
                    message=message,
                    requested_schema=requested_schema,
                ),
            ),
            types.ElicitResult,
            metadata=ServerMessageMetadata(related_request_id=related_request_id),
        )

    async def elicit_url(
        self,
        message: str,
        url: str,
        elicitation_id: str,
        related_request_id: types.RequestId | None = None,
    ) -> types.ElicitResult:
        """Send a URL mode elicitation/create request.

        This directs the user to an external URL for out-of-band interactions
        like OAuth flows, credential collection, or payment processing.

        Args:
            message: Human-readable explanation of why the interaction is needed.
            url: The URL the user should navigate to.
            elicitation_id: Unique identifier for tracking this elicitation.
            related_request_id: Optional ID of the request that triggered this elicitation.

        Returns:
            The client's response indicating acceptance, decline, or cancellation.

        Raises:
            NoBackChannelError: The connection has no back-channel for
                server-initiated requests.
        """
        return await self.send_request(
            types.ElicitRequest(
                params=types.ElicitRequestURLParams(
                    message=message,
                    url=url,
                    elicitation_id=elicitation_id,
                ),
            ),
            types.ElicitResult,
            metadata=ServerMessageMetadata(related_request_id=related_request_id),
        )

    async def send_ping(self) -> types.EmptyResult:
        """Send a ping request."""
        return await self.send_request(
            types.PingRequest(),
            types.EmptyResult,
        )

    async def report_progress(self, progress: float, total: float | None = None, message: str | None = None) -> None:
        """Report progress for the inbound request this session is scoped to.

        A no-op when the caller did not request progress. Dispatcher-agnostic:
        on JSON-RPC the held `DispatchContext` emits ``notifications/progress``
        against the caller's token; on the in-process direct dispatcher it
        invokes the caller's callback directly.
        """
        await self._request_outbound.progress(progress, total, message)

    async def send_progress_notification(
        self,
        progress_token: str | int,
        progress: float,
        total: float | None = None,
        message: str | None = None,
        related_request_id: str | None = None,
    ) -> None:
        """Send a progress notification."""
        await self.send_notification(
            types.ProgressNotification(
                params=types.ProgressNotificationParams(
                    progress_token=progress_token,
                    progress=progress,
                    total=total,
                    message=message,
                ),
            ),
            related_request_id,
        )

    async def send_resource_list_changed(self) -> None:
        """Send a resource list changed notification."""
        await self.send_notification(types.ResourceListChangedNotification())

    async def send_tool_list_changed(self) -> None:
        """Send a tool list changed notification."""
        await self.send_notification(types.ToolListChangedNotification())

    async def send_prompt_list_changed(self) -> None:
        """Send a prompt list changed notification."""
        await self.send_notification(types.PromptListChangedNotification())

    async def send_elicit_complete(
        self,
        elicitation_id: str,
        related_request_id: types.RequestId | None = None,
    ) -> None:
        """Send an elicitation completion notification.

        This should be sent when a URL mode elicitation has been completed
        out-of-band to inform the client that it may retry any requests
        that were waiting for this elicitation.

        Args:
            elicitation_id: The unique identifier of the completed elicitation
            related_request_id: Optional ID of the request that triggered this notification
        """
        await self.send_notification(
            types.ElicitCompleteNotification(
                params=types.ElicitCompleteNotificationParams(elicitation_id=elicitation_id)
            ),
            related_request_id,
        )
