"""Core security utilities: JWT, password hashing, MFA (TOTP)."""

import hashlib
import secrets
from datetime import datetime, timedelta, timezone

import pyotp
from jose import jwt, JWTError  # noqa: F401 – re-exported for convenience

# ---------------------------------------------------------------------------
# Monkey-patch passlib for bcrypt >= 4.1 / 5.x compatibility.
# Newer bcrypt removed ``__about__`` and rejects >72-byte passwords in its
# internal wrap-bug detection.  The patch is applied before CryptContext is
# instantiated so that passlib's backend initialisation succeeds.
# ---------------------------------------------------------------------------
import passlib.handlers.bcrypt as _bcrypt_mod  # noqa: E402

_orig_finalize = _bcrypt_mod._BcryptBackend._finalize_backend_mixin.__func__  # type: ignore[attr-defined]


@classmethod  # type: ignore[misc]
def _patched_finalize(cls, name: str, dryrun: bool = False):  # type: ignore[no-untyped-def]
    try:
        return _orig_finalize(cls, name, dryrun)
    except ValueError:
        # bcrypt 4.1+ raises ValueError on >72-byte secrets during
        # passlib's internal wrap-bug detection — safe to ignore.
        return True


_bcrypt_mod._BcryptBackend._finalize_backend_mixin = _patched_finalize  # type: ignore[assignment]
# ---------------------------------------------------------------------------

from passlib.context import CryptContext  # noqa: E402

from app.config import get_settings

settings = get_settings()

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")


# ---------------------------------------------------------------------------
# Password hashing
# ---------------------------------------------------------------------------

def hash_password(password: str) -> str:
    """Hash a plain-text password using bcrypt."""
    return pwd_context.hash(password)


def verify_password(plain: str, hashed: str) -> bool:
    """Verify a plain-text password against a bcrypt hash."""
    return pwd_context.verify(plain, hashed)


# ---------------------------------------------------------------------------
# JWT tokens
# ---------------------------------------------------------------------------

def create_access_token(user_id: int, role: str) -> str:
    """Create a short-lived JWT access token."""
    expire = datetime.now(timezone.utc) + timedelta(
        minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES
    )
    payload = {
        "sub": str(user_id),
        "role": role,
        "exp": expire,
    }
    return jwt.encode(
        payload, settings.JWT_SECRET_KEY, algorithm=settings.JWT_ALGORITHM
    )


def create_refresh_token() -> str:
    """Create a cryptographically secure refresh token (URL-safe)."""
    return secrets.token_urlsafe(64)


def hash_token(token: str) -> str:
    """Return a SHA-256 hex digest of *token* (used for DB storage)."""
    return hashlib.sha256(token.encode()).hexdigest()


def decode_access_token(token: str) -> dict:
    """Decode and validate a JWT access token.

    Raises ``jose.JWTError`` on invalid or expired tokens.
    """
    return jwt.decode(
        token, settings.JWT_SECRET_KEY, algorithms=[settings.JWT_ALGORITHM]
    )


# ---------------------------------------------------------------------------
# MFA / TOTP
# ---------------------------------------------------------------------------

def generate_mfa_secret() -> str:
    """Generate a new TOTP base-32 secret."""
    return pyotp.random_base32()


def verify_mfa_code(secret: str, code: str) -> bool:
    """Verify a 6-digit TOTP code against *secret*."""
    totp = pyotp.TOTP(secret)
    return totp.verify(code)


def get_mfa_uri(secret: str, email: str) -> str:
    """Return an ``otpauth://`` provisioning URI for QR code generation."""
    totp = pyotp.TOTP(secret)
    return totp.provisioning_uri(name=email, issuer_name=settings.APP_NAME)
