"""OAuth provider registry and helpers for RapidKit projects."""

from __future__ import annotations

import os
import secrets
import time
import urllib.parse
from dataclasses import dataclass, field
from typing import Any, Dict, Iterable, Mapping, MutableMapping, Optional


def _env(name: str, fallback: str | None = None) -> Optional[str]:
    return os.getenv(name, fallback) if name else fallback


_FEATURE_FLAGS: tuple[str, ...] = (
    "provider_registry",
    "state_management",
    "redirect_templates",
    "token_exchange_helpers",
)

@dataclass(slots=True, frozen=True)
class OAuthProvider:
    """Configuration entry describing an OAuth 2.0 provider."""

    name: str
    client_id: str
    client_secret: str
    authorize_url: str
    token_url: str
    scopes: tuple[str, ...] = ()
    userinfo_url: Optional[str] = None
    redirect_uri: Optional[str] = None
    extra_authorize_params: Dict[str, str] = field(default_factory=dict)

    def as_authorize_params(self, state: str) -> Dict[str, str]:
        params = {
            "client_id": self.client_id,
            "response_type": "code",
            "state": state,
        }
        if self.redirect_uri:
            params["redirect_uri"] = self.redirect_uri
        if self.scopes:
            params["scope"] = " ".join(self.scopes)
        params.update(self.extra_authorize_params)
        return params


@dataclass(slots=True)
class OAuthSettings:
    """Top-level OAuth configuration and provider registry."""

    redirect_base_url: str
    state_ttl_seconds: int
    providers: Dict[str, OAuthProvider]
    state_cleanup_interval: int = 60

    @classmethod
    def from_mapping(cls, data: Mapping[str, Any]) -> "OAuthSettings":
        redirect_base = str(data.get("redirect_base_url", "https://example.com/oauth"))
        ttl = int(data.get("state_ttl_seconds", 300))
        cleanup = int(data.get("state_cleanup_interval", 60))
        providers_cfg = data.get("providers", {})
        providers: Dict[str, OAuthProvider] = {}
        for raw_name, raw_cfg in providers_cfg.items():
            if not isinstance(raw_cfg, Mapping):
                continue
            name = str(raw_cfg.get("name", raw_name))
            client_id = _env(str(raw_cfg.get("client_id_env", "")), raw_cfg.get("client_id", ""))
            client_secret = _env(
                str(raw_cfg.get("client_secret_env", "")),
                raw_cfg.get("client_secret", ""),
            )
            authorize_url = str(raw_cfg.get("authorize_url", ""))
            token_url = str(raw_cfg.get("token_url", ""))
            userinfo_url = raw_cfg.get("userinfo_url")
            redirect_override = raw_cfg.get("redirect_uri")
            scopes_iter = raw_cfg.get("scopes", [])
            extra_params = raw_cfg.get("extra_authorize_params", {})

            if not client_id or not client_secret:
                continue

            provider = OAuthProvider(
                name=name,
                client_id=client_id,
                client_secret=client_secret,
                authorize_url=authorize_url,
                token_url=token_url,
                userinfo_url=str(userinfo_url) if userinfo_url else None,
                redirect_uri=str(redirect_override) if redirect_override else None,
                scopes=tuple(str(scope) for scope in scopes_iter if scope),
                extra_authorize_params={
                    str(k): str(v)
                    for k, v in (extra_params or {}).items()
                    if isinstance(k, str)
                },
            )
            providers[raw_name] = provider

        return cls(
            redirect_base_url=redirect_base,
            state_ttl_seconds=ttl,
            state_cleanup_interval=cleanup,
            providers=providers,
        )


@dataclass(slots=True)
class OAuthState:
    """Captures issued OAuth state metadata for callback validation."""

    provider: str
    issued_at: float
    metadata: Dict[str, Any]
    ttl_seconds: int

    def is_expired(self, *, now: Optional[float] = None) -> bool:
        return (now or time.time()) > self.issued_at + self.ttl_seconds


class OAuthStateStore:
    """Simple in-memory state store suitable for development and testing."""

    def __init__(self, *, ttl_seconds: int, cleanup_interval: int = 60) -> None:
        self._ttl = ttl_seconds
        self._cleanup_interval = max(cleanup_interval, 15)
        self._states: MutableMapping[str, OAuthState] = {}
        self._last_cleanup = 0.0

    def issue(self, provider: str, metadata: Optional[Mapping[str, Any]] = None) -> str:
        state = secrets.token_urlsafe(24)
        self._states[state] = OAuthState(
            provider=provider,
            issued_at=time.time(),
            metadata=dict(metadata or {}),
            ttl_seconds=self._ttl,
        )
        self._cleanup()
        return state

    def pop(self, state: str) -> Optional[OAuthState]:
        entry = self._states.pop(state, None)
        if entry is None:
            return None
        if entry.is_expired():
            return None
        return entry

    def _cleanup(self) -> None:
        now = time.time()
        if now - self._last_cleanup < self._cleanup_interval:
            return
        self._last_cleanup = now
        expired: Iterable[str] = [
            token for token, state in self._states.items() if state.is_expired(now=now)
        ]
        for token in expired:
            self._states.pop(token, None)


class OAuthRuntime:
    """High-level orchestrator for OAuth provider flows."""

    def __init__(self, settings: OAuthSettings) -> None:
        self._settings = settings
        self._state_store = OAuthStateStore(
            ttl_seconds=settings.state_ttl_seconds,
            cleanup_interval=settings.state_cleanup_interval,
        )

    @property
    def settings(self) -> OAuthSettings:
        return self._settings

    def get_provider(self, provider: str) -> OAuthProvider:
        try:
            return self._settings.providers[provider]
        except KeyError as exc:  # pragma: no cover - defensive guard
            raise KeyError(f"Unknown OAuth provider: {provider}") from exc

    def issue_state(self, provider: str, metadata: Optional[Mapping[str, Any]] = None) -> str:
        return self._state_store.issue(provider, metadata)

    def build_authorize_url(
        self,
        provider: str,
        *,
        redirect_override: Optional[str] = None,
        metadata: Optional[Mapping[str, Any]] = None,
    ) -> str:
        config = self.get_provider(provider)
        state = self.issue_state(provider, metadata)
        params = config.as_authorize_params(state)
        if redirect_override:
            params["redirect_uri"] = redirect_override
        elif not params.get("redirect_uri"):
            params["redirect_uri"] = self._default_callback_url(provider)

        query = urllib.parse.urlencode(params, safe="/:")
        return f"{config.authorize_url}?{query}"

    def validate_callback(self, provider: str, state: str) -> Dict[str, Any]:
        entry = self._state_store.pop(state)
        if entry is None:
            raise ValueError("OAuth state is invalid or has expired")
        if entry.provider != provider:
            raise ValueError("OAuth state provider mismatch")
        return dict(entry.metadata)

    def _default_callback_url(self, provider: str) -> str:
        base = self._settings.redirect_base_url.rstrip("/")
        return f"{base}/{provider}/callback"

    def metadata(self) -> Dict[str, Any]:
        return describe_oauth(self._settings)


def load_oauth_settings(overrides: Optional[Mapping[str, Any]] = None) -> OAuthSettings:
    """Build OAuth settings optionally merging override values."""

    defaults = {{ module_defaults | default({}) | pprint }}
    merged: Dict[str, Any] = dict(defaults)
    if overrides:
        merged.update(overrides)
        providers_override = overrides.get("providers", {})
        if isinstance(providers_override, Mapping):
            merged.setdefault("providers", {}).update(providers_override)  # type: ignore[arg-type]
    return OAuthSettings.from_mapping(merged)


def describe_oauth(settings: Optional[OAuthSettings] = None) -> Dict[str, Any]:
    """Return a normalized metadata payload for the OAuth module."""

    config = settings or load_oauth_settings()
    providers: Dict[str, Dict[str, Any]] = {}
    for name, provider in config.providers.items():
        providers[name] = {
            "name": provider.name,
            "authorize_url": provider.authorize_url,
            "token_url": provider.token_url,
            "userinfo_url": provider.userinfo_url,
            "redirect_uri": provider.redirect_uri or config.redirect_base_url.rstrip("/") + f"/{name}/callback",
            "scopes": list(provider.scopes),
        }

    return {
        "module": "{{ module_name | default('oauth') }}",
        "redirect_base_url": config.redirect_base_url,
        "state_ttl_seconds": config.state_ttl_seconds,
        "state_cleanup_interval": config.state_cleanup_interval,
        "provider_count": len(providers),
        "providers": providers,
        "features": list_oauth_features(),
    }


def list_oauth_features() -> list[str]:
    """Enumerate the capabilities advertised by the OAuth module."""

    return list(_FEATURE_FLAGS)


__all__ = [
    "OAuthProvider",
    "OAuthSettings",
    "OAuthState",
    "OAuthStateStore",
    "OAuthRuntime",
    "describe_oauth",
    "list_oauth_features",
    "load_oauth_settings",
]
