// Recovery (backup) code store. Logic layer over recoveryCodes.db, doing the
// bcrypt hashing itself — the same pattern as users.model hashing passwords (a
// recovery code is a human-typed, lower-entropy fallback credential, so bcrypt,
// not sha256; see docs/website/TRUSTED_DEVICES_MFA.md §3). Codes are generated in
// batches, shown to the user once, and consumed single-use at login.

const crypto = require('crypto')
const bcrypt = require('bcryptjs')

const db = require('./recoveryCodes.db')

const SALT_ROUNDS = 10
const CODE_COUNT = Number(process.env.RECOVERY_CODE_COUNT) || 10
// 10 chars from a 32-symbol alphabet ≈ 50 bits of entropy per code. Crockford-ish
// base32 minus visually ambiguous glyphs (no I, L, O, U) to keep hand-entry clean.
const ALPHABET = '23456789ABCDEFGHJKMNPQRSTVWXYZ'
const CODE_LEN = 10

// Canonical form used for hashing + comparison: uppercase, alphanumerics only.
// Display adds a dash for readability; input is normalized back to this before
// bcrypt.compare so 'abcde-fghij', 'ABCDEFGHIJ', etc. all verify.
function normalize(code) {
  return String(code || '').toUpperCase().replace(/[^0-9A-Z]/g, '')
}

// One random code in canonical form (no separator).
function generateCode() {
  const bytes = crypto.randomBytes(CODE_LEN)
  let out = ''
  for (let i = 0; i < CODE_LEN; i++) out += ALPHABET[bytes[i] % ALPHABET.length]
  return out
}

// Present a canonical code to the user with a mid-string dash (display only).
function formatForDisplay(code) {
  const mid = Math.floor(code.length / 2)
  return `${code.slice(0, mid)}-${code.slice(mid)}`
}

// Generate a fresh batch, REPLACING any existing codes for the user (regeneration
// invalidates the old set). Returns the plaintext codes for one-time display — the
// only time they exist outside the user's hands.
async function generateForUser(userId, count = CODE_COUNT) {
  const plain = Array.from({ length: count }, generateCode)
  const hashes = await Promise.all(plain.map((c) => bcrypt.hash(c, SALT_ROUNDS)))
  await db.deleteAllForUser(userId)
  await db.insertMany(userId, hashes)
  return plain.map(formatForDisplay)
}

// Verify + consume a recovery code (single-use). Normalizes input, bcrypt-compares
// against the user's unused codes, and marks the first match used. Returns true iff
// a code was consumed. Timing is dominated by bcrypt regardless of match position.
async function consumeForUser(userId, rawCode) {
  const candidate = normalize(rawCode)
  if (!candidate) return false
  const rows = await db.listUnusedForUser(userId)
  for (const row of rows) {
    // eslint-disable-next-line no-await-in-loop
    if (await bcrypt.compare(candidate, row.code_hash)) {
      // eslint-disable-next-line no-await-in-loop
      const changed = await db.markUsed(row.id)
      return changed > 0 // lost the race to consume this exact code → treat as fail
    }
  }
  return false
}

// Remaining (unused) code count — for the status endpoint. Never returns codes.
async function remainingForUser(userId) {
  return db.countUnusedForUser(userId)
}

// Clear every code for a user (TOTP disable / password change/reset).
async function clearForUser(userId) {
  return db.deleteAllForUser(userId)
}

module.exports = {
  CODE_COUNT,
  normalize,
  generateForUser,
  consumeForUser,
  remainingForUser,
  clearForUser,
}
