"""HTTP transport for the BuildAI API.

Both public and internal surfaces use /v1/*. Authorization comes from the
authenticated principal/template behind the presented credential, not from
URL prefixes or ad hoc local profiles.

Two transports:
- ``AsyncHttpTransport`` — primary, uses ``httpx.AsyncClient``
- ``HttpTransport`` — sync wrapper using ``httpx.Client`` (backward compat)
"""

from __future__ import annotations

from pathlib import Path
import os
import random
import time
import tomllib
from typing import Any, Literal

import httpx

from ._version import __version__
from .errors import ApiError, ConfigError

_RETRY_STATUSES = {429, 500, 502, 503, 504}
_IDEMPOTENT_METHODS = {"GET", "HEAD", "OPTIONS", "PUT", "DELETE"}

_DEFAULT_BASE_URL = "https://api.build.ai"
_DEFAULT_TIMEOUT = 30.0
_CLI_CONFIG_PATH = Path.home() / ".buildai" / "config.toml"

ClientMode = Literal["public", "internal"]


def _compute_api_prefix(mode: ClientMode) -> str:
    if mode in ("public", "internal"):
        return "/v1"
    raise ValueError(f"Unknown mode: {mode!r}")


def _backoff_delay(attempt: int, retry_after: str | None) -> float:
    """Exponential backoff with jitter, respecting Retry-After header."""
    if retry_after:
        try:
            return max(float(retry_after), 0.1)
        except ValueError:
            pass
    base = min(2**attempt * 0.5, 30.0)
    jitter = random.uniform(0, base * 0.5)
    return base + jitter


def _raise_for_status(resp: httpx.Response) -> None:
    """Normalize HTTP error responses into ApiError."""
    status = resp.status_code
    message = resp.text or f"HTTP {status}"
    code = "HTTP_ERROR"
    retryable = status in {429, 500, 502, 503, 504}
    request_id: str | None = None
    details: dict[str, Any] | None = None

    try:
        payload = resp.json()
        if isinstance(payload, dict):
            request_id = payload.get("request_id") or request_id

            err = payload.get("error")
            if isinstance(err, dict):
                code = err.get("code") or code
                message = err.get("message") or message
                retryable = bool(err.get("retryable", retryable))
                if isinstance(err.get("details"), dict):
                    details = err.get("details")
            else:
                det = payload.get("detail")
                if isinstance(det, dict):
                    code = det.get("code") or code
                    message = det.get("message") or message
                    retryable = bool(det.get("retryable", retryable))
                    if isinstance(det.get("details"), dict):
                        details = det.get("details")
                elif isinstance(det, str):
                    message = det

                if not message and isinstance(payload.get("message"), str):
                    message = payload["message"]
    except Exception:
        pass

    if request_id is None:
        request_id = resp.headers.get("x-request-id")

    raise ApiError(
        message or f"HTTP {status}",
        code=code,
        retryable=retryable,
        status_code=status,
        request_id=request_id,
        details=details,
    )


def _load_cli_api_key() -> str | None:
    try:
        raw = _CLI_CONFIG_PATH.read_bytes()
    except FileNotFoundError:
        return None
    except Exception:
        return None

    try:
        data = tomllib.loads(raw.decode("utf-8"))
    except Exception:
        return None

    api_key = data.get("cli", {}).get("api_key")
    return api_key if isinstance(api_key, str) and api_key else None


# ---------------------------------------------------------------------------
# Shared config mixin
# ---------------------------------------------------------------------------


class _TransportConfig:
    """Shared configuration for both sync and async transports."""

    def __init__(
        self,
        *,
        api_key: str | None = None,
        base_url: str | None = None,
        timeout: float | None = None,
        max_retries: int = 3,
        mode: ClientMode = "public",
        api_prefix: str | None = None,
        retry_on_mutation: bool = False,
    ) -> None:
        self.api_key = api_key or os.getenv("BUILDAI_API_KEY") or _load_cli_api_key()
        if not self.api_key:
            raise ConfigError(
                "Credential required — set BUILDAI_API_KEY, run `buildai auth login-device`, or pass api_key="
            )

        self.base_url = (base_url or os.getenv("BUILDAI_API_URL") or _DEFAULT_BASE_URL).rstrip("/")
        self.timeout = timeout or float(os.getenv("BUILDAI_TIMEOUT", str(_DEFAULT_TIMEOUT)))
        self.max_retries = max_retries
        self.retry_on_mutation = retry_on_mutation
        self.mode: ClientMode = mode

        prefix = (api_prefix or _compute_api_prefix(mode)).strip()
        if not prefix.startswith("/"):
            prefix = "/" + prefix
        self.api_prefix = prefix.rstrip("/")

    def _headers(self) -> dict[str, str]:
        return {
            "Authorization": f"Bearer {self.api_key}",
            "User-Agent": f"buildai-sdk/{__version__}",
        }

    def _build_url(self, path: str) -> str:
        if not path.startswith("/"):
            raise ValueError("path must start with '/' (e.g. '/clips')")
        return f"{self.base_url}{self.api_prefix}{path}"


# ---------------------------------------------------------------------------
# Async transport (primary)
# ---------------------------------------------------------------------------


class AsyncHttpTransport(_TransportConfig):
    """Async HTTP transport using httpx.AsyncClient."""

    def __init__(
        self,
        *,
        api_key: str | None = None,
        base_url: str | None = None,
        timeout: float | None = None,
        max_retries: int = 3,
        mode: ClientMode = "public",
        api_prefix: str | None = None,
        retry_on_mutation: bool = False,
        client: httpx.AsyncClient | None = None,
        owns_client: bool = True,
    ) -> None:
        super().__init__(
            api_key=api_key,
            base_url=base_url,
            timeout=timeout,
            max_retries=max_retries,
            mode=mode,
            api_prefix=api_prefix,
            retry_on_mutation=retry_on_mutation,
        )
        self._owns_client = owns_client
        self._client = client or httpx.AsyncClient(timeout=self.timeout)

    async def request(
        self,
        method: str,
        path: str,
        *,
        params: dict[str, Any] | None = None,
        json: dict[str, Any] | None = None,
        timeout: float | None = None,
        retry: bool | None = None,
    ) -> Any:
        import asyncio

        url = self._build_url(path)
        is_idempotent = method.upper() in _IDEMPOTENT_METHODS
        should_retry = retry if retry is not None else (is_idempotent or self.retry_on_mutation)
        max_attempts = self.max_retries + 1 if should_retry else 1

        last_resp = None
        for attempt in range(max_attempts):
            resp = await self._client.request(
                method,
                url,
                params=params,
                json=json,
                headers=self._headers(),
                timeout=timeout or self.timeout,
            )

            if resp.status_code < 400:
                if not resp.content:
                    return {}
                return resp.json()

            if resp.status_code in _RETRY_STATUSES and attempt < max_attempts - 1:
                wait = _backoff_delay(attempt, resp.headers.get("Retry-After"))
                await asyncio.sleep(wait)
                last_resp = resp
                continue

            _raise_for_status(resp)

        if last_resp is not None:
            _raise_for_status(last_resp)

    async def get(self, path: str, *, params: dict[str, Any] | None = None) -> Any:
        return await self.request("GET", path, params=params)

    async def post(self, path: str, *, json: dict[str, Any] | None = None) -> Any:
        return await self.request("POST", path, json=json, retry=self.retry_on_mutation)

    async def patch(self, path: str, *, json: dict[str, Any] | None = None) -> Any:
        return await self.request("PATCH", path, json=json, retry=self.retry_on_mutation)

    async def delete(
        self,
        path: str,
        *,
        params: dict[str, Any] | None = None,
        json: dict[str, Any] | None = None,
    ) -> Any:
        return await self.request("DELETE", path, params=params, json=json)

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

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

    async def __aexit__(self, *args: Any) -> None:
        await self.aclose()


# ---------------------------------------------------------------------------
# Sync transport (backward compat)
# ---------------------------------------------------------------------------


class HttpTransport(_TransportConfig):
    """Sync HTTP transport using httpx.Client. Drop-in replacement for the old requests-based transport."""

    def __init__(
        self,
        *,
        api_key: str | None = None,
        base_url: str | None = None,
        timeout: float | None = None,
        max_retries: int = 3,
        mode: ClientMode = "public",
        api_prefix: str | None = None,
        retry_on_mutation: bool = False,
        client: httpx.Client | None = None,
        owns_client: bool = True,
    ) -> None:
        super().__init__(
            api_key=api_key,
            base_url=base_url,
            timeout=timeout,
            max_retries=max_retries,
            mode=mode,
            api_prefix=api_prefix,
            retry_on_mutation=retry_on_mutation,
        )
        self._owns_client = owns_client
        self._client = client or httpx.Client(timeout=self.timeout)

    def derive(
        self, *, mode: ClientMode | None = None, api_prefix: str | None = None
    ) -> HttpTransport:
        """Create a new transport view that shares the underlying client."""
        next_mode = mode or self.mode
        next_prefix = api_prefix or _compute_api_prefix(next_mode)
        return HttpTransport(
            api_key=self.api_key,
            base_url=self.base_url,
            timeout=self.timeout,
            max_retries=self.max_retries,
            mode=next_mode,
            api_prefix=next_prefix,
            client=self._client,
            owns_client=False,
            retry_on_mutation=self.retry_on_mutation,
        )

    def request(
        self,
        method: str,
        path: str,
        *,
        params: dict[str, Any] | None = None,
        json: dict[str, Any] | None = None,
        timeout: float | None = None,
        retry: bool | None = None,
    ) -> Any:
        url = self._build_url(path)
        is_idempotent = method.upper() in _IDEMPOTENT_METHODS
        should_retry = retry if retry is not None else (is_idempotent or self.retry_on_mutation)
        max_attempts = self.max_retries + 1 if should_retry else 1

        last_resp = None
        for attempt in range(max_attempts):
            resp = self._client.request(
                method,
                url,
                params=params,
                json=json,
                headers=self._headers(),
                timeout=timeout or self.timeout,
            )

            if resp.status_code < 400:
                if not resp.content:
                    return {}
                return resp.json()

            if resp.status_code in _RETRY_STATUSES and attempt < max_attempts - 1:
                wait = _backoff_delay(attempt, resp.headers.get("Retry-After"))
                time.sleep(wait)
                last_resp = resp
                continue

            _raise_for_status(resp)

        if last_resp is not None:
            _raise_for_status(last_resp)

    def get(self, path: str, *, params: dict[str, Any] | None = None) -> Any:
        return self.request("GET", path, params=params)

    def post(self, path: str, *, json: dict[str, Any] | None = None) -> Any:
        return self.request("POST", path, json=json, retry=self.retry_on_mutation)

    def patch(self, path: str, *, json: dict[str, Any] | None = None) -> Any:
        return self.request("PATCH", path, json=json, retry=self.retry_on_mutation)

    def delete(
        self,
        path: str,
        *,
        params: dict[str, Any] | None = None,
        json: dict[str, Any] | None = None,
    ) -> Any:
        return self.request("DELETE", path, params=params, json=json)

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

    def __enter__(self) -> HttpTransport:
        return self

    def __exit__(self, *args: Any) -> None:
        self.close()
