"""JWT access and refresh tokens.

Access tokens carry the active workspace as a ``ws`` claim. Refresh tokens are
type-checked and rotated on use (build plan Section 14).
"""

from __future__ import annotations

import uuid
from dataclasses import dataclass
from datetime import UTC, datetime, timedelta

import jwt

from legate.config import get_settings


class TokenError(Exception):
    """Raised when a token is missing, malformed, expired, or the wrong type."""


@dataclass(frozen=True)
class TokenPayload:
    user_id: uuid.UUID
    workspace_id: uuid.UUID | None
    token_type: str
    jti: str


def _now() -> datetime:
    return datetime.now(UTC)


def _encode(
    *,
    user_id: uuid.UUID,
    token_type: str,
    expires_delta: timedelta,
    workspace_id: uuid.UUID | None,
) -> str:
    settings = get_settings()
    issued = _now()
    claims = {
        "sub": str(user_id),
        "type": token_type,
        "ws": str(workspace_id) if workspace_id else None,
        "iat": int(issued.timestamp()),
        "exp": int((issued + expires_delta).timestamp()),
        "jti": uuid.uuid4().hex,
    }
    return jwt.encode(claims, settings.legate_secret_key, algorithm=settings.jwt_algorithm)


def create_access_token(user_id: uuid.UUID, workspace_id: uuid.UUID | None = None) -> str:
    settings = get_settings()
    return _encode(
        user_id=user_id,
        token_type="access",
        expires_delta=timedelta(minutes=settings.jwt_access_ttl_min),
        workspace_id=workspace_id,
    )


def create_refresh_token(user_id: uuid.UUID) -> str:
    settings = get_settings()
    return _encode(
        user_id=user_id,
        token_type="refresh",
        expires_delta=timedelta(days=settings.jwt_refresh_ttl_days),
        workspace_id=None,
    )


def decode_token(token: str, *, expected_type: str | None = None) -> TokenPayload:
    settings = get_settings()
    try:
        claims = jwt.decode(token, settings.legate_secret_key, algorithms=[settings.jwt_algorithm])
    except jwt.PyJWTError as exc:  # expired, bad signature, malformed, ...
        raise TokenError(str(exc)) from exc

    token_type = claims.get("type")
    if expected_type is not None and token_type != expected_type:
        raise TokenError(f"expected {expected_type} token, got {token_type}")

    try:
        user_id = uuid.UUID(claims["sub"])
    except (KeyError, ValueError) as exc:
        raise TokenError("invalid subject") from exc

    ws_raw = claims.get("ws")
    workspace_id = uuid.UUID(ws_raw) if ws_raw else None

    return TokenPayload(
        user_id=user_id,
        workspace_id=workspace_id,
        token_type=token_type or "",
        jti=claims.get("jti", ""),
    )
