"""HTTP client for cnMaestro Cloud v2 API.

Handles OAuth2 client_credentials auth (with in-memory token cache and
on-401 refresh) and paginated GETs.
"""

from __future__ import annotations

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

import httpx

log = logging.getLogger(__name__)

DEFAULT_PAGE_SIZE = 100


class CnMaestroError(RuntimeError):
    """Raised on API errors."""


class CnMaestroClient:
    """Synchronous client for cnMaestro Cloud v2 API."""

    def __init__(
        self,
        base_url: str | None = None,
        client_id: str | None = None,
        client_secret: str | None = None,
        timeout: float = 30.0,
    ) -> None:
        self.base_url = (
            base_url or os.environ.get("CNMAESTRO_BASE_URL") or ""
        ).rstrip("/")
        self.client_id = client_id or os.environ.get("CNMAESTRO_CLIENT_ID")
        self.client_secret = client_secret or os.environ.get("CNMAESTRO_CLIENT_SECRET")
        if not self.base_url:
            raise CnMaestroError(
                "Missing CNMAESTRO_BASE_URL. Copy .env.example to .env and set it "
                "to your tenant's URL (e.g. https://us-e1-s1-XXXX.cloud.cambiumnetworks.com)."
            )
        if not self.client_id or not self.client_secret:
            raise CnMaestroError(
                "Missing CNMAESTRO_CLIENT_ID / CNMAESTRO_CLIENT_SECRET. "
                "Copy .env.example to .env and fill in your API client."
            )
        self._http = httpx.Client(base_url=self.base_url, timeout=timeout)
        self._token: str | None = None

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

    def __enter__(self) -> CnMaestroClient:
        return self

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

    def _fetch_token(self) -> str:
        log.debug("Fetching new OAuth2 token")
        resp = self._http.post(
            "/api/v2/access/token",
            data={"grant_type": "client_credentials"},
            auth=(self.client_id, self.client_secret),  # type: ignore[arg-type]
            headers={"Content-Type": "application/x-www-form-urlencoded"},
        )
        if resp.status_code != 200:
            raise CnMaestroError(
                f"Token request failed: {resp.status_code} {resp.text}"
            )
        body = resp.json()
        token = body.get("access_token")
        if not token:
            raise CnMaestroError(f"Token response missing access_token: {body}")
        return token

    def _auth_headers(self) -> dict[str, str]:
        if self._token is None:
            self._token = self._fetch_token()
        return {"Authorization": f"Bearer {self._token}"}

    def _request(
        self,
        method: str,
        path: str,
        *,
        params: dict[str, Any] | None = None,
        json: Any = None,
    ) -> httpx.Response:
        """Send request, refreshing token once on 401."""
        for attempt in (1, 2):
            resp = self._http.request(
                method,
                path,
                params=params,
                json=json,
                headers=self._auth_headers(),
            )
            if resp.status_code == 401 and attempt == 1:
                log.debug("401 received; refreshing token and retrying")
                self._token = None
                continue
            return resp
        return resp  # unreachable

    def get(self, path: str, params: dict[str, Any] | None = None) -> Any:
        resp = self._request("GET", path, params=params)
        if resp.status_code >= 400:
            raise CnMaestroError(f"GET {path} → {resp.status_code} {resp.text}")
        return resp.json()

    def delete(self, path: str) -> None:
        resp = self._request("DELETE", path)
        if resp.status_code >= 400:
            raise CnMaestroError(f"DELETE {path} → {resp.status_code} {resp.text}")

    def post(self, path: str, payload: Any) -> Any:
        resp = self._request("POST", path, json=payload)
        if resp.status_code >= 400:
            raise CnMaestroError(f"POST {path} → {resp.status_code} {resp.text}")
        if resp.content:
            return resp.json()
        return None

    def paginated(
        self,
        path: str,
        params: dict[str, Any] | None = None,
        page_size: int = DEFAULT_PAGE_SIZE,
    ) -> Iterator[dict[str, Any]]:
        """Yield items from a paginated endpoint.

        cnMaestro v2 returns ``{"data": [...], "paging": {"total": N}}``.
        """
        offset = 0
        params = dict(params or {})
        while True:
            params.update({"limit": page_size, "offset": offset})
            body = self.get(path, params=params)
            items = body.get("data", []) if isinstance(body, dict) else []
            if not items:
                return
            yield from items
            offset += len(items)
            total = (
                body.get("paging", {}).get("total")
                if isinstance(body, dict)
                else None
            )
            if total is not None and offset >= total:
                return
            if len(items) < page_size:
                return
