"""Minimal SAIR Competition Public API client.

Designed to be copied into your own codebase rather than imported as a
dependency. Wraps only the bits worth centralising: auth header, URL joining,
envelope unwrap, error extraction. Pagination and retries are left to the
caller.
"""

from __future__ import annotations

import os
from collections.abc import Iterator
from typing import Any

import httpx

DEFAULT_BASE_URL = "https://api.sair.foundation/api/public/v1"


class SairError(Exception):
    """Raised when the API returns ``{"ok": false, "error": ...}``."""

    def __init__(
        self,
        code: str,
        message: str,
        status: int,
        details: dict[str, Any],
        retry_after: int | None = None,
    ) -> None:
        super().__init__(code, message, status, details)
        self.code = code
        self.message = message
        self.status = status
        self.details = details
        # Seconds from the ``Retry-After`` header on rate-limited (429) responses.
        self.retry_after = retry_after

    def __str__(self) -> str:  # pragma: no cover - display only
        return f"[{self.status} {self.code}] {self.message}"


class Sair:
    """Thin HTTP wrapper. Each method does one thing: send, unwrap, raise."""

    def __init__(
        self,
        api_key: str | None = None,
        base_url: str = DEFAULT_BASE_URL,
        timeout: float = 30.0,
    ) -> None:
        api_key = api_key or os.environ.get("SAIR_API_KEY")
        if not api_key:
            raise ValueError("Set SAIR_API_KEY or pass api_key=... explicitly.")
        self._http = httpx.Client(
            base_url=base_url,
            timeout=timeout,
            headers={
                "Authorization": f"Bearer {api_key}",
                "Accept": "application/json",
            },
        )

    # ── Core ────────────────────────────────────────────────────

    def request(
        self,
        method: str,
        path: str,
        *,
        params: dict[str, Any] | None = None,
        json: dict[str, Any] | None = None,
    ) -> Any:
        resp = self._http.request(method, path, params=params, json=json)
        if resp.status_code == 204:
            return None
        body = resp.json()
        if body.get("ok") is True:
            return body["data"]
        err = body.get("error", {}) or {}
        retry_after_header = resp.headers.get("Retry-After")
        retry_after = (
            int(retry_after_header)
            if retry_after_header and retry_after_header.isdigit()
            else None
        )
        raise SairError(
            code=err.get("code", "UNKNOWN_ERROR"),
            message=err.get("message", "(no message)"),
            status=resp.status_code,
            details=err.get("details") or {},
            retry_after=retry_after,
        )

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

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

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

    def delete(self, path: str) -> None:
        self.request("DELETE", path)

    # ── Helpers ─────────────────────────────────────────────────

    def paginate(self, path: str, **params: Any) -> Iterator[dict[str, Any]]:
        """Yield items across pages, following ``nextCursor`` until exhausted.

        Any ``cursor`` in ``params`` is ignored — this helper manages it.
        """
        params.pop("cursor", None)
        cursor: str | None = None
        while True:
            page = self.get(path, cursor=cursor, **params)
            yield from page["items"]
            cursor = page.get("nextCursor")
            if not cursor:
                return

    def close(self) -> None:
        self._http.close()

    def __enter__(self) -> Sair:
        return self

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