"""Shared HTTP transport: error handling, retries, logging."""

from __future__ import annotations

import json
import logging
import os
import random
import time
from typing import TYPE_CHECKING

import httpx

from primitif._exceptions import NetworkError as PrimitifNetworkError

if TYPE_CHECKING:
    from primitif._exceptions import PrimitifError

DEFAULT_MAX_RETRIES = 2
INITIAL_RETRY_DELAY = 0.5
RETRY_STATUS_CODES = frozenset({429, 500, 502, 503, 504})


def configure_logging(name: str) -> logging.Logger:
    """Return a logger for the given product name.

    The SDK never calls ``basicConfig()`` — applications configure their own
    logging.  We just add a ``NullHandler`` so log calls don't warn about
    missing handlers, and honour ``PRIMITIF_LOG`` for quick debugging.
    """
    logger = logging.getLogger(f"primitif.{name}")
    logger.addHandler(logging.NullHandler())
    level = os.environ.get("PRIMITIF_LOG", "").upper()
    if level in ("DEBUG", "INFO", "WARNING", "ERROR"):
        logger.setLevel(getattr(logging, level))
    return logger


def raise_for_status(
    response: httpx.Response,
    *,
    exc_map: dict[int, type[PrimitifError]],
    server_exc: type[PrimitifError],
    base_exc: type[PrimitifError],
) -> None:
    """Raise a product-specific exception based on HTTP status code."""
    if response.status_code < 400:
        return
    try:
        body = response.json() if response.content else {}
    except (ValueError, json.JSONDecodeError):
        body = {}

    # Handle error body formats:
    # {"error": "...", "message": "..."}
    # {"detail": {"error": {"code": "...", "message": "..."}}}
    # {"detail": [{"loc": [...], "msg": "...", "type": "..."}]}  (FastAPI 422)
    # {"detail": "string message"}
    code = body.get("error", "unknown")
    message = body.get("message", "")
    if not message:
        detail = body.get("detail", {})
        if isinstance(detail, list):
            parts = []
            for err in detail:
                if isinstance(err, dict):
                    loc = " -> ".join(str(part) for part in err.get("loc", []))
                    msg = err.get("msg", "")
                    parts.append(f"{loc}: {msg}" if loc else msg)
                else:
                    parts.append(str(err))
            message = "; ".join(parts) if parts else str(detail)
        elif isinstance(detail, dict):
            error_obj = detail.get("error", {})
            if isinstance(error_obj, dict):
                code = error_obj.get("code", code)
                message = error_obj.get("message", "")
        elif isinstance(detail, str):
            message = detail
        if not message:
            message = str(detail) if detail else response.reason_phrase

    request_id = response.headers.get("x-request-id")
    kwargs = {
        "message": message,
        "status_code": response.status_code,
        "code": code,
        "response": response,
        "request_id": request_id,
    }

    if response.status_code >= 500:
        raise server_exc(**kwargs)
    exc_cls = exc_map.get(response.status_code, base_exc)
    raise exc_cls(**kwargs)


MAX_RETRY_DELAY = 60.0


def _retry_delay(response: httpx.Response, attempt: int) -> float:
    """Calculate retry delay with exponential backoff and jitter."""
    delay = INITIAL_RETRY_DELAY * (2**attempt)
    if response.status_code == 429:
        retry_after = response.headers.get("retry-after")
        if retry_after:
            try:
                delay = max(delay, float(retry_after))
            except (ValueError, TypeError):
                pass
    delay = min(delay, MAX_RETRY_DELAY)
    return delay + random.uniform(0, 0.25 * delay)


def _transport_retry_delay(attempt: int) -> float:
    """Calculate retry delay for transport errors (no response available)."""
    delay = INITIAL_RETRY_DELAY * (2**attempt)
    return delay + random.uniform(0, 0.25 * delay)


def request_with_retries(
    client: httpx.Client,
    method: str,
    path: str,
    max_retries: int,
    *,
    exc_map: dict[int, type[PrimitifError]],
    server_exc: type[PrimitifError],
    base_exc: type[PrimitifError],
    connection_exc: type[PrimitifError] | None = None,
    logger: logging.Logger | None = None,
    **kwargs,
) -> dict:
    """Sync HTTP request with retries on transient failures."""
    _conn_exc = connection_exc or PrimitifNetworkError
    last_response: httpx.Response | None = None
    last_transport_error: httpx.TransportError | None = None

    for attempt in range(max_retries + 1):
        try:
            response = client.request(method, path, **kwargs)
        except (httpx.TimeoutException, httpx.ConnectError) as exc:
            last_transport_error = exc
            if attempt < max_retries:
                delay = _transport_retry_delay(attempt)
                if logger:
                    logger.info(
                        "Retry %d/%d for %s %s (%s) in %.1fs",
                        attempt + 1,
                        max_retries,
                        method,
                        path,
                        type(exc).__name__,
                        delay,
                    )
                time.sleep(delay)
                continue
            raise _conn_exc(
                message=f"{type(exc).__name__}: {exc}",
            ) from exc
        except httpx.TransportError as exc:
            raise _conn_exc(
                message=f"{type(exc).__name__}: {exc}",
            ) from exc

        last_response = response
        last_transport_error = None

        if response.status_code in RETRY_STATUS_CODES and attempt < max_retries:
            delay = _retry_delay(response, attempt)
            if logger:
                logger.info(
                    "Retry %d/%d for %s %s (status %d) in %.1fs",
                    attempt + 1,
                    max_retries,
                    method,
                    path,
                    response.status_code,
                    delay,
                )
            time.sleep(delay)
            continue
        break

    if last_response is None:
        raise _conn_exc(
            message=f"{type(last_transport_error).__name__}: {last_transport_error}",
        ) from last_transport_error

    raise_for_status(
        last_response,
        exc_map=exc_map,
        server_exc=server_exc,
        base_exc=base_exc,
    )
    if last_response.status_code == 204:
        return {}
    try:
        return last_response.json()
    except (ValueError, json.JSONDecodeError) as exc:
        raise base_exc(
            message=f"Expected JSON response but got {last_response.headers.get('content-type', 'unknown')}",
            status_code=last_response.status_code,
        ) from exc
