/**
 * AUDIT-2 #5 maintenance: re-encrypt rows whose envelope still uses
 * the legacy `v1:` / `v2:` prefix as `enc_v1:` / `enc_v2:`.
 *
 * Background:
 *   The original `v1:` / `v2:` prefixes collided with plaintext values
 *   (a monitor header literally starting with "v1:" was misclassified).
 *   The new prefixes are unlikely to collide (6+ chars). This script
 *   walks every column known to carry encrypted data, decrypts rows
 *   whose value starts with the legacy prefix, and re-encrypts with
 *   the new prefix (preserving any AAD the caller used).
 *
 * Usage:
 *   # Dry-run (default) — print what would change, do not write.
 *   npx tsx scripts/maint/reencrypt-legacy-prefixes.ts
 *
 *   # Apply the migration.
 *   npx tsx scripts/maint/reencrypt-legacy-prefixes.ts --apply
 *
 * Idempotent: rows already on the new prefix are skipped. Safe to re-run.
 *
 * Affected columns (all known callers as of 2026-05-23):
 *   - NotificationChannel.config  (no AAD)
 *   - User.twoFactorSecret        (no AAD)
 *   - SystemSetting.value         (no AAD — SMTP password etc.)
 *   - MonitorHeader.value         (no AAD)
 *
 * Note: as of AUDIT-7 the v2 envelope (with AAD) was the infrastructure
 * — no production caller passed an AAD yet. So all encrypted rows on
 * a deployed system today use the v1 envelope (no AAD). This script
 * handles v2 anyway for future-proofing, but the AAD-discovery path
 * is not exercised on current deployments.
 */
import { PrismaClient } from '@prisma/client';
import { isEncrypted, decrypt, encrypt } from '../../src/lib/crypto/secret-vault';

const prisma = new PrismaClient();

const APPLY = process.argv.includes('--apply');

function startsWithLegacy(value: string): boolean {
    return value.startsWith('v1:') || value.startsWith('v2:');
}

interface Row {
    table: string;
    id: number | string;
    column: string;
    aad?: string;
    value: string;
}

async function collectLegacyRows(): Promise<Row[]> {
    const rows: Row[] = [];

    // 1. NotificationChannel.config
    const channels = await prisma.notificationChannel.findMany({ select: { id: true, config: true } });
    for (const ch of channels) {
        if (ch.config && isEncrypted(ch.config) && startsWithLegacy(ch.config)) {
            rows.push({ table: 'NotificationChannel', column: 'config', id: ch.id, value: ch.config });
        }
    }

    // 2. User.twoFactorSecret
    const users = await prisma.user.findMany({
        where: { twoFactorSecret: { not: null } },
        select: { id: true, twoFactorSecret: true },
    });
    for (const u of users) {
        if (u.twoFactorSecret && isEncrypted(u.twoFactorSecret) && startsWithLegacy(u.twoFactorSecret)) {
            rows.push({ table: 'User', column: 'twoFactorSecret', id: u.id, value: u.twoFactorSecret });
        }
    }

    // 3. SystemSetting.value — only keys known to hold encrypted secrets.
    //    Mirrors SECRET_SETTING_KEYS in src/lib/services/system-setting-secrets.ts.
    const SECRET_KEYS = ['smtp.password', 'smtp.host', 'smtp.username'];
    const settings = await prisma.systemSetting.findMany({
        where: { key: { in: SECRET_KEYS } },
        select: { id: true, key: true, value: true },
    });
    for (const s of settings) {
        if (s.value && isEncrypted(s.value) && startsWithLegacy(s.value)) {
            rows.push({ table: `SystemSetting[${s.key}]`, column: 'value', id: s.id, value: s.value });
        }
    }

    // 4. MonitorHeader.value
    const headers = await prisma.monitorHeader.findMany({ select: { id: true, value: true } });
    for (const h of headers) {
        if (h.value && isEncrypted(h.value) && startsWithLegacy(h.value)) {
            rows.push({ table: 'MonitorHeader', column: 'value', id: h.id, value: h.value });
        }
    }

    return rows;
}

async function rewriteRow(row: Row): Promise<void> {
    // No production caller passed an AAD as of the prefix-change PR,
    // so legacy rows are all v1 (no AAD). decrypt() handles both v1
    // and v2 — if we discover a v2 row in the wild, error explicitly
    // so the operator can re-run with the per-table AAD logic added.
    if (row.value.startsWith('v2:')) {
        throw new Error(
            `Row ${row.table}#${row.id} is v2 (AAD-bound). This script does not yet ` +
            `know the per-table AAD pattern. Re-run after extending the script.`,
        );
    }
    const plaintext = decrypt(row.value);
    const rewritten = encrypt(plaintext);

    if (!APPLY) {
        console.log(`  [dry-run] ${row.table}#${row.id}.${row.column}: v1 -> enc_v1`);
        return;
    }

    // Write via the model-specific update. Use raw SQL would be terser
    // but loses the Prisma type-safety on the table+column.
    switch (row.table) {
        case 'NotificationChannel':
            await prisma.notificationChannel.update({ where: { id: row.id as number }, data: { config: rewritten } });
            break;
        case 'User':
            await prisma.user.update({ where: { id: row.id as number }, data: { twoFactorSecret: rewritten } });
            break;
        case 'MonitorHeader':
            await prisma.monitorHeader.update({ where: { id: row.id as number }, data: { value: rewritten } });
            break;
        default:
            if (row.table.startsWith('SystemSetting')) {
                await prisma.systemSetting.update({ where: { id: row.id as number }, data: { value: rewritten } });
                break;
            }
            throw new Error(`Unhandled table: ${row.table}`);
    }
    console.log(`  [applied] ${row.table}#${row.id}.${row.column}: v1 -> enc_v1`);
}

async function main(): Promise<void> {
    console.log(`Re-encrypt legacy-prefix envelopes ${APPLY ? '(APPLY MODE)' : '(dry-run; pass --apply to write)'}`);
    const rows = await collectLegacyRows();
    if (rows.length === 0) {
        console.log('No rows with legacy v1:/v2: prefix found. Nothing to do.');
        return;
    }
    console.log(`Found ${rows.length} row(s) to rewrite:`);
    for (const row of rows) {
        await rewriteRow(row);
    }
    console.log(APPLY ? 'Done.' : 'Dry-run complete. Re-run with --apply to write.');
}

main()
    .catch((err) => {
        console.error('Fatal:', err);
        process.exit(1);
    })
    .finally(() => prisma.$disconnect());
