"""Shared HTTP transport with retry, error handling, and auth injection."""

from __future__ import annotations

import time
from dataclasses import dataclass, field
from typing import Any, Callable, Mapping, MutableMapping, Sequence

import httpx


# ---------------------------------------------------------------------------
# Error types
# ---------------------------------------------------------------------------


class SbnError(RuntimeError):
    """Raised when the SBN API responds with an error payload."""

    def __init__(
        self,
        message: str,
        *,
        status_code: int,
        code: str | None = None,
        details: Mapping[str, Any] | None = None,
    ) -> None:
        super().__init__(message)
        self.status_code = status_code
        self.code = code
        self.details = dict(details or {})

    def __str__(self) -> str:
        base = super().__str__()
        parts = [f"status={self.status_code}"]
        if self.code:
            parts.append(f"code={self.code}")
        if self.details:
            parts.append(f"details={self.details}")
        return f"{base} ({', '.join(parts)})"


class SbnTransportError(RuntimeError):
    """Raised when the SDK cannot reach SBN after retries."""

    def __init__(self, message: str, *, last_exception: Exception | None = None) -> None:
        super().__init__(message)
        self.last_exception = last_exception


# ---------------------------------------------------------------------------
# Retry config
# ---------------------------------------------------------------------------


@dataclass(slots=True)
class RetryConfig:
    """Retry configuration for transient failures."""

    attempts: int = 3
    backoff_factor: float = 0.5
    status_forcelist: Sequence[int] = (500, 502, 503, 504)


# ---------------------------------------------------------------------------
# Auth state
# ---------------------------------------------------------------------------


@dataclass
class AuthState:
    """Mutable auth state shared across sub-clients."""

    api_key: str | None = None
    bearer_token: str | None = None
    tenant_id: str | None = None
    token_provider: Callable[[], str] | None = None


# ---------------------------------------------------------------------------
# HTTP transport
# ---------------------------------------------------------------------------


class HttpTransport:
    """Thin wrapper around httpx.Client with retry and auth injection."""

    def __init__(
        self,
        *,
        base_url: str,
        auth: AuthState,
        retry: RetryConfig | None = None,
        timeout: float = 10.0,
        user_agent: str = "sbn-sdk/0.5",
        transport: httpx.BaseTransport | None = None,
    ) -> None:
        self._base_url = base_url.rstrip("/")
        self._auth = auth
        self._retry = retry or RetryConfig()
        self._client = httpx.Client(
            base_url=self._base_url,
            timeout=timeout,
            headers={"User-Agent": user_agent},
            transport=transport,
        )

    @property
    def base_url(self) -> str:
        return self._base_url

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

    # ------------------------------ auth headers ---------------------------

    def _headers(self, extra: Mapping[str, str] | None = None) -> dict[str, str]:
        headers: dict[str, str] = {}
        if self._auth.tenant_id:
            headers["X-Tenant-ID"] = self._auth.tenant_id

        # API key auth takes precedence
        if self._auth.api_key:
            headers["x-api-key"] = self._auth.api_key
        elif self._auth.token_provider:
            headers["Authorization"] = f"Bearer {self._auth.token_provider()}"
        elif self._auth.bearer_token:
            headers["Authorization"] = f"Bearer {self._auth.bearer_token}"

        if extra:
            headers.update(extra)
        return headers

    # ------------------------------ request core ---------------------------

    def request(
        self,
        method: str,
        path: str,
        *,
        headers: Mapping[str, str] | None = None,
        **kwargs: Any,
    ) -> httpx.Response:
        attempts = max(1, self._retry.attempts)
        backoff = max(0.0, self._retry.backoff_factor)
        method_upper = method.upper()

        for attempt in range(1, attempts + 1):
            try:
                response = self._client.request(
                    method_upper,
                    path,
                    headers=self._headers(headers),
                    **kwargs,
                )
            except httpx.RequestError as exc:
                if attempt >= attempts:
                    raise SbnTransportError(
                        "Failed to reach SBN", last_exception=exc
                    ) from exc
                time.sleep(backoff)
                backoff *= 2
                continue

            if (
                response.status_code in self._retry.status_forcelist
                and attempt < attempts
            ):
                response.close()
                time.sleep(backoff)
                backoff *= 2
                continue

            if response.status_code >= 400:
                raise _error_from_response(response)
            return response

        raise SbnTransportError("Exhausted retries reaching SBN")

    def get(self, path: str, **kwargs: Any) -> httpx.Response:
        return self.request("GET", path, **kwargs)

    def post(self, path: str, **kwargs: Any) -> httpx.Response:
        return self.request("POST", path, **kwargs)

    def patch(self, path: str, **kwargs: Any) -> httpx.Response:
        return self.request("PATCH", path, **kwargs)

    def put(self, path: str, **kwargs: Any) -> httpx.Response:
        return self.request("PUT", path, **kwargs)

    def delete(self, path: str, **kwargs: Any) -> httpx.Response:
        return self.request("DELETE", path, **kwargs)


def _error_from_response(response: httpx.Response) -> SbnError:
    try:
        payload = response.json()
    except ValueError:
        payload = None

    message = response.reason_phrase or "SBN request failed"
    code: str | None = None
    details: MutableMapping[str, Any] = {}

    if isinstance(payload, Mapping):
        if isinstance(payload.get("error"), Mapping):
            err = payload["error"]
            message = str(err.get("message") or message)
            code = err.get("code")
            if isinstance(err.get("details"), Mapping):
                details.update(err["details"])
        elif isinstance(payload.get("error"), str):
            code = payload["error"]
            message = str(payload.get("detail") or payload.get("message") or message)
        else:
            message = str(payload.get("message") or payload.get("detail") or message)
        if isinstance(payload.get("details"), Mapping):
            details.update(payload["details"])
    elif payload is not None:
        details["response"] = payload

    return SbnError(message, status_code=response.status_code, code=code, details=details)
