"""AgentBill Async HTTP Client — v10.3.0

Async counterpart of AgentBillHTTPClient using httpx.AsyncClient.
For use with FastAPI, aiohttp, Django async views, and other async frameworks.

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

import asyncio
from typing import Any, Dict, Optional

import httpx

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

_log = get_logger("async_http")


class AsyncAgentBillHTTPClient:
    """Async HTTP transport for the AgentBill Python SDK.

    Mirrors AgentBillHTTPClient API but all methods are async.
    Uses httpx.AsyncClient for non-blocking I/O.
    """

    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.AsyncClient(
            timeout=timeout,
            headers={
                "Content-Type": "application/json",
                "X-API-Key": api_key,
            },
        )

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

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

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

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

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

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

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

    async 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 async 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 = await self._client.request(
                    method,
                    url,
                    json=json,
                    params=params,
                    headers=merged_headers,
                    timeout=timeout or self.timeout,
                )
                wrapped = AgentBillResponse(resp)

                if resp.status_code in self.retry_on and attempt < self.max_retries:
                    wait = 0.5 * (2 ** attempt)
                    _log.warning(f"Retryable {resp.status_code} on {path}, retrying in {wait:.1f}s")
                    await asyncio.sleep(wait)
                    continue

                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.warning(f"Network error on {path}: {exc}, retrying in {wait:.1f}s")
                    await asyncio.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

            except AgentBillError:
                raise

        raise AgentBillError(f"Request to {path} failed after retries") from last_exc

    # ── error mapping (reuses same logic as sync client) ──────────

    def _raise_for_status(self, resp: AgentBillResponse) -> None:
        status = resp.status
        body = resp.data if isinstance(resp.data, dict) else {}
        message = body.get("error") or body.get("message") or resp.raw.text

        if status == 429:
            raise RateLimitExceededError(
                str(message),
                details={"retry_after": resp.headers.get("Retry-After")},
            )
        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)
            raise PolicyViolationError(reason, details=body)
        if status == 422:
            raise ValidationError(str(message), field=body.get("field"))
        if status >= 400:
            raise AgentBillError(
                f"HTTP {status}: {message}",
                details={"status": status, "body": body},
            )

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

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

    async def __aenter__(self):
        return self

    async def __aexit__(self, *args):
        await self.close()


__all__ = ["AsyncAgentBillHTTPClient"]
