from __future__ import annotations

"""
Copyright (C) 2025 Luciano Guerche

This file is part of rabbitmq-mcp-server.

rabbitmq-mcp-server is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

rabbitmq-mcp-server is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License
along with rabbitmq-mcp-server. If not, see <https://www.gnu.org/licenses/>.
"""

"""HTTP client with connection pooling for RabbitMQ Management API."""

import asyncio
import base64
import hashlib
import ssl
import time
import uuid
from collections import deque
from pathlib import Path
from typing import Any

import httpx
import tomli as tomllib
from pydantic import BaseModel

from config.logging import get_logger
from rabbitmq_mcp_connection.connection.tls import TLSContextManager
from rabbitmq_mcp_connection.schemas.pool import PoolMetrics

logger = get_logger(__name__)


def _get_version_from_pyproject() -> str:
    """Extract version from pyproject.toml for User-Agent header (AC #6)."""
    try:
        pyproject_path = Path(__file__).parents[2] / "pyproject.toml"
        if pyproject_path.exists():
            with open(pyproject_path, "rb") as f:
                data = tomllib.load(f)
                return str(data.get("project", {}).get("version", "unknown"))
    except Exception:
        pass
    return "unknown"


DEFAULT_VERSION = _get_version_from_pyproject()

# Constants for HTTP client configuration
DEFAULT_TIMEOUT = 30
DEFAULT_POOL_SIZE = 5
DEFAULT_POOL_TIMEOUT = 10
MAX_REQUEST_HISTORY = 100


class PoolHealth(BaseModel):
    """Connection pool health metrics model (AC #7)."""

    size: int
    active: int
    idle: int
    evictions_last_hour: int = 0
    avg_connection_age_seconds: float = 0.0
    health_score: float = 1.0


class HTTPClientManager:
    """Async HTTP client manager with connection pooling for RabbitMQ Management API.

    Implements AC #1-8 requirements for HTTP request execution with:
    - Base URL configuration (AC #1)
    - HTTP Basic Auth (AC #2)
    - TLS/SSL support (AC #3, #4)
    - Configurable timeout (AC #5)
    - User-Agent header with version (AC #6)
    - Connection pooling (AC #7)
    - Fail-fast retry policy (AC #8)
    """

    def __init__(
        self,
        host: str,
        port: int,
        username: str,
        password: str,
        use_tls: bool = False,
        verify_cert: bool = True,
        ca_bundle: str | None = None,
        timeout: int = DEFAULT_TIMEOUT,
        pool_size: int = DEFAULT_POOL_SIZE,
        version: str | None = None,
        reconnection_manager: Any | None = None,
        pool_health_check_interval: int = 30,
    ) -> None:
        """Initialize HTTP client manager with connection pool.

        Args:
            host: RabbitMQ host (AC #1)
            port: HTTP Management API port (AC #1)
            username: Username for HTTP Basic Auth (AC #2)
            password: Password for HTTP Basic Auth (AC #2)
            use_tls: Enable TLS/SSL (AC #3)
            verify_cert: Enable certificate verification (AC #4)
            ca_bundle: Path to custom CA bundle (AC #4)
            timeout: Request timeout in seconds (AC #5)
            pool_size: Maximum connections in pool (AC #7)
            version: Server version for User-Agent (AC #6)
            reconnection_manager: Optional ReconnectionManager for state checking
            pool_health_check_interval: Proactive health check interval in seconds (AC #10 Story 2.6)
        """
        # Build base URL (AC #1)
        protocol = "https" if use_tls else "http"
        self.base_url = f"{protocol}://{host}:{port}/api"

        self.username = username
        self.password = password
        self.use_tls = use_tls
        self.verify_cert = verify_cert
        self.ca_bundle = ca_bundle
        self.timeout = timeout
        self.pool_size = pool_size
        self.version = version or DEFAULT_VERSION
        self._reconnection_manager = reconnection_manager
        self._pool_health_check_interval = pool_health_check_interval
        self._health_check_task: asyncio.Task[None] | None = None

        # Store configuration hash for change detection (Story 2.6 AC #9)
        self._config_hash = self._compute_config_hash()

        # Initialize SSL context if TLS enabled (AC #3, #4)
        ssl_context: ssl.SSLContext | None = None
        if use_tls:
            logger.info(
                "http.tls_enabled",
                message=f"Using TLS/SSL for HTTP connection (verify={verify_cert})",
                protocol="HTTP",
                verify_mode=verify_cert,
            )
            ssl_context = TLSContextManager.create_ssl_context(
                verify_cert=verify_cert,
                ca_bundle=ca_bundle,
            )
            if not verify_cert:
                logger.warning(
                    "http.tls_verification_disabled",
                    message="Certificate verification disabled for HTTP - insecure mode",
                    security_warning=True,
                )
        else:
            logger.warning(
                "http.tls_disabled",
                message="TLS disabled for HTTP - use only for development",
                security_warning=True,
            )

        # Configure connection pool (AC #7, Story 2.6 AC #1, #3)
        limits = httpx.Limits(
            max_connections=pool_size,
            max_keepalive_connections=pool_size,
            keepalive_expiry=60.0,  # AC #3 Story 2.6: Close idle connections after 60s
        )

        # Configure timeout (AC #5, Story 2.6 AC #2)
        timeout_config = httpx.Timeout(
            timeout=timeout,
            connect=timeout,
            pool=DEFAULT_POOL_TIMEOUT,  # AC #2 Story 2.6: 10s pool block timeout
        )

        # Set verify parameter based on TLS settings (AC #4)
        verify: bool | ssl.SSLContext = True
        if use_tls and ssl_context:
            verify = ssl_context
        elif not verify_cert:
            verify = False

        # Initialize httpx.AsyncClient with max_retries=0 for fail-fast (AC #8)
        transport = httpx.AsyncHTTPTransport(retries=0)

        self.client = httpx.AsyncClient(
            limits=limits,
            timeout=timeout_config,
            verify=verify,
            transport=transport,
        )

        # Connection pool metrics tracking (AC #7, Story 2.6 AC #5, #11)
        self._active_connections = 0
        self._total_requests = 0
        self._successful_requests = 0
        self._failed_requests = 0
        self._pool_exhaustion_count = 0
        self._evictions_count = 0
        self._evictions_timestamps: deque[float] = deque(maxlen=1000)
        self._request_history: deque[bool] = deque(maxlen=MAX_REQUEST_HISTORY)
        self._pool_start_time = time.time()

        logger.info(
            "http.client_initialized",
            base_url=self.base_url,
            use_tls=use_tls,
            verify_cert=verify_cert,
            timeout=timeout,
            pool_size=pool_size,
        )

    def _build_auth_header(self) -> str:
        """Build HTTP Basic Auth header value (AC #2).

        Returns:
            Base64-encoded "username:password" string
        """
        credentials = f"{self.username}:{self.password}"
        encoded = base64.b64encode(credentials.encode()).decode()
        return f"Basic {encoded}"

    async def request(
        self,
        method: str,
        path: str,
        params: dict[str, Any] | None = None,
        json: dict[str, Any] | None = None,
        timeout: int | None = None,
    ) -> httpx.Response:
        """Execute HTTP request with authentication, logging, and error handling.

        Args:
            method: HTTP method (GET, POST, PUT, PATCH, DELETE)
            path: API path (e.g., "/overview") - appended to base_url
            params: Optional query parameters
            json: Optional JSON request body
            timeout: Optional request-specific timeout (AC #5)

        Returns:
            httpx.Response object with status code and parsed content

        Raises:
            ConnectionError: If reconnection is in progress
            httpx.HTTPStatusError: HTTP error status code (4xx, 5xx) (AC #8)
            httpx.TimeoutException: Request exceeded timeout (AC #5)
            httpx.ConnectError: Connection failed (AC #8)
        """
        # Check if reconnection is in progress
        if self._reconnection_manager is not None:
            from rabbitmq_mcp_connection.schemas.connection import ConnectionState

            state = self._reconnection_manager.get_state()
            if state.state == ConnectionState.RECONNECTING.value:
                raise ConnectionError(
                    "Connection lost, please retry after reconnection"
                )

        # Generate correlation ID for request tracing (AC #6)
        correlation_id = str(uuid.uuid4())

        # Build full URL
        full_url = f"{self.base_url}{path}"

        # Build headers with authentication (AC #2) and User-Agent (AC #6)
        headers = {
            "Authorization": self._build_auth_header(),
            "User-Agent": f"rabbitmq-mcp-server/{self.version}",
            "X-Correlation-ID": correlation_id,
            "Accept": "application/json",
        }

        if json is not None:
            headers["Content-Type"] = "application/json"

        # Use request-specific timeout or default (AC #5)
        request_timeout = timeout if timeout is not None else self.timeout

        # Log request (sanitize URL to remove credentials)
        start_time = time.time()
        logger.debug(
            "http.request",
            method=method,
            path=path,
            correlation_id=correlation_id,
        )

        try:
            self._active_connections += 1
            self._total_requests += 1

            # Check if pool exhausted
            if self._active_connections >= self.pool_size:
                logger.warning(
                    "http.pool_exhausted",
                    active_connections=self._active_connections,
                    pool_size=self.pool_size,
                )

            response = await self.client.request(
                method=method,
                url=full_url,
                headers=headers,
                params=params,
                json=json,
                timeout=request_timeout,
            )

            # Track successful request
            duration_ms = (time.time() - start_time) * 1000
            self._successful_requests += 1
            self._request_history.append(True)

            # Check pool health score and alert if degraded (AC #11)
            await self._check_health_score()

            # Log response
            logger.debug(
                "http.response",
                status_code=response.status_code,
                correlation_id=correlation_id,
                duration_ms=f"{duration_ms:.2f}",
            )

            # Handle HTTP errors (AC #8, Story 2.6 AC #8)
            if response.status_code == 401:
                logger.error(
                    "http.unauthorized",
                    status_code=401,
                    correlation_id=correlation_id,
                    message="Authentication failed - check HTTP_USER and HTTP_PASSWORD",
                )
                response.raise_for_status()
            elif response.status_code == 404:
                logger.warning(
                    "http.not_found",
                    status_code=404,
                    path=path,
                    correlation_id=correlation_id,
                )
                # Return response instead of raising for 404 (graceful handling)
            elif response.status_code == 503:
                # AC #8: HTTP 503 Service Unavailable triggers eviction
                logger.error(
                    "http.service_unavailable",
                    status_code=503,
                    path=path,
                    correlation_id=correlation_id,
                )
                await self._evict_stale_connections("HTTP 503 Service Unavailable")
                response.raise_for_status()
            elif response.status_code >= 400:
                logger.error(
                    "http.error",
                    method=method,
                    path=path,
                    status_code=response.status_code,
                    correlation_id=correlation_id,
                    response_body=response.text[:500],  # Truncate to 500 chars
                )
                response.raise_for_status()

            return response

        except httpx.PoolTimeout:
            # AC #6: Pool exhaustion warning
            duration_ms = (time.time() - start_time) * 1000
            self._failed_requests += 1
            self._request_history.append(False)
            self._pool_exhaustion_count += 1
            logger.warning(
                "http.pool_exhausted",
                message="HTTP connection pool exhausted, waiting for available connection",
                active_connections=self._active_connections,
                pool_size=self.pool_size,
                wait_time=f"{duration_ms:.2f}ms",
                pool_exhaustion_count=self._pool_exhaustion_count,
                correlation_id=correlation_id,
            )
            raise

        except httpx.TimeoutException as e:
            # AC #8: Timeout triggers eviction
            duration_ms = (time.time() - start_time) * 1000
            self._failed_requests += 1
            self._request_history.append(False)
            logger.error(
                "http.timeout",
                method=method,
                path=path,
                timeout=request_timeout,
                duration_ms=f"{duration_ms:.2f}",
                correlation_id=correlation_id,
            )
            await self._evict_stale_connections(f"Timeout: {str(e)}")
            raise

        except (httpx.ConnectError, httpx.RemoteProtocolError) as e:
            # AC #8: Socket errors trigger eviction
            self._failed_requests += 1
            self._request_history.append(False)
            logger.error(
                "http.connection_error",
                method=method,
                url=full_url,
                correlation_id=correlation_id,
                error=str(e),
            )
            await self._evict_stale_connections(f"Socket error: {type(e).__name__}")
            raise

        except httpx.TransportError as e:
            # AC #8: SSL/TLS errors trigger eviction (AC #6: SSL certificate verification)
            self._failed_requests += 1
            self._request_history.append(False)
            error_msg = str(e).lower()

            if "ssl" in error_msg or "tls" in error_msg or "certificate" in error_msg:
                # Extract SSL error details (AC #6)
                from rabbitmq_mcp_connection.connection.exceptions import (
                    SSLCertificateVerificationError,
                )

                error_details = str(e)
                if "certificate verify failed" in error_msg:
                    if "expired" in error_msg:
                        details = "Certificate has expired"
                    elif "hostname" in error_msg or "mismatch" in error_msg:
                        details = "Hostname mismatch in certificate"
                    elif "self signed" in error_msg or "untrusted" in error_msg:
                        details = "Untrusted or self-signed certificate"
                    else:
                        details = error_details

                    logger.error(
                        "http.ssl_verification_failed",
                        method=method,
                        url=full_url,
                        correlation_id=correlation_id,
                        details=details,
                    )
                    await self._evict_stale_connections(
                        f"SSL certificate verification failed: {details}"
                    )
                    raise SSLCertificateVerificationError(
                        "SSL certificate verification failed for HTTP connection",
                        details=details,
                    ) from e
                else:
                    logger.error(
                        "http.ssl_error",
                        method=method,
                        url=full_url,
                        correlation_id=correlation_id,
                        error=error_details,
                    )
                    await self._evict_stale_connections(
                        f"SSL/TLS error: {error_details}"
                    )
            raise

        except httpx.HTTPStatusError:
            self._failed_requests += 1
            self._request_history.append(False)
            raise

        finally:
            self._active_connections -= 1

    async def close(self) -> None:
        """Close HTTP client and release connection pool resources.

        Should be called during graceful shutdown to release connections.
        """
        logger.info("http.client_closing", base_url=self.base_url)

        # Stop health checks
        await self.stop_health_checks()

        # Close client
        await self.client.aclose()
        logger.info("http.client_closed")

    async def _evict_stale_connections(self, reason: str) -> None:
        """Evict stale connections by closing and recreating httpx client (Story 2.6 AC #8).

        Args:
            reason: Reason for eviction (e.g., "HTTP 503", "timeout", "socket error")
        """
        logger.warning(
            "http.pool_eviction",
            reason=reason,
            evictions_count=self._evictions_count + 1,
            message=f"Stale connection detected and evicted: {reason}",
        )

        # Track eviction
        self._evictions_count += 1
        self._evictions_timestamps.append(time.time())

        # Close existing client
        await self.client.aclose()

        # Recreate client with same configuration (flush pool)
        ssl_context: ssl.SSLContext | None = None
        if self.use_tls:
            ssl_context = TLSContextManager.create_ssl_context(
                verify_cert=self.verify_cert,
                ca_bundle=self.ca_bundle,
            )

        limits = httpx.Limits(
            max_connections=self.pool_size,
            max_keepalive_connections=self.pool_size,
            keepalive_expiry=60.0,
        )

        timeout_config = httpx.Timeout(
            timeout=self.timeout,
            connect=self.timeout,
            pool=DEFAULT_POOL_TIMEOUT,
        )

        verify: bool | ssl.SSLContext = True
        if self.use_tls and ssl_context:
            verify = ssl_context
        elif not self.verify_cert:
            verify = False

        transport = httpx.AsyncHTTPTransport(retries=0)

        self.client = httpx.AsyncClient(
            limits=limits,
            timeout=timeout_config,
            verify=verify,
            transport=transport,
        )

        logger.info(
            "http.pool_flushed",
            reason=reason,
            message="Connection pool flushed and recreated",
        )

    async def start_health_checks(self) -> None:
        """Start proactive pool health check background task (Story 2.6 AC #10)."""
        if self._health_check_task is None or self._health_check_task.done():
            self._health_check_task = asyncio.create_task(self._run_health_checks())
            logger.info(
                "http.health_checks_started",
                interval=self._pool_health_check_interval,
                message=f"Proactive pool health checks started (interval: {self._pool_health_check_interval}s)",
            )

    async def stop_health_checks(self) -> None:
        """Stop proactive pool health check background task."""
        if self._health_check_task and not self._health_check_task.done():
            self._health_check_task.cancel()
            try:
                await self._health_check_task
            except asyncio.CancelledError:
                pass
            logger.info("http.health_checks_stopped")

    async def _run_health_checks(self) -> None:
        """Background task for proactive connection health validation (AC #10)."""
        while True:
            try:
                await asyncio.sleep(self._pool_health_check_interval)

                # Send lightweight GET /api/overview to validate connection liveness
                logger.debug(
                    "http.health_check_probe",
                    message="Running proactive pool health check",
                )

                try:
                    response = await self.client.get(
                        f"{self.base_url}/overview",
                        headers={
                            "Authorization": self._build_auth_header(),
                            "User-Agent": f"rabbitmq-mcp-server/{self.version}",
                        },
                        timeout=self.timeout,
                    )

                    if response.status_code == 200:
                        logger.debug(
                            "http.health_check_success",
                            message="Pool health check passed",
                        )
                    else:
                        logger.warning(
                            "http.health_check_failed",
                            status_code=response.status_code,
                            message="Pool health check failed, flushing connection pool",
                        )
                        await self._evict_stale_connections(
                            f"Health check failed: HTTP {response.status_code}"
                        )

                except Exception as e:
                    logger.warning(
                        "http.health_check_error",
                        error=str(e),
                        message="Proactive pool health check failed, flushing connection pool",
                    )
                    await self._evict_stale_connections(f"Health check error: {str(e)}")

            except asyncio.CancelledError:
                logger.info("http.health_checks_cancelled")
                break
            except Exception as e:
                logger.error(
                    "http.health_check_loop_error",
                    error=str(e),
                    message="Error in health check loop, continuing...",
                )

    def _compute_config_hash(self) -> str:
        """Compute hash of HTTP configuration for change detection (AC #9)."""
        config_str = f"{self.base_url}|{self.username}|{self.password}|{self.use_tls}|{self.verify_cert}|{self.ca_bundle}"
        return hashlib.sha256(config_str.encode()).hexdigest()

    async def _check_health_score(self) -> None:
        """Check pool health score and alert if degraded (AC #11)."""
        if len(self._request_history) < 10:
            return  # Need at least 10 requests for meaningful health score

        health_score = sum(self._request_history) / len(self._request_history)

        if health_score < 0.75:
            # Critical: Health score below 0.75 - trigger reconnection
            logger.error(
                "http.pool_health_critical",
                health_score=f"{health_score:.2f}",
                total_requests=self._total_requests,
                successful_requests=self._successful_requests,
                failed_requests=self._failed_requests,
                message=f"Pool health degraded: score={health_score:.2f}, triggering reconnection",
            )
            # Trigger reconnection via ReconnectionManager (circuit breaker pattern)
            if self._reconnection_manager:
                await self._reconnection_manager.handle_connection_loss()
        elif health_score < 0.90:
            # Warning: Health score below 0.90
            logger.warning(
                "http.pool_health_degraded",
                health_score=f"{health_score:.2f}",
                total_requests=self._total_requests,
                successful_requests=self._successful_requests,
                failed_requests=self._failed_requests,
                message=f"Pool health degraded: score={health_score:.2f}, consider increasing pool size or investigating connection issues",
            )

    async def reload_on_config_change(
        self,
        host: str,
        port: int,
        username: str,
        password: str,
        use_tls: bool,
        verify_cert: bool,
        ca_bundle: str | None,
    ) -> bool:
        """Check for configuration changes and reload pool if changed (AC #9).

        Args:
            host: New RabbitMQ host
            port: New HTTP Management API port
            username: New username
            password: New password
            use_tls: New TLS setting
            verify_cert: New cert verification setting
            ca_bundle: New CA bundle path

        Returns:
            True if configuration changed and pool was flushed, False otherwise
        """
        # Build new config string and compute hash
        protocol = "https" if use_tls else "http"
        new_base_url = f"{protocol}://{host}:{port}/api"
        new_config_str = (
            f"{new_base_url}|{username}|{password}|{use_tls}|{verify_cert}|{ca_bundle}"
        )
        new_hash = hashlib.sha256(new_config_str.encode()).hexdigest()

        # Check if configuration changed
        if new_hash == self._config_hash:
            return False

        logger.info(
            "http.config_changed",
            old_base_url=self.base_url,
            new_base_url=new_base_url,
            message="Configuration changed, flushing HTTP connection pool and reconnecting",
        )

        # Update configuration
        self.base_url = new_base_url
        self.username = username
        self.password = password
        self.use_tls = use_tls
        self.verify_cert = verify_cert
        self.ca_bundle = ca_bundle
        self._config_hash = new_hash

        # Flush pool by evicting all connections
        await self._evict_stale_connections("Configuration change")

        return True

    def get_pool_status(self) -> PoolMetrics:
        """Get connection pool health metrics (Story 2.6 AC #5, #7, #11).

        Returns:
            PoolMetrics model with comprehensive health metrics
        """
        # Calculate health score from request history (last 100 requests)
        if len(self._request_history) > 0:
            health_score = sum(self._request_history) / len(self._request_history)
        else:
            health_score = 1.0

        # Calculate evictions in last hour
        current_time = time.time()
        one_hour_ago = current_time - 3600
        evictions_last_hour = sum(
            1 for ts in self._evictions_timestamps if ts >= one_hour_ago
        )

        return PoolMetrics(
            active_connections=self._active_connections,
            pool_size=self.pool_size,
            wait_time=None,  # Not tracked yet (would need timing instrumentation)
            total_requests=self._total_requests,
            successful_requests=self._successful_requests,
            failed_requests=self._failed_requests,
            evictions_last_hour=evictions_last_hour,
            pool_exhaustion_count=self._pool_exhaustion_count,
            health_score=health_score,
        )


# Backward compatibility: Keep HTTPClient as alias
class HTTPClient(HTTPClientManager):
    """Legacy HTTPClient class for backward compatibility.

    Use HTTPClientManager for new code.
    """

    def __init__(
        self,
        base_url: str,
        username: str,
        password: str,
        verify_ssl: bool = True,
        timeout: int = DEFAULT_TIMEOUT,
        pool_size: int = DEFAULT_POOL_SIZE,
    ) -> None:
        """Initialize HTTP client with legacy interface.

        Args:
            base_url: Base URL (e.g., http://localhost:15672)
            username: Username for HTTP Basic Auth
            password: Password for HTTP Basic Auth
            verify_ssl: Enable TLS/SSL certificate verification
            timeout: Request timeout in seconds
            pool_size: Maximum connections in pool
        """
        # Parse base_url to extract host, port, use_tls
        base_url = base_url.rstrip("/")
        use_tls = base_url.startswith("https://")

        # Remove protocol
        url_without_protocol = base_url.replace("https://", "").replace("http://", "")

        # Split host:port
        if ":" in url_without_protocol:
            host, port_str = url_without_protocol.split(":", 1)
            port = int(port_str)
        else:
            host = url_without_protocol
            port = 443 if use_tls else 80

        super().__init__(
            host=host,
            port=port,
            username=username,
            password=password,
            use_tls=use_tls,
            verify_cert=verify_ssl,
            timeout=timeout,
            pool_size=pool_size,
        )


__all__ = [
    "HTTPClientManager",
    "HTTPClient",
    "PoolMetrics",
    "DEFAULT_TIMEOUT",
    "DEFAULT_POOL_SIZE",
]
