from __future__ import annotations

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


class PasswordPolicyError(ValueError):
    pass


@dataclass(frozen=True, slots=True)
class PasswordHasher:
    work_factor: int = 2**15
    block_size: int = 8
    parallelism: int = 1
    salt_bytes: int = 16
    key_bytes: int = 32

    def validate(self, password: str) -> None:
        if not 8 <= len(password) <= 128:
            raise PasswordPolicyError("密码长度应为8至128位。")
        categories = (
            any(character.isalpha() for character in password),
            any(character.isdigit() for character in password),
            any(not character.isalnum() for character in password),
        )
        if sum(categories) < 2:
            raise PasswordPolicyError("密码至少包含字母、数字、符号中的两类。")

    def hash(self, password: str) -> str:
        self.validate(password)
        salt = secrets.token_bytes(self.salt_bytes)
        digest = hashlib.scrypt(
            password.encode("utf-8"),
            salt=salt,
            n=self.work_factor,
            r=self.block_size,
            p=self.parallelism,
            dklen=self.key_bytes,
            maxmem=64 * 1024 * 1024,
        )
        encoded_salt = base64.urlsafe_b64encode(salt).decode("ascii")
        encoded_digest = base64.urlsafe_b64encode(digest).decode("ascii")
        return (
            f"scrypt${self.work_factor}${self.block_size}${self.parallelism}"
            f"${encoded_salt}${encoded_digest}"
        )

    def verify(self, password: str, encoded: str) -> bool:
        try:
            scheme, work_factor, block_size, parallelism, salt, expected = encoded.split("$")
            if scheme != "scrypt":
                return False
            digest = hashlib.scrypt(
                password.encode("utf-8"),
                salt=base64.urlsafe_b64decode(salt.encode("ascii")),
                n=int(work_factor),
                r=int(block_size),
                p=int(parallelism),
                dklen=self.key_bytes,
                maxmem=64 * 1024 * 1024,
            )
            return hmac.compare_digest(base64.urlsafe_b64encode(digest).decode("ascii"), expected)
        except (TypeError, ValueError):
            return False
