"""HTTP transport layer for the Convexity SDK.

Wraps ``httpx`` with:
- Automatic ``X-API-Key`` header injection
- Rich error mapping (status code → SDK exception)
- Exponential-backoff retry on transient errors (429, 502, 503, 504)
- Request / response logging
"""

from __future__ import annotations

import logging
import random
import time
from typing import Any

import httpx

from .config import SDKConfig
from .exceptions import (
    RETRYABLE_STATUS_CODES,
    STATUS_CODE_TO_EXCEPTION,
    ConvexityError,
    RateLimitError,
    ServerError,
)

logger = logging.getLogger("convexity_sdk.http")


def _build_headers(config: SDKConfig) -> dict[str, str]:
    headers: dict[str, str] = {
        "User-Agent": "convexity-sdk-python",
        "Accept": "application/json",
    }
    if config.api_key:
        headers["X-API-Key"] = config.api_key
    headers.update(config.extra_headers)
    return headers


class SyncTransport:
    """Synchronous HTTP transport backed by ``httpx.Client``."""

    def __init__(self, config: SDKConfig) -> None:
        self._config = config
        self._client = httpx.Client(
            base_url=config.base_url,
            headers=_build_headers(config),
            timeout=httpx.Timeout(config.timeout),
            follow_redirects=True,
        )

    # -- lifecycle ------------------------------------------------------------

    def close(self) -> None:
        self._client.close()

    def __enter__(self) -> SyncTransport:
        return self

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

    # -- public request method ------------------------------------------------

    def request(
        self,
        method: str,
        path: str,
        *,
        json: Any | None = None,
        params: dict[str, Any] | None = None,
        content: bytes | None = None,
        headers: dict[str, str] | None = None,
    ) -> httpx.Response:
        """Send an HTTP request with automatic retry and error mapping."""
        last_exc: Exception | None = None

        for attempt in range(1, self._config.max_retries + 1):
            try:
                response = self._send(method, path, json=json, params=params, content=content, headers=headers)
                try:
                    elapsed_ms = int(response.elapsed.total_seconds() * 1000)
                except RuntimeError:
                    elapsed_ms = 0
                logger.info("%s %s → %s (%dms)", method.upper(), path, response.status_code, elapsed_ms)

                if response.status_code < 400:
                    return response

                # Map HTTP errors to SDK exceptions
                if response.status_code in RETRYABLE_STATUS_CODES:
                    exc = self._build_exception(response, method, path)
                    last_exc = exc
                    delay = self._backoff_delay(attempt, response)
                    logger.warning("Retryable error %s on attempt %d/%d – retrying in %.1fs", response.status_code, attempt, self._config.max_retries, delay)
                    time.sleep(delay)
                    continue

                # Non-retryable error — raise immediately
                raise self._build_exception(response, method, path)

            except httpx.TimeoutException:
                last_exc = httpx.TimeoutException(f"Request timed out: {method.upper()} {path}")
                logger.warning("Timeout on attempt %d/%d for %s %s", attempt, self._config.max_retries, method.upper(), path)
                if attempt < self._config.max_retries:
                    time.sleep(self._backoff_delay(attempt))
                    continue
                raise last_exc from None

        # Exhausted all retries
        if last_exc is not None:
            raise last_exc
        raise ServerError("Request failed after all retries", method=method, endpoint=path)  # pragma: no cover

    # -- internals ------------------------------------------------------------

    def _send(
        self,
        method: str,
        path: str,
        *,
        json: Any | None = None,
        params: dict[str, Any] | None = None,
        content: bytes | None = None,
        headers: dict[str, str] | None = None,
    ) -> httpx.Response:
        kwargs: dict[str, Any] = {"method": method, "url": path}
        if json is not None:
            kwargs["json"] = json
        if params is not None:
            # Filter out None values
            kwargs["params"] = {k: v for k, v in params.items() if v is not None}
        if content is not None:
            kwargs["content"] = content
        if headers is not None:
            kwargs["headers"] = headers

        logger.debug("→ %s %s", method.upper(), path)
        return self._client.request(**kwargs)

    def _backoff_delay(self, attempt: int, response: httpx.Response | None = None) -> float:
        """Exponential backoff with jitter.  Respects ``Retry-After`` header."""
        if response is not None:
            retry_after = response.headers.get("Retry-After")
            if retry_after is not None:
                try:
                    return float(retry_after)
                except ValueError:
                    pass

        base = self._config.retry_backoff * (2 ** (attempt - 1))
        jitter = random.uniform(0, base * 0.25)  # noqa: S311 – not crypto
        return base + jitter

    @staticmethod
    def _build_exception(response: httpx.Response, method: str, path: str) -> ConvexityError:
        """Convert an HTTP error response into the appropriate SDK exception."""
        status = response.status_code
        try:
            body = response.json()
            message = body.get("detail", body.get("message", response.text))
        except Exception:
            message = response.text or f"HTTP {status}"

        if isinstance(message, list):
            # FastAPI validation errors return a list of dicts
            message = "; ".join(str(e) for e in message)

        exc_cls = STATUS_CODE_TO_EXCEPTION.get(status)
        if exc_cls is not None:
            if exc_cls is RateLimitError:
                retry_after_raw = response.headers.get("Retry-After")
                retry_after = float(retry_after_raw) if retry_after_raw else None
                return RateLimitError(
                    str(message),
                    retry_after=retry_after,
                    status_code=status,
                    method=method.upper(),
                    endpoint=path,
                )
            return exc_cls(
                str(message),
                status_code=status,
                method=method.upper(),
                endpoint=path,
            )

        # Fallback for any 5xx
        if 500 <= status < 600:
            return ServerError(
                str(message),
                status_code=status,
                method=method.upper(),
                endpoint=path,
            )

        return ConvexityError(
            str(message),
            status_code=status,
            method=method.upper(),
            endpoint=path,
        )


class AsyncTransport:
    """Asynchronous HTTP transport backed by ``httpx.AsyncClient``."""

    def __init__(self, config: SDKConfig) -> None:
        self._config = config
        self._client = httpx.AsyncClient(
            base_url=config.base_url,
            headers=_build_headers(config),
            timeout=httpx.Timeout(config.timeout),
            follow_redirects=True,
        )

    # -- lifecycle ------------------------------------------------------------

    async def close(self) -> None:
        await self._client.aclose()

    async def __aenter__(self) -> AsyncTransport:
        return self

    async def __aexit__(self, *args: Any) -> None:
        await self.close()

    # -- public request method ------------------------------------------------

    async def request(
        self,
        method: str,
        path: str,
        *,
        json: Any | None = None,
        params: dict[str, Any] | None = None,
        content: bytes | None = None,
        headers: dict[str, str] | None = None,
    ) -> httpx.Response:
        """Send an HTTP request with automatic retry and error mapping."""
        import asyncio

        last_exc: Exception | None = None

        for attempt in range(1, self._config.max_retries + 1):
            try:
                response = await self._send(method, path, json=json, params=params, content=content, headers=headers)
                try:
                    elapsed_ms = int(response.elapsed.total_seconds() * 1000)
                except RuntimeError:
                    elapsed_ms = 0
                logger.info("%s %s → %s (%dms)", method.upper(), path, response.status_code, elapsed_ms)

                if response.status_code < 400:
                    return response

                if response.status_code in RETRYABLE_STATUS_CODES:
                    exc = SyncTransport._build_exception(response, method, path)
                    last_exc = exc
                    delay = self._backoff_delay(attempt, response)
                    logger.warning("Retryable error %s on attempt %d/%d – retrying in %.1fs", response.status_code, attempt, self._config.max_retries, delay)
                    await asyncio.sleep(delay)
                    continue

                raise SyncTransport._build_exception(response, method, path)

            except httpx.TimeoutException:
                last_exc = httpx.TimeoutException(f"Request timed out: {method.upper()} {path}")
                logger.warning("Timeout on attempt %d/%d for %s %s", attempt, self._config.max_retries, method.upper(), path)
                if attempt < self._config.max_retries:
                    await asyncio.sleep(self._backoff_delay(attempt))
                    continue
                raise last_exc from None

        if last_exc is not None:
            raise last_exc
        raise ServerError("Request failed after all retries", method=method, endpoint=path)  # pragma: no cover

    # -- internals ------------------------------------------------------------

    async def _send(
        self,
        method: str,
        path: str,
        *,
        json: Any | None = None,
        params: dict[str, Any] | None = None,
        content: bytes | None = None,
        headers: dict[str, str] | None = None,
    ) -> httpx.Response:
        kwargs: dict[str, Any] = {"method": method, "url": path}
        if json is not None:
            kwargs["json"] = json
        if params is not None:
            kwargs["params"] = {k: v for k, v in params.items() if v is not None}
        if content is not None:
            kwargs["content"] = content
        if headers is not None:
            kwargs["headers"] = headers

        logger.debug("→ %s %s", method.upper(), path)
        return await self._client.request(**kwargs)

    def _backoff_delay(self, attempt: int, response: httpx.Response | None = None) -> float:
        if response is not None:
            retry_after = response.headers.get("Retry-After")
            if retry_after is not None:
                try:
                    return float(retry_after)
                except ValueError:
                    pass

        base = self._config.retry_backoff * (2 ** (attempt - 1))
        jitter = random.uniform(0, base * 0.25)  # noqa: S311
        return base + jitter
