/**
 * backfill-body-decoded.ts
 *
 * One-shot backfill: scans macsync.messages for rows that have an
 * attributed_body blob but no body_decoded text yet, runs each through the
 * shared decoder, and writes the recovered text back to the row.
 *
 * Safe to re-run — it only touches rows where body_decoded IS NULL.
 *
 * Usage:
 *   bun run src/scripts/backfill-body-decoded.ts                  full pass
 *   bun run src/scripts/backfill-body-decoded.ts --dry-run        decode + report, no UPDATE
 *   bun run src/scripts/backfill-body-decoded.ts --limit=1000     cap rows for testing
 *   bun run src/scripts/backfill-body-decoded.ts --batch-size=200 tune throughput
 *
 * Env (uses the same wiring as the server):
 *   QUINN_MACSYNC_DB_URL   Postgres DSN (required)
 *
 * Exit code: 0 on clean completion; 1 if any UPDATE failed.
 */
import { decodeAttributedBody } from '@/shared/typedstream/decode';
import { openDb, pgAll, pgRun } from '@/shared/db';
import { logger } from '@/shared/logger';

interface BackfillRow {
  id: string;
  attributed_body: Buffer;
}

interface BackfillArgs {
  readonly dryRun: boolean;
  readonly limit: number | null;
  readonly batchSize: number;
}

function parseArgs(argv: readonly string[]): BackfillArgs {
  let dryRun = false;
  let limit: number | null = null;
  let batchSize = 500;
  for (const arg of argv) {
    if (arg === '--dry-run') dryRun = true;
    else if (arg.startsWith('--limit=')) {
      const n = Number(arg.slice('--limit='.length));
      if (!Number.isFinite(n) || n <= 0) throw new Error(`invalid --limit: ${arg}`);
      limit = n;
    } else if (arg.startsWith('--batch-size=')) {
      const n = Number(arg.slice('--batch-size='.length));
      if (!Number.isFinite(n) || n <= 0) throw new Error(`invalid --batch-size: ${arg}`);
      batchSize = n;
    } else if (arg.startsWith('--')) {
      throw new Error(`unknown flag: ${arg}`);
    }
  }
  return { dryRun, limit, batchSize };
}

async function main(): Promise<void> {
  const args = parseArgs(process.argv.slice(2));
  const pool = await openDb('macsync');

  const totalRow = await pgAll<{ n: string }>(
    pool,
    `SELECT COUNT(*)::text AS n
       FROM macsync.messages
      WHERE attributed_body IS NOT NULL
        AND body_decoded IS NULL`,
  );
  const total = Number(totalRow[0]?.n ?? '0');

  const cap = args.limit !== null ? Math.min(total, args.limit) : total;
  logger.info('backfill-body-decoded: starting', {
    candidates: total,
    processing: cap,
    batchSize: args.batchSize,
    dryRun: args.dryRun,
  });
  if (cap === 0) {
    logger.info('backfill-body-decoded: nothing to do');
    return;
  }

  let processed = 0;
  let decodedCount = 0;
  let nullDecodes = 0;
  let updateErrors = 0;
  let lastId = '';

  while (processed < cap) {
    const fetchLimit = Math.min(args.batchSize, cap - processed);
    const params: unknown[] = lastId ? [lastId, fetchLimit] : [fetchLimit];
    const sql = lastId
      ? `SELECT id, attributed_body
           FROM macsync.messages
          WHERE attributed_body IS NOT NULL
            AND body_decoded IS NULL
            AND id > $1
          ORDER BY id ASC
          LIMIT $2`
      : `SELECT id, attributed_body
           FROM macsync.messages
          WHERE attributed_body IS NOT NULL
            AND body_decoded IS NULL
          ORDER BY id ASC
          LIMIT $1`;
    const rows = await pgAll<BackfillRow>(pool, sql, params);
    if (rows.length === 0) break;

    for (const row of rows) {
      const base64 = row.attributed_body.toString('base64');
      const { text, error } = decodeAttributedBody(base64);
      if (text === null) {
        nullDecodes += 1;
      } else {
        decodedCount += 1;
        if (!args.dryRun) {
          try {
            await pgRun(
              pool,
              `UPDATE macsync.messages
                  SET body_decoded = $2,
                      updated_at = now()
                WHERE id = $1
                  AND body_decoded IS NULL`,
              [row.id, text],
            );
          } catch (err) {
            updateErrors += 1;
            logger.warn('backfill-body-decoded: UPDATE failed', {
              id: row.id,
              error: (err as Error).message,
            });
          }
        }
      }
      if (error) {
        logger.warn('backfill-body-decoded: decode error', { id: row.id, error });
      }
      lastId = row.id;
    }

    processed += rows.length;
    if (processed % (args.batchSize * 4) === 0 || processed === cap) {
      logger.info('backfill-body-decoded: progress', {
        processed,
        total: cap,
        decoded: decodedCount,
        nullDecodes,
        updateErrors,
      });
    }
  }

  logger.info('backfill-body-decoded: done', {
    processed,
    decoded: decodedCount,
    nullDecodes,
    updateErrors,
    dryRun: args.dryRun,
  });
  if (updateErrors > 0) {
    process.exit(1);
  }
}

main().catch((err) => {
  logger.error('backfill-body-decoded: fatal', { stack: (err as Error).stack ?? String(err) });
  process.exit(1);
});
