# -*- coding: utf-8 -*-
"""Location: ./mcpgateway/services/http_client_service.py
Copyright 2026
SPDX-License-Identifier: Apache-2.0
Authors: Mihai Criveti

Shared HTTP Client Service.

This module provides a singleton httpx.AsyncClient that is shared across all
services in ContextForge. Using a shared client instead of per-request clients
provides significant performance benefits:

- Connection reuse: Avoids TCP handshake and TLS negotiation per request
- Connection pooling: Manages concurrent connections efficiently
- Configurable limits: Prevents connection exhaustion under high load

Performance benchmarks show ~20x throughput improvement vs per-request clients.

Usage:
    from mcpgateway.services.http_client_service import get_http_client

    # Get the shared client for making requests
    client = await get_http_client()
    response = await client.get("https://example.com/api")

    # For requests needing isolated TLS/auth context (rare):
    async with get_isolated_http_client(verify=custom_ssl_context) as client:
        response = await client.get("https://example.com/api")

Configuration (environment variables):
    HTTPX_MAX_CONNECTIONS: Maximum concurrent connections (default: 200)
    HTTPX_MAX_KEEPALIVE_CONNECTIONS: Idle connections to retain (default: 100)
    HTTPX_KEEPALIVE_EXPIRY: Idle connection timeout in seconds (default: 30)
    HTTPX_CONNECT_TIMEOUT: Connection timeout in seconds (default: 5)
    HTTPX_READ_TIMEOUT: Read timeout in seconds (default: 120, high for slow MCP tools)
    HTTPX_WRITE_TIMEOUT: Write timeout in seconds (default: 30)
    HTTPX_POOL_TIMEOUT: Pool wait timeout in seconds (default: 10)
    HTTPX_HTTP2_ENABLED: Enable HTTP/2 (default: false)
    HTTPX_ADMIN_READ_TIMEOUT: Read timeout for admin operations (default: 30)
"""

# Future
from __future__ import annotations

# Standard
import asyncio
from contextlib import asynccontextmanager
import logging
import ssl
from typing import AsyncIterator, Optional

# Third-Party
import httpx

logger = logging.getLogger(__name__)


class SharedHttpClient:
    """
    Singleton wrapper for a shared httpx.AsyncClient.

    All callers share the same client instance and its internal connection pool.
    This avoids the overhead of creating new clients per request while providing
    configurable connection limits.

    The client is initialized lazily on first access and shut down during
    application shutdown via the FastAPI lifespan.
    """

    _instance: Optional["SharedHttpClient"] = None
    _lock: asyncio.Lock = asyncio.Lock()

    def __init__(self) -> None:
        """Initialize the SharedHttpClient wrapper (not the actual client)."""
        self._client: Optional[httpx.AsyncClient] = None
        self._initialized: bool = False
        self._limits: Optional[httpx.Limits] = None

    @classmethod
    async def get_instance(cls) -> "SharedHttpClient":
        """
        Get or create the singleton instance.

        Thread-safe initialization using asyncio.Lock.

        Returns:
            SharedHttpClient: The singleton instance with initialized client.
        """
        if cls._instance is None or not cls._instance._initialized:  # pylint: disable=protected-access
            async with cls._lock:
                if cls._instance is None:
                    cls._instance = cls()
                if not cls._instance._initialized:  # pylint: disable=protected-access
                    await cls._instance._initialize()  # pylint: disable=protected-access
        return cls._instance

    async def _initialize(self) -> None:
        """
        Initialize the HTTP client with configured limits and timeouts.

        Reads configuration from settings and creates the shared AsyncClient.
        """
        # Import here to avoid circular imports
        # First-Party
        from mcpgateway.config import settings  # pylint: disable=import-outside-toplevel

        self._limits = httpx.Limits(
            max_connections=settings.httpx_max_connections,
            max_keepalive_connections=settings.httpx_max_keepalive_connections,
            keepalive_expiry=settings.httpx_keepalive_expiry,
        )

        timeout = httpx.Timeout(
            connect=settings.httpx_connect_timeout,
            read=settings.httpx_read_timeout,
            write=settings.httpx_write_timeout,
            pool=settings.httpx_pool_timeout,
        )

        self._client = httpx.AsyncClient(
            limits=self._limits,
            timeout=timeout,
            http2=settings.httpx_http2_enabled,
            follow_redirects=True,
            verify=not settings.skip_ssl_verify,
        )
        self._initialized = True

        logger.info(
            "Shared HTTP client initialized: max_connections=%d, keepalive=%d, http2=%s",
            settings.httpx_max_connections,
            settings.httpx_max_keepalive_connections,
            settings.httpx_http2_enabled,
        )

    @property
    def client(self) -> httpx.AsyncClient:
        """
        Get the shared HTTP client.

        Returns:
            httpx.AsyncClient: The shared client instance.

        Raises:
            RuntimeError: If the client has not been initialized.
        """
        if self._client is None:
            raise RuntimeError("SharedHttpClient not initialized. Call get_instance() first.")
        return self._client

    def get_pool_stats(self) -> dict[str, int]:
        """
        Get connection pool configuration limits.

        Returns:
            dict: Connection pool limit metrics:
                - max_connections: Maximum allowed connections
                - max_keepalive: Maximum idle connections to retain

        Note:
            Returns empty dict if client is not initialized.
            Actual connection counts are not exposed by httpx.
        """
        if self._client is None:
            return {}

        # Return pool configuration limits (actual connection counts not exposed by httpx)
        if self._limits is not None:
            return {
                "max_connections": self._limits.max_connections,
                "max_keepalive": self._limits.max_keepalive_connections,
            }

        # Fallback if _limits somehow not set (should never happen)
        return {}

    async def close(self) -> None:
        """Close the shared HTTP client and release all connections."""
        if self._client:
            await self._client.aclose()
            self._client = None
            self._initialized = False
            self._limits = None
            logger.info("Shared HTTP client closed")

    @classmethod
    async def shutdown(cls) -> None:
        """Shutdown the singleton instance during application shutdown."""
        if cls._instance:
            await cls._instance.close()
            cls._instance = None


# Module-level convenience functions


async def get_http_client() -> httpx.AsyncClient:
    """
    Get the shared HTTP client for making requests.

    This is the primary way to obtain an HTTP client in the application.
    The client is shared across all callers and manages connection pooling
    automatically.

    Returns:
        httpx.AsyncClient: The shared client instance.

    Example:
        client = await get_http_client()
        response = await client.post(url, json=data, headers={"X-Custom": "value"})
    """
    instance = await SharedHttpClient.get_instance()
    return instance.client


def get_http_limits() -> httpx.Limits:
    """
    Get configured HTTPX Limits for use with custom clients.

    Use this when you need to create a separate client (e.g., for SSE/streaming
    with mcp-sdk) but want to use the same connection limits as the shared client.

    Returns:
        httpx.Limits: Configured limits from settings.
    """
    # First-Party
    from mcpgateway.config import settings  # pylint: disable=import-outside-toplevel

    return httpx.Limits(
        max_connections=settings.httpx_max_connections,
        max_keepalive_connections=settings.httpx_max_keepalive_connections,
        keepalive_expiry=settings.httpx_keepalive_expiry,
    )


def get_http_timeout(
    read_timeout: Optional[float] = None,
    connect_timeout: Optional[float] = None,
    write_timeout: Optional[float] = None,
    pool_timeout: Optional[float] = None,
) -> httpx.Timeout:
    """
    Get configured HTTPX Timeout for use with custom clients.

    Allows overriding specific timeout values while using defaults for others.

    Args:
        read_timeout: Override for read timeout (seconds).
        connect_timeout: Override for connect timeout (seconds).
        write_timeout: Override for write timeout (seconds).
        pool_timeout: Override for pool timeout (seconds).

    Returns:
        httpx.Timeout: Configured timeout from settings with optional overrides.
    """
    # First-Party
    from mcpgateway.config import settings  # pylint: disable=import-outside-toplevel

    return httpx.Timeout(
        connect=connect_timeout if connect_timeout is not None else settings.httpx_connect_timeout,
        read=read_timeout if read_timeout is not None else settings.httpx_read_timeout,
        write=write_timeout if write_timeout is not None else settings.httpx_write_timeout,
        pool=pool_timeout if pool_timeout is not None else settings.httpx_pool_timeout,
    )


def get_admin_timeout() -> httpx.Timeout:
    """
    Get a shorter timeout for admin UI operations.

    Use this for operations where fast failure is preferred over waiting for slow
    upstreams (e.g., model list fetching, health checks, admin page data).

    Returns:
        httpx.Timeout: Timeout configured for admin operations (shorter read timeout).
    """
    # First-Party
    from mcpgateway.config import settings  # pylint: disable=import-outside-toplevel

    return httpx.Timeout(
        connect=settings.httpx_connect_timeout,
        read=settings.httpx_admin_read_timeout,
        write=settings.httpx_write_timeout,
        pool=settings.httpx_pool_timeout,
    )


def get_default_verify() -> bool:
    """
    Get the default SSL verification setting based on skip_ssl_verify config.

    Use this when creating factory clients that should respect the global
    skip_ssl_verify setting when no custom SSL context is provided.

    Returns:
        bool: True if SSL should be verified, False if skip_ssl_verify is enabled.
    """
    # First-Party
    from mcpgateway.config import settings  # pylint: disable=import-outside-toplevel

    return not settings.skip_ssl_verify


@asynccontextmanager
async def get_isolated_http_client(
    timeout: Optional[float] = None,
    headers: Optional[dict[str, str]] = None,
    verify: Optional[bool | ssl.SSLContext] = None,
    auth: Optional[httpx.Auth] = None,
    http2: Optional[bool] = None,
    connect_timeout: Optional[float] = None,
    write_timeout: Optional[float] = None,
    pool_timeout: Optional[float] = None,
    follow_redirects: bool = True,
) -> AsyncIterator[httpx.AsyncClient]:
    """
    Create an isolated HTTP client with custom settings.

    WARNING: This creates a NEW client with its own connection pool.
    Connections are NOT shared with the singleton. Use sparingly for cases
    requiring custom TLS context or authentication that can't use the shared client.

    For most cases, prefer get_http_client() which reuses connections.

    Args:
        timeout: Optional read timeout override (seconds).
        headers: Optional default headers for all requests.
        verify: SSL verification setting (True, False, SSLContext, or None).
                If None, uses skip_ssl_verify setting to determine default.
        auth: Optional authentication handler.
        http2: Override HTTP/2 setting (default: use settings).
        connect_timeout: Optional connect timeout override (seconds).
        write_timeout: Optional write timeout override (seconds).
        pool_timeout: Optional pool timeout override (seconds).
        follow_redirects: Whether to follow HTTP redirects (default: True).
                          Set to False for SSRF-sensitive requests.

    Yields:
        httpx.AsyncClient: A new isolated client instance.

    Example:
        async with get_isolated_http_client(verify=custom_ssl_context) as client:
            response = await client.get("https://example.com/api")
    """
    # First-Party
    from mcpgateway.config import settings  # pylint: disable=import-outside-toplevel

    limits = get_http_limits()
    timeout_config = get_http_timeout(
        read_timeout=timeout,
        connect_timeout=connect_timeout,
        write_timeout=write_timeout,
        pool_timeout=pool_timeout,
    )

    # Use skip_ssl_verify setting if no explicit verify value provided
    effective_verify: bool | ssl.SSLContext = verify if verify is not None else get_default_verify()

    async with httpx.AsyncClient(
        limits=limits,
        timeout=timeout_config,
        headers=headers,
        verify=effective_verify,
        auth=auth,
        http2=http2 if http2 is not None else settings.httpx_http2_enabled,
        follow_redirects=follow_redirects,
    ) as client:
        yield client
