"""Bearer-token gate for the HTTP transport of the MCP server (#231.2).

Why this exists
---------------
Over ``stdio`` the protection is the pipe itself: only a process that already
runs as this user can speak to the server. ``streamable-http`` removes that
property entirely — anything that can reach the socket can call any of the 108
registered tools, and the registry includes ``case_delete``,
``precedent_library_delete``, ``document_upload`` and every block-writing tool.
An unauthenticated listener is therefore a delete-any-case endpoint.

The agent platform already speaks this dialect: it injects each runtime MCP
server as ``{type:"http", name, url, headers:[{name:"Authorization",
value:"Bearer <token>"}]}``. So a static bearer token is exactly the shape the
caller will present — no negotiation, no OAuth dance.

Design notes
------------
- We implement the SDK's own ``TokenVerifier`` protocol and let
  ``BearerAuthBackend`` do the enforcement, rather than adding bespoke
  middleware. One auth path, the framework's (G2).
- The token is read from the environment, which the service unit populates from
  Infisical. It is never defaulted, never logged, and never embedded here.
- Comparison is constant-time: a naive ``==`` leaks the token byte-by-byte to a
  caller who can time responses.
- ``verify_token`` returns ``None`` (not an exception) on mismatch — that is the
  protocol's "reject" signal and yields a clean 401 instead of a 500 that would
  read as a server fault.
"""

from __future__ import annotations

import hmac
import logging
import os

from mcp.server.auth.provider import AccessToken, TokenVerifier

logger = logging.getLogger("legal_mcp.http_auth")

#: Environment variable carrying the shared bearer token.
#:
#: Name matches the Infisical key exactly — All Infrastructure / env `main` /
#: `/apps/legal-ai` / ``MCP_HTTP_SHARED_SECRET``, tagged ``credentials``. Keeping
#: the two identical means nobody has to hold a mapping in their head, and it
#: follows the two bridge tokens already in that folder
#: (``COURT_FETCH_SHARED_SECRET``, ``LEGAL_CHAT_SHARED_SECRET``).
TOKEN_ENV = "MCP_HTTP_SHARED_SECRET"

#: Minimum acceptable token length. Short tokens are brute-forceable; refusing
#: them at startup is cheaper than discovering it from an access log.
MIN_TOKEN_LEN = 32

#: Reported as the authenticated principal. Single shared token today, so this
#: is a constant rather than a real identity — kept explicit so that audit rows
#: never imply per-agent attribution we cannot actually make.
CLIENT_ID = "mcp-http-shared"


class MissingTokenError(RuntimeError):
    """Raised when the HTTP transport is requested without a usable token.

    Deliberately fatal. The tempting alternative — start anyway and log a
    warning — produces a listener that looks healthy and answers every
    destructive tool call. Refusing to boot is the safe failure (§6: never
    swallow, never degrade silently).
    """


def load_token() -> str:
    """Return the configured bearer token, or raise if it is unusable."""
    token = (os.environ.get(TOKEN_ENV) or "").strip()
    if not token:
        raise MissingTokenError(
            f"{TOKEN_ENV} is not set. The HTTP transport exposes destructive "
            f"tools and will not start without a bearer token. Set it from "
            f"Infisical, or use MCP_TRANSPORT=stdio.",
        )
    if len(token) < MIN_TOKEN_LEN:
        raise MissingTokenError(
            f"{TOKEN_ENV} is shorter than {MIN_TOKEN_LEN} characters — refusing "
            f"to start. Generate a long random token.",
        )
    return token


class StaticTokenVerifier(TokenVerifier):
    """Verifies the single shared bearer token presented by the platform.

    Not an identity system: it answers "may this caller in at all", not "who is
    it". Per-agent attribution would need per-agent tokens, which is a later
    step once profiles exist (#231.4).
    """

    def __init__(self, expected: str) -> None:
        self._expected = expected

    async def verify_token(self, token: str) -> AccessToken | None:
        # compare_digest over bytes; it tolerates unequal lengths without
        # short-circuiting, which is the whole point.
        if not hmac.compare_digest(token.encode("utf-8"),
                                   self._expected.encode("utf-8")):
            # No token material in the log line — only the fact of a rejection.
            logger.warning("Rejected MCP HTTP request: bearer token mismatch")
            return None
        return AccessToken(token=token, client_id=CLIENT_ID, scopes=[])
