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

from __future__ import annotations

import asyncio
import logging
import time
import warnings

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,  # noqa: A004 - Custom Pinecone exception, not builtin
    TimeoutError,  # noqa: A004 - Custom Pinecone exception, not builtin
)


logger = get_logger(__name__)


class AsyncHTTPClient(HTTPClientBase):
    """Asynchronous HTTP client with retries and rate limiting.

    The underlying httpx.AsyncClient is created lazily on the first async
    method call, ensuring it's initialized in the correct async context.
    This makes the client safe to instantiate in any context (including
    sync code) and use across different event loops.

    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)

        # Defer httpx.AsyncClient creation until first async call
        # This ensures it's created in the correct async context
        self._client: httpx.AsyncClient | None = None
        self._client_lock: asyncio.Lock | None = None
        self._closed: bool = False

    async def _ensure_client(self) -> httpx.AsyncClient:
        """Ensure httpx.AsyncClient is initialized in async context.

        Uses double-checked locking pattern to safely initialize the client
        when multiple concurrent async calls are made. This protects against
        race conditions within the same event loop, not across OS threads.

        Returns:
            Initialized httpx.AsyncClient instance.

        Raises:
            RuntimeError: If client has been explicitly closed via close()
        """
        # Check if client was explicitly closed
        if self._closed:
            raise RuntimeError("HTTP client has been closed. Create a new client to make requests.")

        # Fast path: client already initialized (no lock needed)
        if self._client is not None:
            return self._client

        # Initialize lock if needed (first time only)
        if self._client_lock is None:
            self._client_lock = asyncio.Lock()

        # Slow path: acquire lock and initialize client
        async with self._client_lock:
            # Double-check after acquiring lock
            if self._client is None:
                # keepalive_expiry ensures connections are closed after 5s of inactivity
                # This helps prevent SSL socket leaks in test environments
                self._client = httpx.AsyncClient(
                    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(),
                )

        return self._client

    async 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
            asyncio.CancelledError: If the operation is cancelled
        """
        # 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

        # Ensure client is initialized in async context
        client = await self._ensure_client()

        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 = await 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,
                    )
                    await asyncio.sleep(wait_time)
                    continue

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

                return response  # noqa: TRY300

            except asyncio.CancelledError:
                logger.debug("Request cancelled", method=method, url=url)
                raise

            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,
                    )
                    await asyncio.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,
                    )
                    await asyncio.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,
                    )
                    await asyncio.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

    async 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 await self.request("GET", url, **kwargs)

    async 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 await self.request("POST", url, **kwargs)

    async 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 await self.request("PUT", url, **kwargs)

    async 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 await self.request("DELETE", url, **kwargs)

    async 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 await self.request("PATCH", url, **kwargs)

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

        This method is idempotent - calling it multiple times is safe.
        """
        if self._client is not None:
            await self._client.aclose()
            self._client = None  # Make close() truly idempotent
        self._closed = True  # Mark as closed to prevent reuse

    async def __aenter__(self) -> AsyncHTTPClient:
        """Enter async context manager."""
        return self

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

    def __del__(self) -> None:
        """Warn if client wasn't properly closed."""
        try:
            if self._client is not None and not self._client.is_closed:
                warnings.warn(
                    f"Unclosed {self.__class__.__name__}. Use 'async with' or call 'await client.close()'",
                    ResourceWarning,
                    stacklevel=2,
                )
        except Exception:  # noqa: BLE001, S110
            # Ignore errors during cleanup (e.g., during interpreter shutdown)
            pass
