"""
Base HTTP client for all Lexilux API clients.

Provides common functionality:
- Direct HTTP requests (no connection pooling)
- Retry logic for failed requests
- Configurable timeouts
- Authentication handling
- Unified error handling
- Logging for debugging and monitoring
- Async support via httpx
"""

from __future__ import annotations

import logging
import time
from typing import TYPE_CHECKING, Any, AsyncIterator

import httpx
import requests

from lexilux.exceptions import (
    APIError,
    AuthenticationError,
    NotFoundError,
    RateLimitError,
    ServerError,
    ValidationError,
)
from lexilux.exceptions import (
    ConnectionError as LexiluxConnectionError,
)
from lexilux.exceptions import (
    TimeoutError as LexiluxTimeoutError,
)

if TYPE_CHECKING:
    pass

logger = logging.getLogger(__name__)


class BaseAPIClient:
    """
    Base API client with direct HTTP requests (no connection pooling).

    All API clients (Chat, Embed, Rerank) should inherit from this class
    to get consistent HTTP behavior and configuration.

    Each HTTP request creates a new connection and closes it after completion,
    ensuring no persistent connections are maintained.

    Attributes:
        base_url: Base URL for API requests (without trailing slash).
        api_key: API key for authentication (optional).
        timeout: Request timeout in seconds (float or tuple for connect/read).
        headers: Default headers for all requests.
        proxies: Proxy configuration (None means use environment variables).

    Examples:
        >>> client = BaseAPIClient(
        ...     base_url="https://api.example.com/v1",
        ...     api_key="sk-...",
        ...     connect_timeout_s=5,
        ...     read_timeout_s=30,
        ...     max_retries=2,
        ... )
    """

    def __init__(
        self,
        *,
        base_url: str,
        api_key: str | None = None,
        timeout_s: float = 60.0,
        connect_timeout_s: float | None = None,
        read_timeout_s: float | None = None,
        max_retries: int = 0,
        headers: dict[str, str] | None = None,
        proxies: dict[str, str] | None = None,
    ):
        """
        Initialize base API client.

        Args:
            base_url: Base URL for API requests (e.g., "https://api.openai.com/v1").
            api_key: API key for authentication (added to Authorization header).
            timeout_s: Default timeout for both connect and read (in seconds).
            connect_timeout_s: Connection timeout (overrides timeout_s if both set).
            read_timeout_s: Read timeout (overrides timeout_s if both set).
            max_retries: Maximum number of retries for failed requests (0 = disable).
            headers: Additional headers to include in all requests.
            proxies: Proxy configuration dict (e.g., {"http": "http://proxy:port"}).
                    If None, uses environment variables (HTTP_PROXY, HTTPS_PROXY).
                    To disable proxies, pass {}.
        """
        self.base_url = base_url.rstrip("/")
        self.api_key = api_key
        self.proxies = proxies
        self._max_retries = max_retries

        # Configure timeout
        if connect_timeout_s is not None and read_timeout_s is not None:
            self.timeout = (connect_timeout_s, read_timeout_s)
        else:
            self.timeout = timeout_s

        # Async client (lazy initialization)
        self._async_client: httpx.AsyncClient | None = None

        # Prepare headers
        self.headers = self._prepare_headers(headers, api_key)

    def _prepare_headers(
        self,
        headers: dict[str, str] | None,
        api_key: str | None,
    ) -> dict[str, str]:
        """
        Prepare request headers with authentication.

        Args:
            headers: Additional headers.
            api_key: API key for Bearer authentication.

        Returns:
            Headers dict with authentication and default headers.
        """
        headers = headers or {}
        headers.setdefault("Content-Type", "application/json")

        if api_key:
            headers.setdefault("Authorization", f"Bearer {api_key}")

        return headers

    def _handle_response_error(self, response: requests.Response) -> None:
        """
        Handle HTTP error responses and raise appropriate Lexilux exceptions.

        Args:
            response: The error response from the API.

        Raises:
            AuthenticationError: For 401 status codes.
            RateLimitError: For 429 status codes.
            NotFoundError: For 404 status codes.
            InvalidRequestError: For 400 status codes.
            ServerError: For 5xx status codes.
            APIError: For other error status codes.
        """
        status_code = response.status_code

        # Try to extract error message from response body
        error_message = f"HTTP {status_code}"
        try:
            error_data = response.json()
            if isinstance(error_data, dict):
                # OpenAI-style error
                if "error" in error_data:
                    error_info = error_data["error"]
                    if isinstance(error_info, dict):
                        error_message = error_info.get("message", error_message)
                    else:
                        error_message = str(error_info)
                else:
                    error_message = error_data.get("message", error_message)
        except (ValueError, KeyError):
            # Not JSON or no error field, use default message
            pass

        # Map status codes to specific exceptions
        if status_code == 401:
            raise AuthenticationError(error_message)
        elif status_code == 429:
            raise RateLimitError(error_message)
        elif status_code == 404:
            raise NotFoundError(error_message)
        elif status_code == 400:
            raise ValidationError(error_message)
        elif 500 <= status_code < 600:
            raise ServerError(error_message)
        else:
            raise APIError(
                message=error_message,
                status_code=status_code,
                code="http_error",
                retryable=False,
            )

    def _make_request(
        self,
        endpoint: str,
        payload: dict[str, Any],
    ) -> requests.Response:
        """
        Send POST request to API endpoint (creates new connection each time).

        Args:
            endpoint: API endpoint (e.g., "chat/completions").
            payload: Request body as dict.

        Returns:
            requests.Response object.

        Raises:
            LexiluxTimeoutError: On timeout.
            LexiluxConnectionError: On connection failure.
            AuthenticationError: On authentication failure.
            RateLimitError: On rate limit exceeded.
            APIError: On other API errors.
            ValidationError: On invalid input.
        """
        url = f"{self.base_url}/{endpoint}"
        start_time = time.time()

        logger.debug("Making POST request to %s", url)
        logger.debug("Request timeout: %s", self.timeout)

        # Direct request without session - connection closed after response
        try:
            response = requests.post(
                url,
                json=payload,
                timeout=self.timeout,
                headers=self.headers,
                proxies=self.proxies,
            )
        except requests.exceptions.Timeout as e:
            elapsed = time.time() - start_time
            logger.error("Request timeout after %.2fs: %s", elapsed, url)
            raise LexiluxTimeoutError(f"Request timeout: {e}") from e
        except requests.exceptions.ConnectionError as e:
            elapsed = time.time() - start_time
            logger.error("Connection failed after %.2fs: %s", elapsed, url)
            raise LexiluxConnectionError(f"Connection failed: {e}") from e
        except requests.exceptions.RequestException as e:
            elapsed = time.time() - start_time
            logger.error("Request failed after %.2fs: %s - %s", elapsed, url, e)
            # Generic requests error
            raise APIError(f"Request failed: {e}") from e
        finally:
            # Connection is automatically closed when response object is garbage collected
            # or when we explicitly close it
            pass

        elapsed = time.time() - start_time

        # Handle HTTP error status codes
        if not response.ok:
            logger.warning(
                "Request failed with status %d after %.2fs: %s",
                response.status_code,
                elapsed,
                url,
            )
            self._handle_response_error(response)

        logger.info(
            "Request completed in %.2fs with status %d: %s",
            elapsed,
            response.status_code,
            url,
        )

        # Close the response to release the connection
        response.close()

        return response

    def _make_streaming_request(
        self,
        endpoint: str,
        payload: dict[str, Any],
    ) -> requests.Response:
        """
        Send streaming POST request to API endpoint.

        Args:
            endpoint: API endpoint (e.g., "chat/completions").
            payload: Request body as dict.

        Returns:
            requests.Response object with stream=True.

        Raises:
            LexiluxTimeoutError: On timeout.
            LexiluxConnectionError: On connection failure.
            AuthenticationError: On authentication failure.
            RateLimitError: On rate limit exceeded.
            APIError: On other API errors.
            ValidationError: On invalid input.
        """
        url = f"{self.base_url}/{endpoint}"
        start_time = time.time()

        logger.debug("Making streaming POST request to %s", url)
        logger.debug("Request timeout: %s", self.timeout)

        # Direct request without session - connection managed by response lifecycle
        try:
            response = requests.post(
                url,
                json=payload,
                timeout=self.timeout,
                headers=self.headers,
                proxies=self.proxies,
                stream=True,
            )
        except requests.exceptions.Timeout as e:
            elapsed = time.time() - start_time
            logger.error("Streaming request timeout after %.2fs: %s", elapsed, url)
            raise LexiluxTimeoutError(f"Request timeout: {e}") from e
        except requests.exceptions.ConnectionError as e:
            elapsed = time.time() - start_time
            logger.error("Streaming connection failed after %.2fs: %s", elapsed, url)
            raise LexiluxConnectionError(f"Connection failed: {e}") from e
        except requests.exceptions.RequestException as e:
            elapsed = time.time() - start_time
            logger.error(
                "Streaming request failed after %.2fs: %s - %s", elapsed, url, e
            )
            # Generic requests error
            raise APIError(f"Request failed: {e}") from e

        elapsed = time.time() - start_time

        # Handle HTTP error status codes
        if not response.ok:
            logger.warning(
                "Streaming request failed with status %d after %.2fs: %s",
                response.status_code,
                elapsed,
                url,
            )
            self._handle_response_error(response)
            # Close the response on error
            response.close()

        logger.info(
            "Streaming request initiated in %.2fs with status %d: %s",
            elapsed,
            response.status_code,
            url,
        )

        # Note: Caller is responsible for closing the response when done streaming
        return response

    # =========================================================================
    # Async Methods (using httpx)
    # =========================================================================

    def _get_async_client(self) -> httpx.AsyncClient:
        """
        Get or create the async HTTP client (lazy initialization).

        Returns:
            httpx.AsyncClient instance (no connection pooling).
        """
        if self._async_client is None:
            # Configure timeout
            if isinstance(self.timeout, tuple):
                timeout = httpx.Timeout(
                    connect=self.timeout[0],
                    read=self.timeout[1],
                    write=self.timeout[1],
                    pool=self.timeout[0],
                )
            else:
                timeout = httpx.Timeout(self.timeout)

            # Create async client without connection limits
            self._async_client = httpx.AsyncClient(
                timeout=timeout,
                headers=self.headers,
                limits=httpx.Limits(max_connections=1, max_keepalive_connections=0),
            )

        return self._async_client

    def _handle_async_response_error(self, response: httpx.Response) -> None:
        """
        Handle HTTP error responses from httpx and raise appropriate Lexilux exceptions.

        Args:
            response: The error response from the API.

        Raises:
            AuthenticationError: For 401 status codes.
            RateLimitError: For 429 status codes.
            NotFoundError: For 404 status codes.
            ValidationError: For 400 status codes.
            ServerError: For 5xx status codes.
            APIError: For other error status codes.
        """
        status_code = response.status_code

        # Try to extract error message from response body
        error_message = f"HTTP {status_code}"
        try:
            error_data = response.json()
            if isinstance(error_data, dict):
                # OpenAI-style error
                if "error" in error_data:
                    error_info = error_data["error"]
                    if isinstance(error_info, dict):
                        error_message = error_info.get("message", error_message)
                    else:
                        error_message = str(error_info)
                else:
                    error_message = error_data.get("message", error_message)
        except (ValueError, KeyError):
            # Not JSON or no error field, use default message
            pass

        # Map status codes to specific exceptions
        if status_code == 401:
            raise AuthenticationError(error_message)
        elif status_code == 429:
            raise RateLimitError(error_message)
        elif status_code == 404:
            raise NotFoundError(error_message)
        elif status_code == 400:
            raise ValidationError(error_message)
        elif 500 <= status_code < 600:
            raise ServerError(error_message)
        else:
            raise APIError(
                message=error_message,
                status_code=status_code,
                code="http_error",
                retryable=False,
            )

    async def _amake_request(
        self,
        endpoint: str,
        payload: dict[str, Any],
    ) -> httpx.Response:
        """
        Send async POST request to API endpoint.

        Args:
            endpoint: API endpoint (e.g., "chat/completions").
            payload: Request body as dict.

        Returns:
            httpx.Response object.

        Raises:
            LexiluxTimeoutError: On timeout.
            LexiluxConnectionError: On connection failure.
            AuthenticationError: On authentication failure.
            RateLimitError: On rate limit exceeded.
            APIError: On other API errors.
            ValidationError: On invalid input.
        """
        url = f"{self.base_url}/{endpoint}"
        start_time = time.time()

        logger.debug("Making async POST request to %s", url)

        client = self._get_async_client()

        try:
            response = await client.post(
                url,
                json=payload,
            )
        except httpx.TimeoutException as e:
            elapsed = time.time() - start_time
            logger.error("Async request timeout after %.2fs: %s", elapsed, url)
            raise LexiluxTimeoutError(f"Request timeout: {e}") from e
        except httpx.ConnectError as e:
            elapsed = time.time() - start_time
            logger.error("Async connection failed after %.2fs: %s", elapsed, url)
            raise LexiluxConnectionError(f"Connection failed: {e}") from e
        except httpx.HTTPError as e:
            elapsed = time.time() - start_time
            logger.error("Async request failed after %.2fs: %s - %s", elapsed, url, e)
            raise APIError(f"Request failed: {e}") from e

        elapsed = time.time() - start_time

        # Handle HTTP error status codes
        if not response.is_success:
            logger.warning(
                "Async request failed with status %d after %.2fs: %s",
                response.status_code,
                elapsed,
                url,
            )
            self._handle_async_response_error(response)

        logger.info(
            "Async request completed in %.2fs with status %d: %s",
            elapsed,
            response.status_code,
            url,
        )
        return response

    async def _amake_streaming_request(
        self,
        endpoint: str,
        payload: dict[str, Any],
    ) -> AsyncIterator[str]:
        """
        Send async streaming POST request to API endpoint.

        Args:
            endpoint: API endpoint (e.g., "chat/completions").
            payload: Request body as dict.

        Yields:
            Lines from the SSE stream.

        Raises:
            LexiluxTimeoutError: On timeout.
            LexiluxConnectionError: On connection failure.
            AuthenticationError: On authentication failure.
            RateLimitError: On rate limit exceeded.
            APIError: On other API errors.
            ValidationError: On invalid input.
        """
        url = f"{self.base_url}/{endpoint}"
        start_time = time.time()

        logger.debug("Making async streaming POST request to %s", url)

        client = self._get_async_client()

        try:
            async with client.stream("POST", url, json=payload) as response:
                elapsed = time.time() - start_time

                # Handle HTTP error status codes
                if not response.is_success:
                    # Read the body to get error message
                    await response.aread()
                    logger.warning(
                        "Async streaming request failed with status %d after %.2fs: %s",
                        response.status_code,
                        elapsed,
                        url,
                    )
                    self._handle_async_response_error(response)

                logger.info(
                    "Async streaming request initiated in %.2fs with status %d: %s",
                    elapsed,
                    response.status_code,
                    url,
                )

                # Yield lines from the stream
                async for line in response.aiter_lines():
                    if line:
                        yield line

        except httpx.TimeoutException as e:
            elapsed = time.time() - start_time
            logger.error(
                "Async streaming request timeout after %.2fs: %s", elapsed, url
            )
            raise LexiluxTimeoutError(f"Request timeout: {e}") from e
        except httpx.ConnectError as e:
            elapsed = time.time() - start_time
            logger.error(
                "Async streaming connection failed after %.2fs: %s", elapsed, url
            )
            raise LexiluxConnectionError(f"Connection failed: {e}") from e
        except httpx.HTTPError as e:
            elapsed = time.time() - start_time
            logger.error(
                "Async streaming request failed after %.2fs: %s - %s", elapsed, url, e
            )
            raise APIError(f"Request failed: {e}") from e

    async def aclose(self) -> None:
        """Close the async client and release resources."""
        if self._async_client is not None:
            await self._async_client.aclose()
            self._async_client = None

    def close(self):
        """Close any resources (no-op for sync client with no pooling)."""
        pass

    def __enter__(self):
        """Context manager entry."""
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        """Context manager exit."""
        self.close()

    async def __aenter__(self):
        """Async context manager entry."""
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        """Async context manager exit."""
        await self.aclose()
