"""Generic RFC 6749 client-credentials TokenProvider.

Works unmodified against any standards-compliant IdP -- the mock IdP
shipped in a2a_passport.testing, or a real one (Auth0, Keycloak, Okta,
Azure AD). IdP-specific quirks (Auth0 wants `audience` in the form body,
Keycloak wants a separate client per audience or a `scope` claim mapper)
are handled by passing `extra_form_fields`, not by subclassing -- keeps one
implementation instead of one class per vendor.
"""
from __future__ import annotations

import time

import httpx

from a2a_passport.config import settings
from a2a_passport.exceptions import TokenAcquisitionError


class OAuth2ClientCredentialsProvider:
    """Acquires and caches scoped client-credentials tokens, one per audience.

    A single instance is safe to reuse across every outbound call an agent
    makes -- it holds a separate cached token per target audience and
    refreshes each independently, shortly before it actually expires.
    """

    def __init__(
        self,
        *,
        token_url: str,
        client_id: str,
        client_secret: str,
        audience_field: str = "audience",
        extra_form_fields: dict[str, str] | None = None,
        request_timeout_seconds: float | None = None,
    ):
        self._token_url = token_url
        self._client_id = client_id
        self._client_secret = client_secret
        self._audience_field = audience_field
        self._extra_form_fields = extra_form_fields or {}
        self._timeout = request_timeout_seconds or settings.request_timeout_seconds
        self._cache: dict[str, tuple[str, float]] = {}

    async def get_token(self, audience: str) -> str:
        cached = self._cache.get(audience)
        now = time.time()
        if cached and cached[1] > now:
            return cached[0]

        form = {
            "grant_type": "client_credentials",
            "client_id": self._client_id,
            "client_secret": self._client_secret,
            self._audience_field: audience,
            **self._extra_form_fields,
        }
        async with httpx.AsyncClient(timeout=self._timeout) as client:
            resp = await client.post(self._token_url, data=form)

        if resp.status_code != 200:
            raise TokenAcquisitionError(
                f"failed to acquire token for audience={audience!r} from "
                f"{self._token_url}: HTTP {resp.status_code} {resp.text}"
            )
        body = resp.json()
        token = body["access_token"]
        expires_in = body.get("expires_in", 60)
        expiry = now + expires_in - settings.token_cache_leeway_seconds
        self._cache[audience] = (token, expiry)
        return token
