"""OIDC single sign-on (build plan Section 8).

The provider client is behind an interface with a swappable factory, so tests
drive the callback against a simulated IdP without any network. A real client
performs the authorization-code exchange and reads the userinfo endpoint.
"""

from __future__ import annotations

import urllib.parse
from collections.abc import Callable
from dataclasses import dataclass
from typing import Protocol

import httpx

from legate.config import Settings, get_settings


class OIDCError(Exception):
    """Raised when an OIDC exchange fails or SSO is misconfigured."""


@dataclass
class OIDCIdentity:
    subject: str
    email: str
    name: str


class OIDCClient(Protocol):
    def authorization_url(self, state: str) -> str: ...

    def exchange_code(self, code: str) -> OIDCIdentity: ...


class HttpOIDCClient:
    def __init__(self, settings: Settings) -> None:
        self._s = settings

    def authorization_url(self, state: str) -> str:
        params = {
            "response_type": "code",
            "client_id": self._s.oidc_client_id,
            "redirect_uri": self._s.oidc_redirect_uri,
            "scope": self._s.oidc_scopes,
            "state": state,
        }
        return f"{self._s.oidc_authorization_endpoint}?{urllib.parse.urlencode(params)}"

    def exchange_code(self, code: str) -> OIDCIdentity:
        try:
            with httpx.Client(timeout=30.0) as client:
                token_res = client.post(
                    self._s.oidc_token_endpoint,
                    data={
                        "grant_type": "authorization_code",
                        "code": code,
                        "redirect_uri": self._s.oidc_redirect_uri,
                        "client_id": self._s.oidc_client_id,
                        "client_secret": self._s.oidc_client_secret,
                    },
                )
                token_res.raise_for_status()
                access = token_res.json()["access_token"]
                info_res = client.get(
                    self._s.oidc_userinfo_endpoint,
                    headers={"Authorization": f"Bearer {access}"},
                )
                info_res.raise_for_status()
                info = info_res.json()
        except (httpx.HTTPError, KeyError) as exc:
            raise OIDCError(f"OIDC exchange failed: {exc}") from exc
        return OIDCIdentity(
            subject=str(info.get("sub", "")),
            email=str(info.get("email", "")),
            name=str(info.get("name", "")),
        )


_factory: Callable[[Settings], OIDCClient] | None = None


def set_oidc_client_factory(factory: Callable[[Settings], OIDCClient] | None) -> None:
    global _factory
    _factory = factory


def get_oidc_client() -> OIDCClient:
    settings = get_settings()
    if _factory is not None:
        return _factory(settings)
    return HttpOIDCClient(settings)
