"""AgentBill Unified HTTP Client — v10.2.0

Centralized HTTP transport layer for the Python SDK.
All resource modules and internal HTTP calls should go through this client
to ensure consistent auth, retries, timeouts, error mapping, and logging.

Usage:
    http = AgentBillHTTPClient(base_url="https://api.agentbill.io", api_key="agb_...")
    response = http.post("/functions/v1/otel-collector", json=payload)
    print(response.data)
"""

import time
from typing import Any, Dict, Optional, Union

import httpx

from .logger import get_logger
from .exceptions import (
    AgentBillError,
    BudgetExceededError,
    RateLimitExceededError,
    PolicyViolationError,
    ValidationError,
)

_log = get_logger("http")


class AgentBillResponse:
    """Unified response wrapper.

    Attributes:
        status: HTTP status code.
        headers: Response headers as a dict.
        data: Parsed JSON body (or raw text if not JSON).
        raw: The underlying ``httpx.Response``.
    """

    __slots__ = ("status", "headers", "data", "raw")

    def __init__(self, response: httpx.Response):
        self.status: int = response.status_code
        self.headers: Dict[str, str] = dict(response.headers)
        self.raw = response
        try:
            self.data: Any = response.json()
        except Exception:
            self.data = response.text

    def __repr__(self) -> str:
        return f"AgentBillResponse(status={self.status})"

    @property
    def ok(self) -> bool:
        return 200 <= self.status < 300


class AgentBillHTTPClient:
    """Shared HTTP transport for every SDK module.

    Features:
      - Automatic ``X-API-Key`` header injection
      - Configurable timeout (default 30 s)
      - Automatic retry with exponential back-off (default 2 retries)
      - Unified error mapping to typed SDK exceptions
      - Structured logging via ``agentbill.logger``
    """

    def __init__(
        self,
        base_url: str,
        api_key: str,
        timeout: float = 30.0,
        max_retries: int = 2,
        retry_on: tuple = (502, 503, 504),
    ):
        self.base_url = base_url.rstrip("/")
        self.api_key = api_key
        self.timeout = timeout
        self.max_retries = max_retries
        self.retry_on = retry_on
        self._client = httpx.Client(
            timeout=timeout,
            headers={
                "Content-Type": "application/json",
                "X-API-Key": api_key,
            },
        )

    # ── public convenience methods ────────────────────────────────

    def get(self, path: str, *, params: Optional[Dict] = None, **kwargs) -> AgentBillResponse:
        return self.request("GET", path, params=params, **kwargs)

    def post(self, path: str, *, json: Any = None, **kwargs) -> AgentBillResponse:
        return self.request("POST", path, json=json, **kwargs)

    def put(self, path: str, *, json: Any = None, **kwargs) -> AgentBillResponse:
        return self.request("PUT", path, json=json, **kwargs)

    def patch(self, path: str, *, json: Any = None, **kwargs) -> AgentBillResponse:
        return self.request("PATCH", path, json=json, **kwargs)

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

    # ── core request with retry ───────────────────────────────────

    def request(
        self,
        method: str,
        path: str,
        *,
        json: Any = None,
        params: Optional[Dict] = None,
        headers: Optional[Dict[str, str]] = None,
        timeout: Optional[float] = None,
    ) -> AgentBillResponse:
        """Execute an HTTP request with retry, error mapping, and logging."""
        url = f"{self.base_url}{path}" if path.startswith("/") else path

        merged_headers = {}
        if headers:
            merged_headers.update(headers)

        last_exc: Optional[Exception] = None

        for attempt in range(self.max_retries + 1):
            try:
                _log.debug(f"{method} {path} (attempt {attempt + 1})")
                resp = self._client.request(
                    method,
                    url,
                    json=json,
                    params=params,
                    headers=merged_headers,
                    timeout=timeout or self.timeout,
                )
                wrapped = AgentBillResponse(resp)

                # Retry on transient server errors
                if resp.status_code in self.retry_on and attempt < self.max_retries:
                    wait = 0.5 * (2 ** attempt)
                    _log.warn(f"Retryable {resp.status_code} on {path}, retrying in {wait:.1f}s")
                    time.sleep(wait)
                    continue

                # Map HTTP errors to typed exceptions
                if not wrapped.ok:
                    self._raise_for_status(wrapped)

                _log.debug(f"{method} {path} → {resp.status_code}")
                return wrapped

            except (httpx.TimeoutException, httpx.ConnectError) as exc:
                last_exc = exc
                if attempt < self.max_retries:
                    wait = 0.5 * (2 ** attempt)
                    _log.warn(f"Network error on {path}: {exc}, retrying in {wait:.1f}s")
                    time.sleep(wait)
                else:
                    _log.error(f"Request failed after {self.max_retries + 1} attempts: {exc}")
                    raise AgentBillError(f"Request to {path} failed: {exc}") from exc

            except (BudgetExceededError, RateLimitExceededError, PolicyViolationError):
                raise  # Don't retry policy errors

            except AgentBillError:
                raise  # Don't retry mapped errors

        # Should not reach here, but just in case
        raise AgentBillError(f"Request to {path} failed after retries") from last_exc

    # ── error mapping ─────────────────────────────────────────────

    def _raise_for_status(self, resp: AgentBillResponse) -> None:
        """Map HTTP error codes to typed SDK exceptions."""
        status = resp.status
        body = resp.data if isinstance(resp.data, dict) else {}
        message = body.get("error") or body.get("message") or resp.raw.text
        request_id = body.get("request_id") or resp.headers.get("x-request-id")

        if status == 429:
            raise RateLimitExceededError(
                str(message),
                details={"retry_after": resp.headers.get("Retry-After")},
                status=status,
                request_id=request_id,
            )
        if status == 403:
            reason = body.get("reason", str(message))
            code = body.get("code", "")
            if code == "BUDGET_EXCEEDED" or "budget" in reason.lower():
                raise BudgetExceededError(reason, details=body, status=status, request_id=request_id)
            raise PolicyViolationError(reason, details=body, status=status, request_id=request_id)
        if status == 422:
            raise ValidationError(str(message), field=body.get("field"), status=status, request_id=request_id)
        if status >= 400:
            raise AgentBillError(
                f"HTTP {status}: {message}",
                details={"status": status, "body": body},
                status=status,
                request_id=request_id,
            )

    # ── lifecycle ─────────────────────────────────────────────────

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

    def __enter__(self):
        return self

    def __exit__(self, *args):
        self.close()

    def __del__(self):
        try:
            self.close()
        except Exception:
            pass


__all__ = ["AgentBillHTTPClient", "AgentBillResponse"]
