import {
  randomBytes,
  scrypt as scryptCallback,
  timingSafeEqual,
  type ScryptOptions,
} from "node:crypto";

// Kept free of `server-only` so CLI scripts (seed, create-admin) can import it.

const scrypt = (
  password: string,
  salt: Buffer,
  keylen: number,
  options: ScryptOptions,
) =>
  new Promise<Buffer>((resolve, reject) => {
    scryptCallback(password, salt, keylen, options, (error, key) =>
      error ? reject(error) : resolve(key),
    );
  });

// scrypt N=2^15, r=8, p=1: OWASP's minimum, and well under 100ms per login on small servers.
const COST = 2 ** 15;
const BLOCK_SIZE = 8;
const PARALLELISM = 1;
const KEY_LENGTH = 64;
const MAX_MEMORY = 128 * COST * BLOCK_SIZE * 2;

/** Hashes a password as `scrypt$N$r$p$salt$key` (base64url). */
export async function hashPassword(password: string) {
  const salt = randomBytes(16);
  const key = await scrypt(password.normalize("NFKC"), salt, KEY_LENGTH, {
    N: COST,
    r: BLOCK_SIZE,
    p: PARALLELISM,
    maxmem: MAX_MEMORY,
  });
  return [
    "scrypt",
    COST,
    BLOCK_SIZE,
    PARALLELISM,
    salt.toString("base64url"),
    key.toString("base64url"),
  ].join("$");
}

export async function verifyPassword(password: string, stored: string) {
  const [algorithm, n, r, p, saltValue, keyValue] = stored.split("$");
  if (algorithm !== "scrypt" || !saltValue || !keyValue) return false;

  const expected = Buffer.from(keyValue, "base64url");
  const actual = await scrypt(
    password.normalize("NFKC"),
    Buffer.from(saltValue, "base64url"),
    expected.length,
    { N: Number(n), r: Number(r), p: Number(p), maxmem: MAX_MEMORY },
  );
  return actual.length === expected.length && timingSafeEqual(actual, expected);
}

let dummyHash: Promise<string> | undefined;

/**
 * Takes as long as a real check, so a login for an unknown email can't be told
 * apart from a wrong password by response time.
 */
export async function verifyAgainstDummy(password: string) {
  dummyHash ??= hashPassword(randomBytes(24).toString("base64url"));
  await verifyPassword(password, await dummyHash);
  return false;
}
