import { randomBytes, scrypt as scryptCallback, timingSafeEqual } from "node:crypto";
import { promisify } from "node:util";
import { DomainError } from "../../shared/errors.ts";

const scrypt = promisify(scryptCallback);
const CURRENT_N = 16_384;
const CURRENT_R = 8;
const CURRENT_P = 1;
const KEY_LENGTH = 64;

export function validatePassword(password: string): void {
  if (password.length < 12 || password.length > 256) {
    throw new DomainError("INVALID_PASSWORD", "Password must contain 12 to 256 characters.");
  }
}

export async function hashPassword(password: string): Promise<string> {
  validatePassword(password);
  const salt = randomBytes(16);
  const derived = await scrypt(password, salt, KEY_LENGTH, { N: CURRENT_N, r: CURRENT_R, p: CURRENT_P, maxmem: 64 * 1024 * 1024 }) as Buffer;
  return `scrypt$${CURRENT_N}$${CURRENT_R}$${CURRENT_P}$${salt.toString("base64url")}$${derived.toString("base64url")}`;
}

export async function verifyPassword(password: string, encoded: string): Promise<boolean> {
  try {
    const [algorithm, n, r, p, saltValue, digestValue] = encoded.split("$");
    if (algorithm !== "scrypt" || !n || !r || !p || !saltValue || !digestValue) return false;
    const parameters = { N: Number(n), r: Number(r), p: Number(p) };
    if (!Number.isSafeInteger(parameters.N) || parameters.N < 2 || parameters.N > 1_048_576) return false;
    if (!Number.isSafeInteger(parameters.r) || parameters.r < 1 || parameters.r > 32) return false;
    if (!Number.isSafeInteger(parameters.p) || parameters.p < 1 || parameters.p > 16) return false;
    const expected = Buffer.from(digestValue, "base64url");
    if (expected.length !== KEY_LENGTH) return false;
    const actual = await scrypt(password, Buffer.from(saltValue, "base64url"), expected.length, { ...parameters, maxmem: 128 * 1024 * 1024 }) as Buffer;
    return timingSafeEqual(actual, expected);
  } catch {
    return false;
  }
}

export function passwordHashNeedsUpgrade(encoded: string): boolean {
  const [algorithm, n, r, p] = encoded.split("$");
  return algorithm !== "scrypt" || Number(n) !== CURRENT_N || Number(r) !== CURRENT_R || Number(p) !== CURRENT_P;
}
