"""Dataverse OAuth2 authentication helpers."""

from __future__ import annotations

import time
from dataclasses import dataclass
from typing import Any, Dict

import requests


class DataverseAuthError(RuntimeError):
    """Raised when Dataverse authentication fails."""


@dataclass
class DataverseToken:
    access_token: str
    expires_at: float

    @property
    def is_expired(self) -> bool:
        return time.time() >= (self.expires_at - 60)


class DataverseAuthenticator:
    """
    OAuth2 client-credentials authenticator for Dataverse Web API.

    Note: Dataverse S2S auth requires an Application User with security roles
    assigned in the Dataverse admin center.
    """

    def __init__(
        self,
        tenant_id: str | None,
        client_id: str | None,
        client_secret: str | None,
        environment_url: str | None,
    ) -> None:
        self.tenant_id = (tenant_id or "").strip()
        self.client_id = (client_id or "").strip()
        self.client_secret = (client_secret or "").strip()
        env_url = (environment_url or "").strip()
        self.environment_url = env_url.rstrip("/") if env_url else ""
        self._token: DataverseToken | None = None

    def get_token(self) -> str:
        if self._token and not self._token.is_expired:
            return self._token.access_token

        self._ensure_credentials()
        token = self._request_token()
        self._token = token
        return token.access_token

    def _ensure_credentials(self) -> None:
        missing = []
        if not self.tenant_id:
            missing.append("tenant_id")
        if not self.client_id:
            missing.append("client_id")
        if not self.environment_url:
            missing.append("environment_url")

        if missing:
            raise DataverseAuthError(
                "Missing Dataverse credentials: " + ", ".join(missing)
            )

        if not self.client_secret:
            raise DataverseAuthError(
                "Device code flow is not supported in v1. "
                "Provide client_secret for client credentials auth."
            )

    def _request_token(self) -> DataverseToken:
        url = (
            f"https://login.microsoftonline.com/{self.tenant_id}/oauth2/v2.0/token"
        )
        scope = f"{self.environment_url}/.default"
        data: Dict[str, Any] = {
            "grant_type": "client_credentials",
            "client_id": self.client_id,
            "client_secret": self.client_secret,
            "scope": scope,
        }

        try:
            resp = requests.post(url, data=data, timeout=20)
        except requests.RequestException as exc:
            raise DataverseAuthError(f"Dataverse token request failed: {exc}") from exc

        if resp.status_code != 200:
            detail = ""
            try:
                payload = resp.json()
                detail = payload.get("error_description") or payload.get("error") or ""
            except Exception:
                detail = resp.text or ""
            detail = detail.strip()
            if detail:
                raise DataverseAuthError(
                    f"Dataverse token request failed ({resp.status_code}): {detail}"
                )
            raise DataverseAuthError(
                f"Dataverse token request failed ({resp.status_code})."
            )

        try:
            payload = resp.json()
        except Exception as exc:  # pragma: no cover - defensive
            raise DataverseAuthError(f"Invalid token response: {exc}") from exc

        access_token = payload.get("access_token")
        if not access_token:
            raise DataverseAuthError("Dataverse token response missing access_token.")

        expires_in = payload.get("expires_in", 3600)
        try:
            expires_in_float = float(expires_in)
        except Exception:
            expires_in_float = 3600.0
        expires_at = time.time() + expires_in_float
        return DataverseToken(access_token=access_token, expires_at=expires_at)


__all__ = ["DataverseAuthenticator", "DataverseToken", "DataverseAuthError"]
