"""Base class for HTTP clients with shared logic."""

from __future__ import annotations

import platform
import random
import sys

import httpx

from pinecone import __version__
from pinecone._internal.constants import DEFAULT_TIMEOUT
from pinecone._internal.http.config import HTTPConfig
from pinecone._internal.logging import get_logger
from pinecone.exceptions import (
    APIError,
    BadRequestError,
    ConflictError,
    ForbiddenError,
    NotFoundError,
    RateLimitError,
    ServerError,
    UnauthorizedError,
)


logger = get_logger(__name__)


class HTTPClientBase:
    """Shared logic for sync and async HTTP clients.

    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):
        self._api_key = api_key
        self._config = config or HTTPConfig()

    def _default_headers(self) -> dict[str, str]:
        """Generate default HTTP headers."""
        return {
            "Api-Key": self._api_key,
            "User-Agent": self._user_agent(),
            "Content-Type": "application/json",
        }

    def _user_agent(self) -> str:
        """Generate User-Agent string.

        Format: pinecone-python-client/{version} python/{py_version} {os}/{os_version}
        """
        py_version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
        os_name = platform.system()
        os_version = platform.release()
        return f"pinecone-python-client/{__version__} python/{py_version} {os_name}/{os_version}"

    def _create_timeout(self) -> httpx.Timeout:
        """Convert timeout config to httpx.Timeout object.

        Returns:
            httpx.Timeout object with appropriate timeout values
        """
        if isinstance(self._config.timeout, dict):
            # For partial dicts, use DEFAULT_TIMEOUT as default for unspecified timeout types
            return httpx.Timeout(DEFAULT_TIMEOUT, **self._config.timeout)
        return httpx.Timeout(self._config.timeout)

    def _resolve_timeout_value(self, exception: httpx.TimeoutException) -> float:
        """Determine which timeout value to report based on exception type.

        Args:
            exception: The httpx timeout exception that was raised

        Returns:
            The timeout value in seconds that corresponds to the exception type
        """
        if isinstance(self._config.timeout, (int, float)):
            return float(self._config.timeout)

        # Timeout is a dict - check which specific timeout fired
        if isinstance(exception, httpx.ConnectTimeout):
            return float(self._config.timeout.get("connect", DEFAULT_TIMEOUT))
        if isinstance(exception, httpx.ReadTimeout):
            return float(self._config.timeout.get("read", DEFAULT_TIMEOUT))
        if isinstance(exception, httpx.WriteTimeout):
            return float(self._config.timeout.get("write", DEFAULT_TIMEOUT))
        if isinstance(exception, httpx.PoolTimeout):
            return float(self._config.timeout.get("pool", DEFAULT_TIMEOUT))

        # Generic timeout or unspecified timeout type
        return float(DEFAULT_TIMEOUT)

    def _backoff_time(self, attempt: int, response: httpx.Response | None = None) -> float:
        """Calculate exponential backoff time with jitter.

        Args:
            attempt: Current attempt number (0-indexed)
            response: Optional HTTP response to check for Retry-After header

        Returns:
            Number of seconds to wait before next retry
        """
        # Check for Retry-After header
        if response and "Retry-After" in response.headers:
            try:
                return float(response.headers["Retry-After"])
            except ValueError:
                pass

        # Exponential backoff with jitter to prevent thundering herd
        base_delay = min(
            self._config.retry_backoff_factor**attempt,
            self._config.retry_max_wait,
        )
        return base_delay * random.uniform(0.5, 1.0)  # noqa: S311

    def _raise_for_status(self, response: httpx.Response) -> None:  # noqa: PLR0912
        """Raise appropriate exception for error status codes.

        Args:
            response: HTTP response to check

        Raises:
            BadRequestError: For 400 status codes
            UnauthorizedError: For 401 status codes
            ForbiddenError: For 403 status codes
            NotFoundError: For 404 status codes
            ConflictError: For 409 status codes
            RateLimitError: For 429 status codes
            ServerError: For 5xx status codes
            APIError: For other error status codes
        """
        if response.is_success:
            return

        # Extract structured error data
        error_code = None
        message = None

        try:
            error_data = response.json()
            message = error_data.get("message")
            # Try to extract error code from various common patterns
            if isinstance(error_data.get("error"), dict):
                error_code = error_data["error"].get("code")
            elif "code" in error_data:
                error_code = error_data["code"]
            elif "error_code" in error_data:
                error_code = error_data["error_code"]
        except (ValueError, KeyError):
            pass

        # Fall back to response text if no message in JSON
        if not message:
            message = response.text or None

        # Extract metadata for debugging and support
        request_id = response.headers.get("X-Request-Id") or response.headers.get("x-request-id")
        url = str(response.url)
        method = response.request.method if response.request else None

        # Log error with full context
        logger.error(
            "HTTP error response",
            status=response.status_code,
            error_message=message,
            request_id=request_id,
            error_code=error_code,
            url=url,
            method=method,
        )

        # Build common kwargs for all exceptions
        kwargs = {
            "response_body": response.text,
            "request_id": request_id,
            "error_code": error_code,
            "url": url,
            "method": method,
        }

        # Map status code to specific exception with full context
        if response.status_code == 400:
            raise BadRequestError(message, **kwargs)
        if response.status_code == 401:
            raise UnauthorizedError(message=message, **kwargs)
        if response.status_code == 403:
            raise ForbiddenError(message, **kwargs)
        if response.status_code == 404:
            raise NotFoundError(message, **kwargs)
        if response.status_code == 409:
            raise ConflictError(message, **kwargs)
        if response.status_code == 429:
            retry_after = response.headers.get("Retry-After")
            raise RateLimitError(
                retry_after=int(retry_after) if retry_after else None,
                message=message,
                **kwargs,
            )
        if 500 <= response.status_code < 600:
            raise ServerError(message, status_code=response.status_code, **kwargs)

        # Ensure message is not None for generic APIError
        if message is None:
            message = f"HTTP {response.status_code}"
        raise APIError(message, response.status_code, **kwargs)
