/**
 * Secret hashing at rest (INV5). Pairing codes and v0.8 tokens are stored as salted scrypt
 * hashes, never raw. Verification is constant-time. scrypt is a Node builtin (no native dep);
 * argon2id would be the production upgrade (integration note) but scrypt satisfies INV5 here.
 */
import { randomBytes, scryptSync, timingSafeEqual, createHash } from 'node:crypto'

/**
 * Deterministic hash for high-entropy lookup keys (pairing codes: ≥128-bit input, so a fast
 * preimage-resistant hash is safe at rest — the input space is infeasible to brute-force, INV5).
 * A salted scrypt cannot be used here because redemption must find the row by hash of the
 * presented code. Production may HMAC this with a server-side pepper (integration note).
 */
export function sha256Hex(raw: string): string {
  return createHash('sha256').update(raw, 'utf8').digest('hex')
}

const SCRYPT_KEYLEN = 32
const SCRYPT_COST = 1 << 14 // N=16384 — modest, fast enough for tests, memory-hard
const SALT_LEN = 16

/** Produce a self-describing `scrypt$<saltHex>$<hashHex>` string. Raw secret is discarded. */
export function hashSecret(raw: string): string {
  const salt = randomBytes(SALT_LEN)
  const hash = scryptSync(raw, salt, SCRYPT_KEYLEN, { N: SCRYPT_COST })
  return `scrypt$${salt.toString('hex')}$${hash.toString('hex')}`
}

/** Constant-time verify a raw secret against a stored `scrypt$salt$hash`. */
export function verifySecret(raw: string, stored: string): boolean {
  const parts = stored.split('$')
  if (parts.length !== 3 || parts[0] !== 'scrypt') return false
  const saltHex = parts[1] as string
  const hashHex = parts[2] as string
  let expected: Buffer
  try {
    expected = Buffer.from(hashHex, 'hex')
    const salt = Buffer.from(saltHex, 'hex')
    const actual = scryptSync(raw, salt, expected.length, { N: SCRYPT_COST })
    return timingSafeEqual(actual, expected)
  } catch {
    return false
  }
}
