"""Core token verification engine."""

import jwt
from jwt import PyJWKClient

from ._config import AuthConfig
from ._models import AuthMethod, AuthenticatedUser, IdpConfig


class AuthenticationError(Exception):
    """Raised when token verification fails."""


class _IdpVerifier:
    """Verifies JWTs for a single identity provider."""

    def __init__(self, idp: IdpConfig, *, verify_exp: bool = True, verify_aud: bool = True):
        self._idp = idp
        self._jwks_client = PyJWKClient(idp.jwks_uri)
        self._verify_exp = verify_exp
        self._verify_aud = verify_aud

    def verify(self, token: str) -> AuthenticatedUser:
        signing_key = self._jwks_client.get_signing_key_from_jwt(token)

        has_audience = self._verify_aud and self._idp.audience
        options: dict = {
            "verify_exp": self._verify_exp,
            "verify_aud": has_audience,
        }

        decode_kwargs: dict = {
            "algorithms": self._idp.algorithms,
            "issuer": self._idp.issuer,
            "options": options,
        }
        if has_audience:
            decode_kwargs["audience"] = self._idp.audience

        payload = jwt.decode(token, signing_key.key, **decode_kwargs)
        return _payload_to_user(payload, token=token, idp_name=self._idp.name)


def _payload_to_user(
    payload: dict, *, token: str, idp_name: str | None = None
) -> AuthenticatedUser:
    """Convert a JWT payload dict into an AuthenticatedUser."""
    subject = payload.get("sub", "")
    email = payload.get("email") or payload.get("preferred_username") or payload.get("upn")
    name = payload.get("name")

    return AuthenticatedUser(
        subject=subject,
        email=email,
        name=name,
        auth_method=AuthMethod.JWT,
        idp_name=idp_name,
        claims=payload,
        token=token,
    )


class AuthVerifier:
    """Main verification engine supporting multiple IdPs and static tokens."""

    def __init__(self, config: AuthConfig):
        self._config = config
        self._static_tokens: set[str] = config.get_static_token_set()
        self._issuer_to_verifier: dict[str, _IdpVerifier] = {}

        for idp in config.get_idp_configs():
            verifier = _IdpVerifier(
                idp,
                verify_exp=config.verify_exp,
                verify_aud=config.verify_aud,
            )
            self._issuer_to_verifier[idp.issuer] = verifier

    @property
    def auth_enabled(self) -> bool:
        return self._config.auth_enabled

    def verify_token(self, token: str | None) -> AuthenticatedUser:
        """Verify a token and return the authenticated user.

        Flow:
        1. If auth not enabled → raise AuthenticationError
        2. If no token → raise AuthenticationError
        3. If token matches a static token → static AuthenticatedUser
        4. Peek issuer from unverified decode → route to IdP verifier
        """
        if not self.auth_enabled:
            raise AuthenticationError(
                "Authentication is not configured. "
                "Set REMEM_AUTH_* environment variables to enable authentication."
            )

        if not token:
            raise AuthenticationError("No token provided")

        # Static token check — O(1) set lookup, cheapest path
        if token in self._static_tokens:
            return AuthenticatedUser(
                subject="static-token-user",
                auth_method=AuthMethod.STATIC,
                claims={},
                token=token,
            )

        # JWT verification — peek issuer then route
        try:
            unverified = jwt.decode(token, options={"verify_signature": False})
        except jwt.DecodeError as exc:
            raise AuthenticationError(f"Malformed token: {exc}") from exc

        issuer = unverified.get("iss", "")
        verifier = self._issuer_to_verifier.get(issuer)
        if verifier is None:
            raise AuthenticationError(f"Unknown issuer: {issuer!r}")

        try:
            return verifier.verify(token)
        except jwt.ExpiredSignatureError as exc:
            raise AuthenticationError(f"Token expired: {exc}") from exc
        except jwt.InvalidAudienceError as exc:
            raise AuthenticationError(f"Invalid audience: {exc}") from exc
        except jwt.InvalidIssuerError as exc:
            raise AuthenticationError(f"Invalid issuer: {exc}") from exc
        except Exception as exc:
            raise AuthenticationError(f"Token verification failed: {exc}") from exc
