import { randomBytes, scrypt as scryptCb, timingSafeEqual, createHash } from 'node:crypto';
import { promisify } from 'node:util';

const scrypt = promisify(scryptCb) as (
  password: string,
  salt: Buffer,
  keyLen: number,
) => Promise<Buffer>;

const KEY_LENGTH = 64;
const SALT_LENGTH = 16;

/**
 * Hash a password with a fresh random salt. Stored format is
 * `salt:keyHex` (both as hex strings) so verification can re-derive without
 * a separate salt column.
 */
export async function hashPassword(password: string): Promise<string> {
  const salt = randomBytes(SALT_LENGTH);
  const key = await scrypt(password, salt, KEY_LENGTH);
  return `${salt.toString('hex')}:${key.toString('hex')}`;
}

/**
 * Constant-time check of a candidate password against a stored
 * `salt:keyHex` hash. Returns false on any malformed input rather than
 * throwing - callers should treat false uniformly.
 */
export async function verifyPassword(password: string, stored: string): Promise<boolean> {
  const parts = stored.split(':');
  if (parts.length !== 2) return false;
  const [saltHex, keyHex] = parts;
  if (!saltHex || !keyHex) return false;
  let salt: Buffer;
  let expected: Buffer;
  try {
    salt = Buffer.from(saltHex, 'hex');
    expected = Buffer.from(keyHex, 'hex');
  } catch {
    return false;
  }
  if (expected.length !== KEY_LENGTH) return false;
  const candidate = await scrypt(password, salt, KEY_LENGTH);
  return timingSafeEqual(candidate, expected);
}

/**
 * Mint a fresh raw token (returned to the caller) and its SHA-256 hash
 * (stored in the DB). The raw token is meant to be embedded in a one-shot
 * URL; only the hash persists.
 */
export function mintToken(byteLength = 32): { raw: string; hash: string } {
  const raw = randomBytes(byteLength).toString('base64url');
  const hash = createHash('sha256').update(raw).digest('hex');
  return { raw, hash };
}

export function hashToken(raw: string): string {
  return createHash('sha256').update(raw).digest('hex');
}
