import { randomBytes } from "node:crypto";
import { hash, verify } from "@node-rs/argon2";

// Backup codes are the last-resort credential when a user loses their TOTP
// device. Design constraints:
//
//   1. High entropy but human-typeable. 10 chars of Crockford-base32 =
//      50 bits — well above the 20-bit floor that brute-force-proofs the
//      6 codes/15 min rate limit (2^20 / (6/900) ≈ 5000 years average).
//   2. Never logged or stored in plaintext. We hash with argon2id (same
//      hasher as passwords) and delete the row on redemption, so replay is
//      physically impossible even if the DB leaks post-redemption.
//   3. One-shot visibility. Plaintext is returned exactly once from the
//      generate mutation — re-display is not supported; lost codes must be
//      regenerated, which invalidates the full set.
//
// The formatted shape (XXXXX-XXXXX) is cosmetic only; validation strips the
// dash so users can paste either form.

export const BACKUP_CODE_COUNT = 10;
const CODE_LENGTH = 10; // chars, pre-dash
// Crockford base32 alphabet: no 0/O/1/I/L to avoid transcription errors.
const ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";

export function generatePlaintextBackupCodes(count: number = BACKUP_CODE_COUNT): string[] {
  const codes: string[] = [];
  for (let i = 0; i < count; i++) {
    const bytes = randomBytes(CODE_LENGTH);
    let code = "";
    for (let j = 0; j < CODE_LENGTH; j++) {
      code += ALPHABET[bytes[j]! % ALPHABET.length];
    }
    codes.push(`${code.slice(0, 5)}-${code.slice(5)}`);
  }
  return codes;
}

// Users may paste the code with or without the dash, and in either case;
// store and compare the canonical form (uppercase, no dash, no whitespace)
// so accidental formatting does not reject an otherwise-valid code.
export function normalizeBackupCode(input: string): string {
  return input.replace(/[\s-]+/g, "").toUpperCase();
}

export async function hashBackupCode(plaintext: string): Promise<string> {
  return hash(normalizeBackupCode(plaintext));
}

export async function verifyBackupCode(codeHash: string, plaintext: string): Promise<boolean> {
  try {
    return await verify(codeHash, normalizeBackupCode(plaintext));
  } catch {
    return false;
  }
}
