"""Passwords, JWT access tokens, and the current-user dependency."""
import uuid
from dataclasses import dataclass
from datetime import timedelta
from typing import Annotated

import bcrypt
import jwt
from fastapi import Depends, Request
from fastapi.concurrency import run_in_threadpool
from fastapi.security import OAuth2PasswordBearer

from .config import Settings
from .db import utcnow
from .errors import ApiError

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/v1/auth/token", auto_error=False)
ALGORITHM = "HS256"
BEARER = {"WWW-Authenticate": "Bearer"}


@dataclass(frozen=True)
class Principal:
    id: int
    email: str
    role: str

    @property
    def is_admin(self) -> bool:
        return self.role == "admin"


def hash_password(password: str, rounds: int) -> str:
    return bcrypt.hashpw(password.encode(), bcrypt.gensalt(rounds)).decode()


async def verify_password(password: str, password_hash: str) -> bool:
    """bcrypt is deliberately slow (CPU): run it in the thread pool, never on the event loop."""
    try:
        return await run_in_threadpool(bcrypt.checkpw, password.encode(), password_hash.encode())
    except ValueError:            # bcrypt 5 rejects passwords over 72 bytes
        return False


def create_access_token(principal: Principal, settings: Settings) -> tuple[str, int]:
    now = utcnow()
    lifetime = timedelta(minutes=settings.access_token_minutes)
    claims = {"sub": str(principal.id), "email": principal.email, "role": principal.role,
              "iss": settings.jwt_issuer, "aud": settings.jwt_audience,
              "iat": now, "exp": now + lifetime, "jti": uuid.uuid4().hex}
    token = jwt.encode(claims, settings.jwt_secret.get_secret_value(), algorithm=ALGORITHM)
    return token, int(lifetime.total_seconds())


def decode_access_token(token: str, settings: Settings) -> Principal:
    claims = jwt.decode(token, settings.jwt_secret.get_secret_value(), algorithms=[ALGORITHM],
                        audience=settings.jwt_audience, issuer=settings.jwt_issuer,
                        options={"require": ["exp", "iat", "sub", "aud", "iss"]})
    return Principal(id=int(claims["sub"]), email=claims["email"], role=claims["role"])


async def current_user(request: Request, token: Annotated[str | None, Depends(oauth2_scheme)]) -> Principal:
    if token is None:
        raise ApiError(401, "missing bearer token", type_="unauthenticated", headers=BEARER)
    try:
        principal = decode_access_token(token, request.state.settings)
    except jwt.PyJWTError:
        raise ApiError(401, "invalid or expired token", type_="unauthenticated", headers=BEARER) from None
    request.state.principal = principal               # for the access log and the rate limiter
    return principal


CurrentUser = Annotated[Principal, Depends(current_user)]


async def require_admin(principal: CurrentUser) -> Principal:
    if not principal.is_admin:
        raise ApiError(403, "this operation requires the admin role", type_="forbidden")
    return principal


Admin = Annotated[Principal, Depends(require_admin)]
