"""Minimal in-process OAuth pieces for the auth stories.

A story-shaped subset; ``tests/interaction/auth`` keeps its own (richer) provider.
"""

from __future__ import annotations

import os
import secrets
import time
from urllib.parse import parse_qs, urlsplit

import httpx
from pydantic import AnyHttpUrl

from mcp.server.auth.provider import (
    AccessToken,
    AuthorizationCode,
    AuthorizationParams,
    OAuthAuthorizationServerProvider,
    RefreshToken,
    construct_redirect_uri,
)
from mcp.server.auth.settings import AuthSettings, ClientRegistrationOptions
from mcp.shared.auth import AuthorizationCodeResult, OAuthClientInformationFull, OAuthToken

BASE_URL = "http://127.0.0.1:8000"
MCP_URL = f"{BASE_URL}/mcp"
REDIRECT_URI = f"{BASE_URL}/oauth/callback"


class InMemoryTokenStorage:
    """A ``TokenStorage`` that keeps tokens and DCR client info on instance attributes."""

    tokens: OAuthToken | None = None
    client_info: OAuthClientInformationFull | None = None

    async def get_tokens(self) -> OAuthToken | None:
        return self.tokens

    async def set_tokens(self, tokens: OAuthToken) -> None:
        self.tokens = tokens

    async def get_client_info(self) -> OAuthClientInformationFull | None:
        return self.client_info

    async def set_client_info(self, client_info: OAuthClientInformationFull) -> None:
        self.client_info = client_info


class HeadlessOAuth:
    """Completes the authorize redirect in-process via the bound ``httpx`` client."""

    def __init__(self) -> None:
        self.authorize_url: str | None = None
        self._http: httpx.AsyncClient | None = None
        self._result = AuthorizationCodeResult(code="", state=None)

    def bind(self, http_client: httpx.AsyncClient) -> None:
        self._http = http_client

    async def redirect_handler(self, authorization_url: str) -> None:
        assert self._http is not None
        self.authorize_url = authorization_url
        # ``auth=None`` is load-bearing: re-entering the locked auth flow would deadlock.
        response = await self._http.get(authorization_url, follow_redirects=False, auth=None)
        assert response.status_code == 302, f"authorize returned {response.status_code}: {response.text}"
        params = parse_qs(urlsplit(response.headers["location"]).query)
        self._result = AuthorizationCodeResult(code=params.get("code", [""])[0], state=params.get("state", [None])[0])

    async def callback_handler(self) -> AuthorizationCodeResult:
        return self._result


class InMemoryAuthorizationServerProvider(
    OAuthAuthorizationServerProvider[AuthorizationCode, RefreshToken, AccessToken]
):
    """Minimal demo AS: DCR + authorize + auth-code exchange held in instance dicts.

    ``authorize`` auto-consents only when ``OAUTH_DEMO_AUTO_CONSENT=1``; otherwise it redirects
    with ``error=interaction_required`` so a manual run shows where a real browser would open.
    """

    def __init__(self) -> None:
        self.clients: dict[str, OAuthClientInformationFull] = {}
        self.codes: dict[str, AuthorizationCode] = {}
        self.access_tokens: dict[str, AccessToken] = {}

    def mint_access_token(
        self, *, client_id: str, scopes: list[str], resource: str | None = None, subject: str | None = None
    ) -> str:
        access = f"access_{secrets.token_hex(16)}"
        self.access_tokens[access] = AccessToken(
            token=access,
            client_id=client_id,
            scopes=scopes,
            expires_at=int(time.time()) + 3600,
            resource=resource,
            subject=subject,
        )
        return access

    async def get_client(self, client_id: str) -> OAuthClientInformationFull | None:
        return self.clients.get(client_id)

    async def register_client(self, client_info: OAuthClientInformationFull) -> None:
        assert client_info.client_id is not None
        self.clients[client_info.client_id] = client_info

    async def authorize(self, client: OAuthClientInformationFull, params: AuthorizationParams) -> str:
        target = str(params.redirect_uri)
        if os.environ.get("OAUTH_DEMO_AUTO_CONSENT") != "1":
            return construct_redirect_uri(target, error="interaction_required", state=params.state)
        assert client.client_id is not None
        code = AuthorizationCode(
            code=f"code_{secrets.token_hex(16)}",
            client_id=client.client_id,
            scopes=params.scopes or ["mcp"],
            expires_at=time.time() + 300,
            code_challenge=params.code_challenge,
            redirect_uri=params.redirect_uri,
            redirect_uri_provided_explicitly=params.redirect_uri_provided_explicitly,
            resource=params.resource,
        )
        self.codes[code.code] = code
        return construct_redirect_uri(target, code=code.code, state=params.state)

    async def load_authorization_code(
        self, client: OAuthClientInformationFull, authorization_code: str
    ) -> AuthorizationCode | None:
        return self.codes.get(authorization_code)

    async def exchange_authorization_code(
        self, client: OAuthClientInformationFull, authorization_code: AuthorizationCode
    ) -> OAuthToken:
        scopes = authorization_code.scopes
        access = self.mint_access_token(
            client_id=authorization_code.client_id, scopes=scopes, resource=authorization_code.resource
        )
        del self.codes[authorization_code.code]
        return OAuthToken(access_token=access, token_type="Bearer", expires_in=3600, scope=" ".join(scopes))

    async def load_access_token(self, token: str) -> AccessToken | None:
        return self.access_tokens.get(token)

    async def load_refresh_token(self, client: OAuthClientInformationFull, refresh_token: str) -> RefreshToken | None:
        raise NotImplementedError

    async def exchange_refresh_token(
        self, client: OAuthClientInformationFull, refresh_token: RefreshToken, scopes: list[str]
    ) -> OAuthToken:
        raise NotImplementedError

    async def revoke_token(self, token: AccessToken | RefreshToken) -> None:
        raise NotImplementedError


def auth_settings(
    *, required_scopes: list[str] | None = None, identity_assertion_enabled: bool = False
) -> AuthSettings:
    """``AuthSettings`` for the co-hosted demo AS+RS on the loopback origin, DCR enabled.

    ``identity_assertion_enabled`` passes through to the SEP-990 jwt-bearer grant flag.
    """
    scopes = required_scopes or ["mcp"]
    return AuthSettings(
        issuer_url=AnyHttpUrl(BASE_URL),
        resource_server_url=AnyHttpUrl(MCP_URL),
        required_scopes=scopes,
        client_registration_options=ClientRegistrationOptions(enabled=True, valid_scopes=scopes, default_scopes=scopes),
        identity_assertion_enabled=identity_assertion_enabled,
    )
