"""RS256 JWT validation for WordPress and other external platform clients."""

from __future__ import annotations

import os
import re
from dataclasses import dataclass
from pathlib import Path
from typing import Any
from datetime import datetime, timezone

import jwt
from flask import Request
from sqlalchemy.exc import SQLAlchemyError

from bridge_platform.sites.registry import consume_jti, get_signing_key_record


_KID_RE = re.compile(r"^[A-Za-z0-9._-]{1,128}$")
_DEFAULT_KEYS_DIR = Path(__file__).resolve().parents[1] / "secrets" / "site_keys"


class AuthenticationError(Exception):
    """Raised when an external request cannot be authenticated."""


@dataclass(frozen=True, slots=True)
class AuthenticatedIdentity:
    """Normalized, trusted identity extracted from a verified JWT."""

    tenant_id: str
    site_id: str
    subject: str
    issuer: str
    key_id: str
    key_source: str
    roles: tuple[str, ...]
    scopes: tuple[str, ...]
    claims: dict[str, Any]


def authenticate_request(request: Request) -> AuthenticatedIdentity:
    """Validate a Bearer JWT and return its trusted identity."""
    token = _extract_bearer_token(request)
    try:
        header = jwt.get_unverified_header(token)
    except jwt.PyJWTError as exc:
        raise AuthenticationError("Invalid JWT header") from exc

    if header.get("alg") != "RS256":
        raise AuthenticationError("JWT algorithm must be RS256")

    kid = str(header.get("kid") or "")
    if not _KID_RE.fullmatch(kid):
        raise AuthenticationError("Missing or invalid JWT kid")

    key_record = _registered_key(kid)
    if key_record is not None:
        if key_record["site_status"] != "active":
            raise AuthenticationError("JWT site is not active")
        if key_record["key_status"] != "active":
            raise AuthenticationError("JWT signing key is revoked or inactive")
        public_key = key_record["public_key_pem"]
        key_source = "control_plane"
        audience = key_record["audience"]
        allowed_issuers = {key_record["issuer"]}
    else:
        public_key = read_bootstrap_public_key(kid)
        key_source = "bootstrap_file"
        audience = os.getenv("JWT_AUDIENCE", "absolutems-api")
        allowed_issuers = _csv_values(os.getenv("JWT_ALLOWED_ISSUERS", "https://absolutems.com.au"))


    try:
        claims = jwt.decode(
            token,
            public_key,
            algorithms=["RS256"],
            audience=audience,
            options={
                "require": ["exp", "iat", "jti", "iss", "aud", "sub", "tenant_id", "site_id"],
            },
        )
    except jwt.ExpiredSignatureError as exc:
        raise AuthenticationError("JWT has expired") from exc
    except jwt.InvalidTokenError as exc:
        raise AuthenticationError("JWT validation failed") from exc

    issuer = str(claims.get("iss") or "").rstrip("/")
    if allowed_issuers and issuer not in allowed_issuers:
        raise AuthenticationError("JWT issuer is not allowed")

    tenant_id = _required_string_claim(claims, "tenant_id")
    site_id = _required_string_claim(claims, "site_id")
    subject = _required_string_claim(claims, "sub")
    if key_record is not None:
        if key_record["tenant_id"] != tenant_id or key_record["site_id"] != site_id:
            raise AuthenticationError("JWT site or tenant does not match signing key")
        try:
            consume_jti(
                site_id=site_id,
                jti=_required_string_claim(claims, "jti"),
                expires_at=datetime.fromtimestamp(int(claims["exp"]), tz=timezone.utc),
            )
        except PermissionError as exc:
            raise AuthenticationError(str(exc)) from exc

    return AuthenticatedIdentity(
        tenant_id=tenant_id,
        site_id=site_id,
        subject=subject,
        issuer=issuer,
        key_id=kid,
        key_source=key_source,
        roles=_string_tuple(claims.get("roles")),
        scopes=_string_tuple(claims.get("scopes")),
        claims=claims,
    )


def require_scope(identity: AuthenticatedIdentity, scope: str) -> None:
    """Require one exact scope from an authenticated identity."""
    if scope not in identity.scopes:
        raise PermissionError(f"Missing required scope: {scope}")


def _extract_bearer_token(request: Request) -> str:
    authorization = (request.headers.get("Authorization") or "").strip()
    scheme, separator, token = authorization.partition(" ")
    if separator != " " or scheme.lower() != "bearer" or not token.strip():
        raise AuthenticationError("Missing Bearer token")
    return token.strip()


def _keys_dir() -> Path:
    configured = os.getenv("JWT_PUBLIC_KEYS_DIR")
    return Path(configured).resolve() if configured else _DEFAULT_KEYS_DIR


def read_bootstrap_public_key(kid: str) -> str:
    public_key_path = _keys_dir() / f"{kid}.pem"
    try:
        return public_key_path.read_text(encoding="utf-8")
    except FileNotFoundError as exc:
        raise AuthenticationError("Unknown JWT signing key") from exc
    except OSError as exc:
        raise AuthenticationError("JWT signing key is unavailable") from exc


def _registered_key(kid: str) -> dict[str, Any] | None:
    try:
        return get_signing_key_record(kid)
    except SQLAlchemyError:
        # Additive Phase 1 bootstrap: before the control-plane migration exists,
        # only an explicitly pretrusted public-key file may authenticate.
        return None


def _csv_values(raw: str) -> set[str]:
    return {value.strip().rstrip("/") for value in raw.split(",") if value.strip()}


def _required_string_claim(claims: dict[str, Any], name: str) -> str:
    value = str(claims.get(name) or "").strip()
    if not value:
        raise AuthenticationError(f"Missing JWT claim: {name}")
    return value


def _string_tuple(value: Any) -> tuple[str, ...]:
    if not isinstance(value, list):
        return ()
    return tuple(str(item).strip() for item in value if str(item).strip())
