"""Client factory for per-request Crustdata API client management.

This module provides a factory pattern for creating and caching CrustdataClient
instances. For HTTP transport, each request can use a different API token
(multi-tenant support). For STDIO transport, the token from environment
variables is used.
"""

from contextvars import ContextVar
from typing import Optional
from weakref import WeakValueDictionary

from .client import CrustdataClient
from .config import TransportType, get_token

# Context variable to store the current request's token
_request_token: ContextVar[Optional[str]] = ContextVar("request_token", default=None)


def set_request_token(token: str) -> None:
    """Set the token for the current request context."""
    _request_token.set(token)


def get_request_token() -> Optional[str]:
    """Get the token for the current request context."""
    return _request_token.get()


class ClientFactory:
    """Factory for creating/caching CrustdataClient instances per token.

    This factory supports two modes:
    - HTTP transport: Token extracted from request context (multi-tenant)
    - STDIO transport: Token from environment variable (single-tenant)

    Clients are cached using weak references for automatic cleanup when no
    longer referenced. Note: Clients become eligible for garbage collection
    immediately after each request ends; httpx handles underlying connection
    cleanup via __del__.
    """

    def __init__(self) -> None:
        """Initialize the client factory."""
        # Cache clients by token (weak refs for automatic cleanup)
        self._clients: WeakValueDictionary[str, CrustdataClient] = WeakValueDictionary()
        # Strong reference to STDIO client to keep it alive
        self._stdio_client: Optional[CrustdataClient] = None
        self._stdio_token: Optional[str] = None
        # Transport mode tracking for explicit error handling
        self._transport_mode: Optional[TransportType] = None

    def set_transport_mode(self, mode: TransportType) -> None:
        """Set the transport mode for explicit error handling.

        This should be called during server startup to configure how the factory
        handles missing tokens:
        - "stdio": Falls back to env var token (single-tenant)
        - "http": Requires per-request token, fails explicitly if missing

        Args:
            mode: Either "stdio" or "http"
        """
        self._transport_mode = mode

    def get_client_for_request(self) -> CrustdataClient:
        """Get a client for the current request.

        For HTTP transport: extracts token from request context via get_access_token()
        For STDIO transport: uses env var token (CRUSTDATA_TOKEN)

        Returns:
            CrustdataClient configured with the appropriate token.

        Raises:
            ValueError: If no token is available. For HTTP mode, this is a hard
                       failure (no fallback to env var). For STDIO mode or when
                       transport mode is not set, falls back to env var.
        """
        # Try to get token from HTTP request context first
        token = get_request_token()
        if token:
            return self._get_or_create_client(token)

        # In HTTP mode, require a per-request token — don't fall back to
        # the env var, which would silently use the operator's API key.
        if self._transport_mode == "http":
            raise ValueError(
                "No authentication token in request context. "
                "Please reconnect to the MCP server to re-authenticate."
            )

        # STDIO mode: fall back to env var token
        return self._get_stdio_client()

    def _get_or_create_client(self, token: str) -> CrustdataClient:
        """Get or create a client for the given token.

        Uses weak reference caching - if the client is still in use
        elsewhere, the cached instance is returned. Otherwise, a new
        client is created.

        Args:
            token: The Crustdata API token.

        Returns:
            CrustdataClient for the given token.
        """
        # Check cache first
        client = self._clients.get(token)
        if client is not None:
            return client

        # Create new client and cache it
        client = CrustdataClient(token)
        self._clients[token] = client
        return client

    def _get_stdio_client(self) -> CrustdataClient:
        """Get the STDIO mode client (uses env var token).

        The STDIO client is kept alive with a strong reference since
        there's only one token for the entire server lifetime.

        Returns:
            CrustdataClient configured with env var token.

        Raises:
            ValueError: If no token is set in environment variables.
        """
        token = get_token()
        if not token:
            raise ValueError(
                "No authentication token available. "
                "For STDIO transport, set CRUSTDATA_TOKEN environment variable. "
                "For HTTP transport, provide Authorization: Bearer <token> header."
            )

        # Reuse existing client if token hasn't changed
        if self._stdio_client is not None and self._stdio_token == token:
            return self._stdio_client

        # Create new client for this token
        self._stdio_client = CrustdataClient(token)
        self._stdio_token = token
        return self._stdio_client

    async def close_all(self) -> None:
        """Close all cached clients.

        Should be called during server shutdown to clean up connections.
        """
        # Close cached HTTP clients (weak refs may have been cleared)
        for client in list(self._clients.values()):
            await client.close()
        self._clients.clear()

        # Close STDIO client
        if self._stdio_client is not None:
            await self._stdio_client.close()
            self._stdio_client = None
            self._stdio_token = None


# Global singleton instance
_factory: Optional[ClientFactory] = None


def get_client_factory() -> ClientFactory:
    """Get the global client factory singleton.

    Returns:
        The shared ClientFactory instance.
    """
    global _factory
    if _factory is None:
        _factory = ClientFactory()
    return _factory


def get_client_for_request() -> CrustdataClient:
    """Convenience function to get client for current request.

    This is the main entry point for tools to get a client.
    It automatically handles both HTTP (per-request token) and
    STDIO (env var token) modes.

    Returns:
        CrustdataClient configured with the appropriate token.

    Raises:
        ValueError: If no token is available.

    Example:
        from ..client_factory import get_client_for_request

        @mcp.tool()
        async def my_tool(params: MyInput, ctx: Context) -> dict:
            client = get_client_for_request()
            result = await client.get("/api/endpoint")
            return result
    """
    return get_client_factory().get_client_for_request()


def reset_factory() -> None:
    """Reset the global factory singleton.

    Primarily used for testing to ensure clean state between tests.
    """
    global _factory
    _factory = None
