"""Core HTTP client for the Sika SDK.

``SikaHTTPClient`` wraps httpx to provide connection pooling, automatic
retry with exponential backoff, configurable timeouts, and API key
injection. All SDK resource classes use this client for HTTP operations.

Proxy support: httpx respects the standard ``HTTPS_PROXY``,
``HTTP_PROXY``, and ``NO_PROXY`` environment variables. No additional
configuration is needed for enterprise proxy traversal.

TLS: Uses httpx defaults for TLS. Certificate validation against the
system CA bundle is enabled by default.
"""

from __future__ import annotations

import time
from typing import Any

import httpx

from sika._errors import SikaError, parse_error_response
from sika._mask import mask_api_key
from sika._version import __version__

BASE_URLS: dict[str, str] = {
    "production": "https://api.sikasecurity.com",
    "dev": "https://dev.api.sikasecurity.com",
}

_DEFAULT_TIMEOUT = httpx.Timeout(30.0, connect=10.0)
_DEFAULT_MAX_RETRIES = 3
_BACKOFF_BASE = 1.0
_BACKOFF_FACTOR = 2.0
_RETRYABLE_STATUS_CODES = frozenset({429, 500, 502, 503, 504})


class SikaHTTPClient:
    """Low-level HTTP client for the Sika API.

    Provides ``get``, ``post``, and ``delete`` methods that return parsed
    JSON responses. Non-2xx responses raise typed exceptions from
    ``sika._errors``.

    Retry behaviour:
        - 5xx and 429 responses are retried up to ``max_retries`` times
          with exponential backoff (1s, 2s, 4s, ...).
        - If the server sends a ``Retry-After`` header, its value is
          used instead of the computed backoff.
        - 4xx responses (except 429) are never retried.

    Args:
        base_url: API base URL. Use :data:`BASE_URLS` values or a custom URL.
        api_key: API key sent as the ``X-Api-Key`` header.
        timeout: httpx ``Timeout`` instance. Defaults to 10s connect, 30s
            read, 30s overall.
        max_retries: Maximum number of retry attempts for retryable errors.
            Defaults to 3.
    """

    def __init__(
        self,
        *,
        base_url: str,
        api_key: str,
        timeout: httpx.Timeout | None = None,
        max_retries: int = _DEFAULT_MAX_RETRIES,
    ) -> None:
        self._base_url = base_url.rstrip("/")
        self._api_key = api_key
        self._max_retries = max_retries
        self._client = httpx.Client(
            base_url=self._base_url,
            timeout=timeout or _DEFAULT_TIMEOUT,
            headers={
                "User-Agent": f"sika-python/{__version__}",
                "X-Api-Key": api_key,
            },
        )

    # -- Public API ----------------------------------------------------------

    def get(
        self,
        path: str,
        *,
        params: dict[str, Any] | None = None,
    ) -> Any:
        """Send a GET request and return parsed JSON.

        Args:
            path: API path (e.g. ``/recon/v1/scans``).
            params: Optional query parameters.

        Returns:
            Parsed JSON response body.

        Raises:
            AuthenticationError: On 401 responses.
            NotFoundError: On 404 responses.
            RateLimitError: On 429 responses after retries exhausted.
            ServerError: On 5xx responses after retries exhausted.
            SikaError: On other non-2xx responses.
        """
        return self._request("GET", path, params=params)

    def post(
        self,
        path: str,
        *,
        json: Any | None = None,
    ) -> Any:
        """Send a POST request and return parsed JSON.

        Args:
            path: API path.
            json: Request body to send as JSON.

        Returns:
            Parsed JSON response body.

        Raises:
            AuthenticationError: On 401 responses.
            NotFoundError: On 404 responses.
            RateLimitError: On 429 responses after retries exhausted.
            ServerError: On 5xx responses after retries exhausted.
            SikaError: On other non-2xx responses.
        """
        return self._request("POST", path, json=json)

    def delete(self, path: str) -> Any:
        """Send a DELETE request and return parsed JSON (or None for 204).

        Args:
            path: API path.

        Returns:
            Parsed JSON response body, or ``None`` for 204 No Content.

        Raises:
            AuthenticationError: On 401 responses.
            NotFoundError: On 404 responses.
            RateLimitError: On 429 responses after retries exhausted.
            ServerError: On 5xx responses after retries exhausted.
            SikaError: On other non-2xx responses.
        """
        return self._request("DELETE", path)

    def close(self) -> None:
        """Close the underlying HTTP connection pool."""
        self._client.close()

    # -- Internals -----------------------------------------------------------

    def _request(
        self,
        method: str,
        path: str,
        *,
        params: dict[str, Any] | None = None,
        json: Any | None = None,
    ) -> Any:
        """Execute an HTTP request with retry logic.

        Retries on 5xx and 429 responses with exponential backoff.
        Respects the ``Retry-After`` header when present.
        """
        last_exception: SikaError | None = None

        for attempt in range(1 + self._max_retries):
            try:
                response = self._client.request(
                    method,
                    path,
                    params=params,
                    json=json,
                )
            except httpx.TimeoutException as exc:
                raise SikaError(
                    f"Request timed out: {exc}",
                    status_code=0,
                ) from exc

            if response.status_code >= 200 and response.status_code < 300:
                if response.status_code == 204:
                    return None
                return response.json()

            error = _error_from_response(response)

            if response.status_code not in _RETRYABLE_STATUS_CODES:
                raise error

            last_exception = error

            if attempt < self._max_retries:
                delay = _compute_delay(response, attempt)
                time.sleep(delay)

        # All retries exhausted — raise the last error.
        assert last_exception is not None
        raise last_exception

    def __repr__(self) -> str:
        return (
            f"SikaHTTPClient(base_url={self._base_url!r}, "
            f"api_key={mask_api_key(self._api_key)!r})"
        )

    def __enter__(self) -> SikaHTTPClient:
        return self

    def __exit__(self, *args: object) -> None:
        self.close()


def _error_from_response(response: httpx.Response) -> SikaError:
    """Parse an HTTP error response into a typed exception.

    Delegates to ``parse_error_response`` for RFC 7807 body parsing
    and exception class selection. Injects ``Retry-After`` header
    value into rate limit errors.
    """
    try:
        body: Any = response.json()
    except Exception:
        body = response.text or None

    error = parse_error_response(response.status_code, body)

    # Inject Retry-After header into rate limit errors.
    retry_after = _parse_retry_after(response)
    if retry_after is not None and hasattr(error, "retry_after"):
        error.retry_after = retry_after

    return error


def _parse_retry_after(response: httpx.Response) -> float | None:
    """Parse the Retry-After header value as seconds."""
    value = response.headers.get("Retry-After")
    if value is None:
        return None
    try:
        return float(value)
    except (ValueError, TypeError):
        return None


def _compute_delay(response: httpx.Response, attempt: int) -> float:
    """Compute retry delay, preferring Retry-After header over backoff."""
    retry_after = _parse_retry_after(response)
    if retry_after is not None and retry_after > 0:
        return retry_after
    return _BACKOFF_BASE * (_BACKOFF_FACTOR**attempt)
