"""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 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)