"""Password hashing, JWT helpers, and secure random token utilities."""

from __future__ import annotations

import base64
import secrets
from datetime import UTC, datetime, timedelta
from typing import Any

import bcrypt
import jwt
from jwt.exceptions import ExpiredSignatureError
from jwt.exceptions import InvalidTokenError as JWTInvalidTokenError

from aura_auth._core.exceptions import InvalidPasswordError, InvalidTokenError, TokenExpiredError


class PasswordHelper:
    """Handles password hashing and verification using bcrypt."""

    def __init__(self, min_length: int = 8) -> None:
        self._min_length = min_length

    def hash(self, password: str) -> str:
        """Return a bcrypt hash for the given password."""
        self.validate_strength(password)
        hashed = bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt())
        return hashed.decode("utf-8")

    def verify(self, password: str, hashed: str) -> bool:
        """Return True if the password matches the bcrypt hash."""
        try:
            return bcrypt.checkpw(password.encode("utf-8"), hashed.encode("utf-8"))
        except ValueError:
            return False

    def validate_strength(self, password: str) -> None:
        """Enforce minimum password length.

        Raises:
            InvalidPasswordError: If the password is too short or empty.
        """
        if len(password) < self._min_length:
            msg = f"Password must be at least {self._min_length} characters"
            raise InvalidPasswordError(msg)


class TokenHelper:
    """Creates and validates JWT tokens."""

    def __init__(self, secret: str, algorithm: str = "HS256", lifetime_seconds: int = 3600) -> None:
        self._secret = secret
        self._algorithm = algorithm
        self._lifetime_seconds = lifetime_seconds

    def create_token(self, user_id: str, extra_claims: dict[str, Any] | None = None) -> str:
        """Create a signed JWT with standard claims.

        Args:
            user_id: Subject claim for the authenticated user.
            extra_claims: Optional additional JWT claims.

        Returns:
            Encoded JWT string.

        Raises:
            ValueError: If user_id is empty.
        """
        if not user_id:
            msg = "user_id must be non-empty"
            raise ValueError(msg)
        now = datetime.now(tz=UTC)
        exp = now + timedelta(seconds=self._lifetime_seconds)
        payload: dict[str, Any] = {
            "sub": user_id,
            "iat": int(now.timestamp()),
            "exp": int(exp.timestamp()),
            "jti": secrets.token_urlsafe(16),
        }
        if extra_claims:
            payload.update(extra_claims)
        return jwt.encode(payload, self._secret, algorithm=self._algorithm)

    def decode_token(self, token: str) -> dict[str, Any]:
        """Decode and validate a JWT, returning its payload.

        Raises:
            TokenExpiredError: If the token is expired.
            InvalidTokenError: If the token is invalid or missing required claims.
        """
        try:
            payload = jwt.decode(token, self._secret, algorithms=[self._algorithm])
        except ExpiredSignatureError as exc:
            raise TokenExpiredError("Token has expired") from exc
        except JWTInvalidTokenError as exc:
            raise InvalidTokenError("Invalid token") from exc
        if "sub" not in payload:
            raise InvalidTokenError("Token missing subject")
        return payload


def generate_random_token(length: int = 32) -> str:
    """Generate a URL-safe random token without ``+``, ``/``, or ``=`` padding.

    Args:
        length: Number of random bytes used to build the token before encoding.

    Returns:
        A URL-safe base64 string without padding.
    """
    raw = secrets.token_bytes(length)
    return base64.urlsafe_b64encode(raw).decode("ascii").rstrip("=")
