"""Password hashing with stdlib scrypt (Argon2id is not used)."""

from __future__ import annotations

import base64
import hashlib
import hmac
import secrets

_SCRYPT_N = 2**17
_SCRYPT_R = 8
_SCRYPT_P = 1
_SCRYPT_DKLEN = 64
_SCRYPT_SALT_BYTES = 16
_SCRYPT_MAXMEM = 128 * _SCRYPT_N * _SCRYPT_R * (_SCRYPT_P + 2)
_ALGORITHM = "scrypt"


def _password_material(password: str, config_salt: str) -> str:
    if not config_salt:
        return password
    return f"{config_salt}:{password}"


def _scrypt_key(password: str, *, salt: bytes, n: int, r: int, p: int, dklen: int) -> bytes:
    return hashlib.scrypt(
        password.encode("utf-8"),
        salt=salt,
        n=n,
        r=r,
        p=p,
        maxmem=max(128 * n * r * (p + 2), _SCRYPT_MAXMEM),
        dklen=dklen,
    )


def hash_password(password: str, *, config_salt: str = "") -> str:
    material = _password_material(password, config_salt)
    salt = secrets.token_bytes(_SCRYPT_SALT_BYTES)
    key = _scrypt_key(material, salt=salt, n=_SCRYPT_N, r=_SCRYPT_R, p=_SCRYPT_P, dklen=_SCRYPT_DKLEN)
    salt_b64 = base64.b64encode(salt).decode("ascii")
    key_b64 = base64.b64encode(key).decode("ascii")
    return f"{_ALGORITHM}${_SCRYPT_N}${_SCRYPT_R}${_SCRYPT_P}${salt_b64}${key_b64}"


def is_password_hash(value: str) -> bool:
    parts = value.split("$")
    if len(parts) != 6 or parts[0] != _ALGORITHM:
        return False
    try:
        int(parts[1])
        int(parts[2])
        int(parts[3])
        base64.b64decode(parts[4], validate=True)
        base64.b64decode(parts[5], validate=True)
    except (ValueError, TypeError):
        return False
    return True


def verify_password(password: str, pass_hash: str, *, config_salt: str = "") -> bool:
    if not pass_hash:
        return False
    if config_salt and _verify_password_material(_password_material(password, config_salt), pass_hash):
        return True
    return _verify_password_material(password, pass_hash)


def _verify_password_material(material: str, pass_hash: str) -> bool:
    if not pass_hash:
        return False
    parts = pass_hash.split("$")
    if len(parts) != 6 or parts[0] != _ALGORITHM:
        return False
    try:
        n = int(parts[1])
        r = int(parts[2])
        p = int(parts[3])
        if n < _SCRYPT_N or r < _SCRYPT_R or p < _SCRYPT_P:
            return False
        salt = base64.b64decode(parts[4], validate=True)
        expected = base64.b64decode(parts[5], validate=True)
    except (ValueError, TypeError):
        return False
    try:
        actual = _scrypt_key(material, salt=salt, n=n, r=r, p=p, dklen=len(expected))
    except ValueError:
        return False
    return hmac.compare_digest(actual, expected)