from __future__ import annotations

from dataclasses import dataclass
import base64
import hashlib
import hmac
import secrets
import string


_OTP_ALPHABET = string.ascii_letters + string.digits


@dataclass(frozen=True)
class PasswordHash:
    salt_b64: str
    iterations: int
    hash_b64: str


def hash_password(password: str, *, iterations: int = 210_000) -> PasswordHash:
    if password == "":
        raise ValueError("password must not be empty")
    salt = secrets.token_bytes(16)
    derived = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt, iterations)
    return PasswordHash(
        salt_b64=base64.b64encode(salt).decode("ascii"),
        iterations=iterations,
        hash_b64=base64.b64encode(derived).decode("ascii"),
    )


def verify_password(password: str, password_hash: PasswordHash) -> bool:
    salt = base64.b64decode(password_hash.salt_b64.encode("ascii"))
    expected = base64.b64decode(password_hash.hash_b64.encode("ascii"))
    derived = hashlib.pbkdf2_hmac(
        "sha256", password.encode("utf-8"), salt, int(password_hash.iterations)
    )
    return hmac.compare_digest(derived, expected)


def new_session_token() -> str:
    return secrets.token_urlsafe(32)


def sha256_hex(value: str) -> str:
    return hashlib.sha256(value.encode("utf-8")).hexdigest()


def generate_otp32() -> str:
    return "".join(secrets.choice(_OTP_ALPHABET) for _ in range(32))


def clamp_push_preview(text: str, limit: int = 12) -> str:
    if text is None:
        return ""
    text = text.strip()
    if text == "":
        return ""
    return text[:limit]

