"""Camada de transporte HTTP da abacapython.

Encapsula `httpx` para fornecer:

- autenticação automática via Bearer Token;
- User-Agent identificável (importante para o time da AbacatePay rastrear uso);
- retries com backoff exponencial para erros transitórios (5xx, 429, conexão);
- desempacotamento padronizado do envelope ``{"data": ..., "error": ...}``;
- conversão de erros HTTP para a hierarquia de :mod:`abacapython.exceptions`.

A mesma implementação serve tanto para o cliente síncrono (`AbacaPay`)
quanto para o assíncrono (`AsyncAbacaPay`), graças ao suporte dual do httpx.
"""

from __future__ import annotations

import asyncio
import logging
import random
import time
from collections.abc import Iterable, Mapping
from typing import Any

import httpx

from ._version import __version__
from .exceptions import (
    APIConnectionError,
    APIResponseError,
    APITimeoutError,
    exception_for_status,
)

_logger = logging.getLogger("abacapython")

DEFAULT_BASE_URL = "https://api.abacatepay.com/v2"
DEFAULT_TIMEOUT = 30.0
DEFAULT_MAX_RETRIES = 2
DEFAULT_BACKOFF_FACTOR = 0.5
DEFAULT_BACKOFF_MAX = 8.0
_RETRYABLE_STATUSES = frozenset({408, 425, 429, 500, 502, 503, 504})

JsonLike = Mapping[str, Any] | Iterable[Any] | str | int | float | bool | None


def _user_agent() -> str:
    """Retorna o User-Agent usado em todas as requisições."""
    return f"abacapython/{__version__} (+https://github.com/nicollasrezende/abacapython)"


def _compute_backoff(attempt: int, factor: float, cap: float) -> float:
    """Backoff exponencial com jitter (Full Jitter — AWS Architecture Blog)."""
    delay = min(cap, factor * (2**attempt))
    return random.uniform(0, delay)


def _build_headers(api_key: str, extra: Mapping[str, str] | None = None) -> dict[str, str]:
    headers = {
        "Authorization": f"Bearer {api_key}",
        "User-Agent": _user_agent(),
        "Accept": "application/json",
        "Content-Type": "application/json",
    }
    if extra:
        headers.update(extra)
    return headers


def _unwrap_envelope(body: Any) -> Any:
    """Desempacota o envelope ``{"data": ..., "error": ...}`` da API.

    Se a resposta veio com ``error != null``, levanta :class:`APIResponseError`
    com a mensagem original. Caso contrário, retorna ``body["data"]``.

    Respostas que não seguem o envelope (ex.: TrustMRR público) são devolvidas
    inalteradas.
    """
    if not isinstance(body, dict):
        return body
    if "data" not in body and "error" not in body:
        return body
    error = body.get("error")
    if error:
        raise APIResponseError(
            message=str(error) if not isinstance(error, dict) else error.get("message", str(error)),
            response_body=body,
        )
    return body.get("data")


def _raise_for_status(response: httpx.Response) -> None:
    if response.is_success:
        return
    try:
        body = response.json()
    except ValueError:
        body = response.text

    message = _extract_error_message(body) or response.reason_phrase or "Erro na API AbacatePay"
    exc_cls = exception_for_status(response.status_code)
    raise exc_cls(
        message=message,
        status_code=response.status_code,
        response_body=body,
        request_id=response.headers.get("x-request-id"),
    )


def _extract_error_message(body: Any) -> str | None:
    if isinstance(body, dict):
        error = body.get("error")
        if isinstance(error, dict):
            return error.get("message") or error.get("code")
        if isinstance(error, str):
            return error
        message = body.get("message")
        if isinstance(message, str):
            return message
    return None


class HTTPTransport:
    """Wrapper síncrono em torno de :class:`httpx.Client`.

    Args:
        api_key: Chave de API da AbacatePay (`abc_dev_...` ou `abc_live_...`).
        base_url: Override da URL base — útil para testes ou ambientes futuros.
        timeout: Timeout em segundos para qualquer requisição.
        max_retries: Quantas vezes retentar em caso de erro transitório.
        backoff_factor: Fator base do backoff exponencial.
        backoff_max: Limite superior (em segundos) para qualquer tentativa.
        client: Instância customizada de :class:`httpx.Client` (opcional).
    """

    def __init__(
        self,
        api_key: str,
        *,
        base_url: str = DEFAULT_BASE_URL,
        timeout: float = DEFAULT_TIMEOUT,
        max_retries: int = DEFAULT_MAX_RETRIES,
        backoff_factor: float = DEFAULT_BACKOFF_FACTOR,
        backoff_max: float = DEFAULT_BACKOFF_MAX,
        client: httpx.Client | None = None,
    ) -> None:
        if not api_key:
            raise ValueError("api_key não pode ser vazio")
        self._api_key = api_key
        self._base_url = base_url.rstrip("/")
        self._timeout = timeout
        self._max_retries = max_retries
        self._backoff_factor = backoff_factor
        self._backoff_max = backoff_max
        self._owns_client = client is None
        self._client = client or httpx.Client(timeout=timeout)

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

    def request(
        self,
        method: str,
        path: str,
        *,
        params: Mapping[str, Any] | None = None,
        json: JsonLike | None = None,
        headers: Mapping[str, str] | None = None,
    ) -> Any:
        url = f"{self._base_url}{path}"
        merged_headers = _build_headers(self._api_key, headers)
        last_exc: Exception | None = None

        for attempt in range(self._max_retries + 1):
            try:
                _logger.debug("HTTP %s %s (tentativa=%d)", method, url, attempt)
                response = self._client.request(
                    method,
                    url,
                    params=params,
                    json=json,
                    headers=merged_headers,
                )
            except httpx.TimeoutException as exc:
                last_exc = APITimeoutError(f"Timeout em {method} {url}: {exc}")
            except httpx.HTTPError as exc:
                last_exc = APIConnectionError(f"Falha de conexão em {method} {url}: {exc}")
            else:
                if response.status_code in _RETRYABLE_STATUSES and attempt < self._max_retries:
                    sleep_for = _compute_backoff(attempt, self._backoff_factor, self._backoff_max)
                    _logger.info(
                        "Resposta %s — retentando em %.2fs", response.status_code, sleep_for
                    )
                    time.sleep(sleep_for)
                    continue
                _raise_for_status(response)
                if response.status_code == 204 or not response.content:
                    return None
                try:
                    return _unwrap_envelope(response.json())
                except ValueError:
                    return response.text

            if attempt < self._max_retries:
                time.sleep(_compute_backoff(attempt, self._backoff_factor, self._backoff_max))
                continue
            assert last_exc is not None
            raise last_exc

        # Inalcançável; loop sempre retorna ou levanta.
        assert last_exc is not None  # pragma: no cover
        raise last_exc  # pragma: no cover

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

    def __enter__(self) -> HTTPTransport:
        return self

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


class AsyncHTTPTransport:
    """Versão assíncrona de :class:`HTTPTransport`."""

    def __init__(
        self,
        api_key: str,
        *,
        base_url: str = DEFAULT_BASE_URL,
        timeout: float = DEFAULT_TIMEOUT,
        max_retries: int = DEFAULT_MAX_RETRIES,
        backoff_factor: float = DEFAULT_BACKOFF_FACTOR,
        backoff_max: float = DEFAULT_BACKOFF_MAX,
        client: httpx.AsyncClient | None = None,
    ) -> None:
        if not api_key:
            raise ValueError("api_key não pode ser vazio")
        self._api_key = api_key
        self._base_url = base_url.rstrip("/")
        self._timeout = timeout
        self._max_retries = max_retries
        self._backoff_factor = backoff_factor
        self._backoff_max = backoff_max
        self._owns_client = client is None
        self._client = client or httpx.AsyncClient(timeout=timeout)

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

    async def request(
        self,
        method: str,
        path: str,
        *,
        params: Mapping[str, Any] | None = None,
        json: JsonLike | None = None,
        headers: Mapping[str, str] | None = None,
    ) -> Any:
        url = f"{self._base_url}{path}"
        merged_headers = _build_headers(self._api_key, headers)
        last_exc: Exception | None = None

        for attempt in range(self._max_retries + 1):
            try:
                _logger.debug("HTTP %s %s (tentativa=%d)", method, url, attempt)
                response = await self._client.request(
                    method,
                    url,
                    params=params,
                    json=json,
                    headers=merged_headers,
                )
            except httpx.TimeoutException as exc:
                last_exc = APITimeoutError(f"Timeout em {method} {url}: {exc}")
            except httpx.HTTPError as exc:
                last_exc = APIConnectionError(f"Falha de conexão em {method} {url}: {exc}")
            else:
                if response.status_code in _RETRYABLE_STATUSES and attempt < self._max_retries:
                    sleep_for = _compute_backoff(attempt, self._backoff_factor, self._backoff_max)
                    _logger.info(
                        "Resposta %s — retentando em %.2fs", response.status_code, sleep_for
                    )
                    await asyncio.sleep(sleep_for)
                    continue
                _raise_for_status(response)
                if response.status_code == 204 or not response.content:
                    return None
                try:
                    return _unwrap_envelope(response.json())
                except ValueError:
                    return response.text

            if attempt < self._max_retries:
                await asyncio.sleep(
                    _compute_backoff(attempt, self._backoff_factor, self._backoff_max)
                )
                continue
            assert last_exc is not None
            raise last_exc

        assert last_exc is not None  # pragma: no cover
        raise last_exc  # pragma: no cover

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

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

    async def __aexit__(self, *_: object) -> None:
        await self.aclose()


__all__ = [
    "DEFAULT_BASE_URL",
    "DEFAULT_MAX_RETRIES",
    "DEFAULT_TIMEOUT",
    "AsyncHTTPTransport",
    "HTTPTransport",
]
