/**
 * Backfill embeddings for all existing messages.
 *
 * Streams messages in batches that lack embeddings, calls model-boss on
 * apricot, and upserts vectors into macsync.message_embeddings.
 *
 * Run: bun run src/commands/backfill-embeddings.ts
 *
 * Required env:
 *   QUINN_MACSYNC_DB_URL  — postgres connection string
 *   MODEL_BOSS_URL       — model-boss coordinator URL (default: http://10.0.0.13:8210)
 */

import { Pool } from 'pg';
import { runMigrations } from '@/shared/db';
import { logger } from '@/shared/logger';
import { messageMigrations } from '@/entities/message';
import { messageEmbeddingMigrations, bulkUpsertEmbeddings, getEmbeddingCoverage } from '@/entities/message-embedding';
import { embedTexts, EMBEDDING_MODEL_VERSION } from '@/shared/model-boss';

const BATCH_SIZE = 64;

function requireEnv(name: string): string {
  const val = process.env[name];
  if (!val) throw new Error(`Required environment variable ${name} is not set`);
  return val;
}

interface MessageRow {
  id: string;
  body: string;
}

async function fetchUnembeddedBatch(pool: Pool, cursor: string | null, limit: number): Promise<MessageRow[]> {
  const rows = await pool.query<MessageRow>(
    `SELECT m.id, m.body
     FROM macsync.messages m
     LEFT JOIN macsync.message_embeddings e ON e.message_id = m.id
     WHERE e.message_id IS NULL
       AND m.body != ''
       AND ($1::uuid IS NULL OR m.id > $1)
     ORDER BY m.id
     LIMIT $2`,
    [cursor, limit],
  );
  return rows.rows;
}

async function main(): Promise<void> {
  const dbUrl = requireEnv('QUINN_MACSYNC_DB_URL');
  const pool = new Pool({ connectionString: dbUrl });

  try {
    logger.info('backfill: running migrations');
    await runMigrations(pool, [
      ...messageMigrations,
      ...messageEmbeddingMigrations,
    ]);

    const initial = await getEmbeddingCoverage(pool);
    logger.info('backfill: starting', { covered: initial.covered, total: initial.total });

    let cursor: string | null = null;
    let processed = 0;
    let errors = 0;

    while (true) {
      const batch = await fetchUnembeddedBatch(pool, cursor, BATCH_SIZE);
      if (batch.length === 0) break;

      const texts = batch.map((r) => r.body);

      let embeddings: number[][];
      try {
        embeddings = await embedTexts(texts);
      } catch (err) {
        logger.error('backfill: embed batch failed', {
          count: batch.length,
          err: err instanceof Error ? err.message : String(err),
        });
        errors += batch.length;
        cursor = batch[batch.length - 1]?.id ?? null;
        continue;
      }

      const drafts = batch.map((row, i) => ({
        messageId: row.id,
        embedding: embeddings[i]!,
        modelVersion: EMBEDDING_MODEL_VERSION,
      }));

      try {
        await bulkUpsertEmbeddings(pool, drafts);
      } catch (err) {
        logger.error('backfill: upsert batch failed', {
          count: batch.length,
          err: err instanceof Error ? err.message : String(err),
        });
        errors += batch.length;
      }

      processed += batch.length;
      cursor = batch[batch.length - 1]?.id ?? null;

      logger.info('backfill: progress', {
        processed,
        errors,
        total: initial.total,
        pct: initial.total > 0 ? Math.round(((initial.covered + processed) / initial.total) * 100) : 0,
      });
    }

    const final = await getEmbeddingCoverage(pool);
    const coveragePct = final.total > 0 ? ((final.covered / final.total) * 100).toFixed(1) : '0.0';

    logger.info('backfill: complete', {
      covered: final.covered,
      total: final.total,
      coveragePct,
      errors,
    });

    if (errors > 0) {
      logger.warn('backfill: some messages could not be embedded — re-run to retry', { errors });
    }
  } finally {
    await pool.end();
  }
}

main().catch((err) => {
  logger.error('backfill: fatal error', { err: err instanceof Error ? err.message : String(err) });
  process.exit(1);
});
