"""Synchronous HTTP client with retries and connection pooling."""

from __future__ import annotations

import logging
import time

from typing import TYPE_CHECKING, Any

import httpx


if TYPE_CHECKING:
    from types import TracebackType

    from pinecone._internal.http.config import HTTPConfig

from pinecone._internal.http.base import HTTPClientBase
from pinecone._internal.logging import get_logger
from pinecone.exceptions import (
    APIConnectionError,
    ConnectionError,
    TimeoutError,
)


logger = get_logger(__name__)


class HTTPClient(HTTPClientBase):
    """Synchronous HTTP client with retries and rate limiting.

    Args:
        api_key: Pinecone API key for authentication
        config: HTTP client configuration (uses defaults if not provided)
    """

    def __init__(self, api_key: str, config: HTTPConfig | None = None):
        super().__init__(api_key, config)

        # Create httpx client with connection pooling
        # keepalive_expiry ensures connections are closed after 5s of inactivity
        # This helps prevent SSL socket leaks in test environments
        self._client: httpx.Client | None = httpx.Client(
            http2=self._config.http2,
            timeout=self._create_timeout(),
            limits=httpx.Limits(
                max_connections=self._config.max_connections,
                max_keepalive_connections=self._config.max_keepalive_connections,
                keepalive_expiry=5.0,  # Close idle connections after 5 seconds
            ),
            headers=self._default_headers(),
        )

    def request(self, method: str, url: str, **kwargs: Any) -> httpx.Response:
        """Make HTTP request with automatic retries.

        Args:
            method: HTTP method (GET, POST, PUT, DELETE, PATCH)
            url: URL to request
            **kwargs: Additional arguments passed to httpx

        Returns:
            HTTP response

        Raises:
            APIConnectionError: If connection fails after all retries
            TimeoutError: If request times out
            APIError: If server returns an error response
            RuntimeError: If client has been closed
        """
        if self._client is None:
            raise RuntimeError("HTTP client has been closed. Create a new client to make requests.")

        # Merge custom headers with default headers (custom headers take precedence)
        # This ensures Api-Key and User-Agent are always sent
        if "headers" in kwargs:
            merged_headers = {**self._default_headers(), **kwargs["headers"]}
            kwargs["headers"] = merged_headers

        last_error: Exception | None = None

        for attempt in range(self._config.max_retries):
            try:
                # Log request (with guard for hot-path performance)
                if logger._logger.isEnabledFor(logging.DEBUG):
                    # Include request body for debugging
                    request_body = None
                    if "content" in kwargs:
                        content = kwargs["content"]
                        if isinstance(content, bytes):
                            request_body = content.decode("utf-8")[:1000]
                        elif isinstance(content, str):
                            request_body = content[:1000]
                        if request_body and len(str(kwargs.get("content", ""))) > 1000:
                            request_body += "... (truncated)"

                    logger.debug(
                        "HTTP request",
                        method=method,
                        url=url,
                        attempt=attempt + 1,
                        body=request_body,
                    )

                # Make request
                start = time.perf_counter()
                response = self._client.request(method, url, **kwargs)
                duration = time.perf_counter() - start

                # Log response (with guard for hot-path performance)
                if logger._logger.isEnabledFor(logging.DEBUG):
                    # Include response body for debugging
                    response_body = response.text[:1000] if response.text else ""
                    if len(response.text) > 1000:
                        response_body += "... (truncated)"

                    logger.debug(
                        "HTTP response",
                        method=method,
                        url=url,
                        status=response.status_code,
                        duration=duration,
                        body=response_body,
                    )

                # Check if should retry based on status code
                if (
                    response.status_code in self._config.retry_statuses
                    and attempt < self._config.max_retries - 1
                ):
                    wait_time = self._backoff_time(attempt, response)
                    logger.info(
                        f"Retrying after {wait_time}s",
                        status=response.status_code,
                        attempt=attempt + 1,
                    )
                    time.sleep(wait_time)
                    continue

                # Raise for error status codes
                self._raise_for_status(response)

                return response  # noqa: TRY300

            except httpx.TimeoutException as e:
                last_error = e
                if attempt < self._config.max_retries - 1:
                    wait_time = self._backoff_time(attempt)
                    logger.warning(
                        f"Request timed out, retrying after {wait_time}s",
                        error=str(e),
                        attempt=attempt + 1,
                    )
                    time.sleep(wait_time)
                    continue
                logger.exception(
                    "Request timed out after all retries",
                    timeout=self._config.timeout,
                    attempts=attempt + 1,
                )
                raise TimeoutError(timeout=self._resolve_timeout_value(e)) from e

            except httpx.ConnectError as e:
                last_error = e
                if attempt < self._config.max_retries - 1:
                    wait_time = self._backoff_time(attempt)
                    logger.warning(
                        f"Connection failed, retrying after {wait_time}s",
                        error=str(e),
                        attempt=attempt + 1,
                    )
                    time.sleep(wait_time)
                    continue
                logger.exception(
                    "Connection failed after all retries",
                    url=url,
                    attempts=attempt + 1,
                )
                raise ConnectionError(f"Failed to connect to {url}") from e

            except httpx.RequestError as e:
                last_error = e
                if attempt < self._config.max_retries - 1:
                    wait_time = self._backoff_time(attempt)
                    logger.warning(
                        f"Request failed, retrying after {wait_time}s",
                        error=str(e),
                        attempt=attempt + 1,
                    )
                    time.sleep(wait_time)
                    continue

        # All retries exhausted
        logger.error(
            "Request failed after all retries",
            method=method,
            url=url,
            attempts=self._config.max_retries,
        )
        raise APIConnectionError(
            f"Request failed after {self._config.max_retries} attempts"
        ) from last_error

    def get(self, url: str, **kwargs: Any) -> httpx.Response:
        """Make GET request.

        Args:
            url: URL to request
            **kwargs: Additional arguments passed to httpx

        Returns:
            HTTP response
        """
        return self.request("GET", url, **kwargs)

    def post(self, url: str, **kwargs: Any) -> httpx.Response:
        """Make POST request.

        Args:
            url: URL to request
            **kwargs: Additional arguments passed to httpx

        Returns:
            HTTP response
        """
        return self.request("POST", url, **kwargs)

    def put(self, url: str, **kwargs: Any) -> httpx.Response:
        """Make PUT request.

        Args:
            url: URL to request
            **kwargs: Additional arguments passed to httpx

        Returns:
            HTTP response
        """
        return self.request("PUT", url, **kwargs)

    def delete(self, url: str, **kwargs: Any) -> httpx.Response:
        """Make DELETE request.

        Args:
            url: URL to request
            **kwargs: Additional arguments passed to httpx

        Returns:
            HTTP response
        """
        return self.request("DELETE", url, **kwargs)

    def patch(self, url: str, **kwargs: Any) -> httpx.Response:
        """Make PATCH request.

        Args:
            url: URL to request
            **kwargs: Additional arguments passed to httpx

        Returns:
            HTTP response
        """
        return self.request("PATCH", url, **kwargs)

    def close(self) -> None:
        """Close HTTP client and clean up resources.

        This method ensures all connections are properly closed and resources
        are released. It's important to call this method when done with the
        client to avoid resource leaks, especially in test environments.

        This method is idempotent - calling it multiple times is safe.
        """
        # Explicitly close the client to ensure all connections are released
        # This is especially important with HTTP/2 and connection pooling
        if hasattr(self, "_client") and self._client is not None:
            self._client.close()
            self._client = None  # Make close() truly idempotent

    def __enter__(self) -> HTTPClient:
        """Enter context manager."""
        return self

    def __exit__(
        self,
        exc_type: type[BaseException] | None,
        exc_val: BaseException | None,
        exc_tb: TracebackType | None,
    ) -> None:
        """Exit context manager and clean up resources."""
        self.close()
