import { verifyBackupCode } from "./mfa-backup-codes.js";

// Redeem a backup code atomically. The flow is:
//
//   1. Load all still-redeemable rows (usedAt IS NULL) for the user.
//   2. Linear-scan with argon2 verify until one matches. Hashes are
//      expensive by design — 10 candidates max is fine, and the cost is
//      the user's own memory-hard-hash budget, not an attacker-chosen one.
//   3. The matching row is deleted under a WHERE-guard on (id, usedAt IS
//      NULL). Count=0 means another request consumed the same code first
//      (replay race); the caller treats it as an invalid code.
//
// Deleting (vs marking `usedAt`) keeps the table small and makes post-
// compromise forensics simpler — a used code is an absence, not a
// still-present-but-tombstoned row that could be reactivated via SQL
// injection or bad migration.
//
// Returned `remaining` lets the UI warn "3 backup codes left — generate
// more" without a second round-trip.

interface BackupCodeRow {
  id: string;
  codeHash: string;
}

interface RedeemDb {
  mfaBackupCode: {
    findMany: (args: {
      where: { userId: string; usedAt: null };
      select: { id: true; codeHash: true };
    }) => Promise<BackupCodeRow[]>;
    deleteMany: (args: { where: { id: string; usedAt: null } }) => Promise<{ count: number }>;
    count: (args: { where: { userId: string; usedAt: null } }) => Promise<number>;
  };
}

export interface RedeemResult {
  accepted: boolean;
  remaining: number;
}

export async function redeemBackupCode(
  db: { mfaBackupCode: unknown },
  userId: string,
  plaintext: string,
): Promise<RedeemResult> {
  const typed = db as unknown as RedeemDb;

  const rows = await typed.mfaBackupCode.findMany({
    where: { userId, usedAt: null },
    select: { id: true, codeHash: true },
  });

  for (const row of rows) {
    if (!(await verifyBackupCode(row.codeHash, plaintext))) continue;

    const del = await typed.mfaBackupCode.deleteMany({
      where: { id: row.id, usedAt: null },
    });
    if (del.count === 0) {
      // Raced — another request consumed this same code. Treat as invalid
      // so the attacker cannot learn it was valid; an honest user retries
      // with a fresh code.
      return {
        accepted: false,
        remaining: await typed.mfaBackupCode.count({ where: { userId, usedAt: null } }),
      };
    }
    const remaining = await typed.mfaBackupCode.count({ where: { userId, usedAt: null } });
    return { accepted: true, remaining };
  }

  return { accepted: false, remaining: rows.length };
}
