/**
 * One-time migration: encrypt any plaintext credential rows in
 * `system_settings` that should now be AES-256-GCM encrypted per the
 * settings registry. Safe to re-run (idempotent — only touches plaintext
 * rows, skips rows that are already encrypted envelopes).
 *
 * Currently handles:
 *   - `documenso_api_key_override`            → in-place encrypt
 *   - `storage_s3_access_key` (legacy)        → encrypt + move to
 *                                                `storage_s3_access_key_encrypted`
 *   - `documenso_webhook_secret` (if string)  → in-place encrypt
 *
 * Run: `pnpm tsx scripts/encrypt-plaintext-credentials.ts`
 */
import { and, eq, isNull } from 'drizzle-orm';

import { db } from '@/lib/db';
import { systemSettings } from '@/lib/db/schema';
import { encrypt } from '@/lib/utils/encryption';

const KEYS_TO_ENCRYPT_IN_PLACE = ['documenso_api_key_override', 'documenso_webhook_secret'];

function isEncryptedEnvelope(value: unknown): boolean {
  return (
    typeof value === 'object' &&
    value !== null &&
    typeof (value as { iv?: unknown }).iv === 'string' &&
    typeof (value as { tag?: unknown }).tag === 'string' &&
    typeof (value as { data?: unknown }).data === 'string'
  );
}

async function encryptInPlace(key: string): Promise<{ touched: number; skipped: number }> {
  const rows = await db
    .select({ key: systemSettings.key, portId: systemSettings.portId, value: systemSettings.value })
    .from(systemSettings)
    .where(eq(systemSettings.key, key));

  let touched = 0;
  let skipped = 0;
  for (const row of rows) {
    if (isEncryptedEnvelope(row.value)) {
      skipped++;
      continue;
    }
    if (typeof row.value !== 'string' || row.value === '') {
      skipped++;
      continue;
    }
    const envelope = JSON.parse(encrypt(row.value)) as {
      iv: string;
      tag: string;
      data: string;
    };
    if (row.portId) {
      await db
        .update(systemSettings)
        .set({ value: envelope, updatedAt: new Date() })
        .where(and(eq(systemSettings.key, key), eq(systemSettings.portId, row.portId)));
    } else {
      await db
        .update(systemSettings)
        .set({ value: envelope, updatedAt: new Date() })
        .where(and(eq(systemSettings.key, key), isNull(systemSettings.portId)));
    }
    touched++;
  }
  return { touched, skipped };
}

async function moveS3AccessKeyToEncrypted(): Promise<{
  moved: number;
  alreadyMigrated: number;
}> {
  // Move global rows only — s3 storage settings are global by design.
  const legacyRows = await db
    .select({ value: systemSettings.value })
    .from(systemSettings)
    .where(and(eq(systemSettings.key, 'storage_s3_access_key'), isNull(systemSettings.portId)));

  if (legacyRows.length === 0) {
    return { moved: 0, alreadyMigrated: 0 };
  }

  // Check if the encrypted form already exists.
  const existingEncrypted = await db
    .select({ key: systemSettings.key })
    .from(systemSettings)
    .where(
      and(eq(systemSettings.key, 'storage_s3_access_key_encrypted'), isNull(systemSettings.portId)),
    );

  if (existingEncrypted.length > 0) {
    // Encrypted form wins; leave the legacy row in place so reads still
    // tolerate it (the storage layer reads both and prefers encrypted).
    return { moved: 0, alreadyMigrated: legacyRows.length };
  }

  const plaintext = legacyRows[0]!.value;
  if (typeof plaintext !== 'string' || plaintext === '') {
    return { moved: 0, alreadyMigrated: 0 };
  }
  const envelope = JSON.parse(encrypt(plaintext)) as { iv: string; tag: string; data: string };
  await db.insert(systemSettings).values({
    key: 'storage_s3_access_key_encrypted',
    portId: null,
    value: envelope,
  });
  // Drop the legacy plaintext row so it doesn't show up in admin
  // settings dumps anymore. The storage layer's backward-compat path
  // continues to handle older rows on other deployments.
  await db
    .delete(systemSettings)
    .where(and(eq(systemSettings.key, 'storage_s3_access_key'), isNull(systemSettings.portId)));
  return { moved: 1, alreadyMigrated: 0 };
}

async function main(): Promise<void> {
  console.log('Encrypting plaintext credentials...');

  for (const key of KEYS_TO_ENCRYPT_IN_PLACE) {
    const { touched, skipped } = await encryptInPlace(key);
    console.log(`  ${key}: ${touched} encrypted, ${skipped} skipped`);
  }

  const s3 = await moveS3AccessKeyToEncrypted();
  console.log(
    `  storage_s3_access_key → _encrypted: ${s3.moved} moved, ${s3.alreadyMigrated} already migrated`,
  );

  console.log('Done.');
  process.exit(0);
}

main().catch((err: unknown) => {
  console.error('Migration failed:', err);
  process.exit(1);
});
