/**
 * Database Migrations
 *
 * Schema versioning and migration support.
 */

import { SqliteDatabase } from './sqlite-adapter';

/**
 * Current schema version
 */
export const CURRENT_SCHEMA_VERSION = 8;

/**
 * Migration definition
 */
interface Migration {
  version: number;
  description: string;
  up: (db: SqliteDatabase) => void;
}

/**
 * All migrations in order
 *
 * Note: Version 1 is the initial schema, handled by schema.sql
 * Future migrations go here.
 */
const migrations: Migration[] = [
  {
    version: 2,
    description: 'Add project metadata, provenance tracking, and unresolved ref context',
    up: (db) => {
      db.exec(`
        CREATE TABLE IF NOT EXISTS project_metadata (
          key TEXT PRIMARY KEY,
          value TEXT NOT NULL,
          updated_at INTEGER NOT NULL
        );
        ALTER TABLE unresolved_refs ADD COLUMN file_path TEXT NOT NULL DEFAULT '';
        ALTER TABLE unresolved_refs ADD COLUMN language TEXT NOT NULL DEFAULT 'unknown';
        ALTER TABLE edges ADD COLUMN provenance TEXT DEFAULT NULL;
        CREATE INDEX IF NOT EXISTS idx_unresolved_file_path ON unresolved_refs(file_path);
        CREATE INDEX IF NOT EXISTS idx_edges_provenance ON edges(provenance);
      `);
    },
  },
  {
    version: 3,
    description: 'Add lower(name) expression index for memory-efficient case-insensitive lookups',
    up: (db) => {
      db.exec(`
        CREATE INDEX IF NOT EXISTS idx_nodes_lower_name ON nodes(lower(name));
      `);
    },
  },
  {
    version: 4,
    description:
      'Drop redundant idx_edges_source / idx_edges_target (covered by source_kind / target_kind composites)',
    up: (db) => {
      db.exec(`
        DROP INDEX IF EXISTS idx_edges_source;
        DROP INDEX IF EXISTS idx_edges_target;
      `);
    },
  },
  {
    version: 5,
    description:
      'Add nodes.return_type — normalized return/result type for receiver-type inference (C++ singletons/factories, #645)',
    up: (db) => {
      db.exec(`
        ALTER TABLE nodes ADD COLUMN return_type TEXT;
      `);
    },
  },
  {
    version: 6,
    description:
      'Dedup duplicate edge rows and add a UNIQUE identity index so INSERT OR IGNORE actually dedups (#1034)',
    up: (db) => {
      // `insertEdge` has always used `INSERT OR IGNORE`, but the edges table had
      // no UNIQUE constraint, so nothing conflicted and byte-identical rows
      // accumulated whenever two passes emitted the same edge. Collapse each
      // identity group to its lowest id, then add the constraint that makes
      // `OR IGNORE` keep its promise. IFNULL folds nullable line/col so
      // coordinate-less edges dedup too (SQLite treats each NULL as distinct) —
      // and it MUST match the GROUP BY exactly, or the index creation would
      // fail on a pair the DELETE left behind. Idempotent: the index is
      // `IF NOT EXISTS` and the DELETE is a no-op once the table is unique.
      db.exec(`
        DELETE FROM edges
        WHERE id NOT IN (
          SELECT MIN(id) FROM edges
          GROUP BY source, target, kind, IFNULL(line, -1), IFNULL(col, -1)
        );
        CREATE UNIQUE INDEX IF NOT EXISTS idx_edges_identity
          ON edges(source, target, kind, IFNULL(line, -1), IFNULL(col, -1));
      `);
    },
  },
  {
    version: 7,
    description:
      'Add name_segment_vocab — prose-word → symbol-name lookup for the prompt hook’s graph-derived gate',
    up: (db) => {
      // DDL only — instant on any size database (the row-churn hazards of #1067
      // don't apply). The table starts EMPTY on migrated databases; `sync`
      // detects that over a populated graph and backfills batched+yielding
      // (CodeGraph.rebuildNameSegmentVocab), and any full index rebuilds it
      // from scratch. Keep the definition in lockstep with schema.sql.
      db.exec(`
        CREATE TABLE IF NOT EXISTS name_segment_vocab (
          segment TEXT NOT NULL,
          name TEXT NOT NULL,
          PRIMARY KEY (segment, name)
        ) WITHOUT ROWID;
      `);
    },
  },
  {
    version: 8,
    description:
      'Track attempted-but-unresolvable refs as status=failed so sync can retry them when a changed file adds a matching symbol (#1240)',
    up: (db) => {
      // DDL only — instant on any size database. No backfill needed: rows are
      // only ever queried by name_tail once they carry status='failed', and
      // both fields are written together by markReferencesFailed. Legacy rows
      // (all 'pending' after this migration) are orphans from interrupted runs
      // that the #1187 sweep grinds down on the next sync, marking survivors
      // failed with their tails as it goes. The tail index is partial: on a
      // healthy index the pending set is empty and the failed set is the only
      // population worth indexing. Keep the definitions in lockstep with
      // schema.sql. ALTER TABLE has no IF NOT EXISTS, so guard each column for
      // idempotency — a database created from current schema.sql already has
      // both (matters when migrations are re-run from an older recorded
      // version, as the v6 regression test does).
      const cols = db.prepare('PRAGMA table_info(unresolved_refs)').all() as Array<{ name: string }>;
      const hasColumn = (name: string) => cols.some((c) => c.name === name);
      if (!hasColumn('status')) {
        db.exec("ALTER TABLE unresolved_refs ADD COLUMN status TEXT NOT NULL DEFAULT 'pending'");
      }
      if (!hasColumn('name_tail')) {
        db.exec("ALTER TABLE unresolved_refs ADD COLUMN name_tail TEXT NOT NULL DEFAULT ''");
      }
      db.exec(`
        CREATE INDEX IF NOT EXISTS idx_unresolved_status ON unresolved_refs(status);
        CREATE INDEX IF NOT EXISTS idx_unresolved_failed_tail ON unresolved_refs(name_tail) WHERE status = 'failed';
      `);
    },
  },
];

/**
 * Get the current schema version from the database
 */
export function getCurrentVersion(db: SqliteDatabase): number {
  try {
    const row = db
      .prepare('SELECT MAX(version) as version FROM schema_versions')
      .get() as { version: number | null } | undefined;
    return row?.version ?? 0;
  } catch {
    // Table doesn't exist yet
    return 0;
  }
}

/**
 * Record a migration as applied
 */
function recordMigration(db: SqliteDatabase, version: number, description: string): void {
  db.prepare(
    'INSERT INTO schema_versions (version, applied_at, description) VALUES (?, ?, ?)'
  ).run(version, Date.now(), description);
}

/**
 * Run all pending migrations
 */
export function runMigrations(db: SqliteDatabase, fromVersion: number): void {
  const pending = migrations.filter((m) => m.version > fromVersion);

  if (pending.length === 0) {
    return;
  }

  // Sort by version
  pending.sort((a, b) => a.version - b.version);

  // Run each migration in a transaction
  for (const migration of pending) {
    db.transaction(() => {
      migration.up(db);
      recordMigration(db, migration.version, migration.description);
    })();
  }
}

/**
 * Check if the database needs migration
 */
export function needsMigration(db: SqliteDatabase): boolean {
  const current = getCurrentVersion(db);
  return current < CURRENT_SCHEMA_VERSION;
}

/**
 * Get list of pending migrations
 */
export function getPendingMigrations(db: SqliteDatabase): Migration[] {
  const current = getCurrentVersion(db);
  return migrations
    .filter((m) => m.version > current)
    .sort((a, b) => a.version - b.version);
}

/**
 * Get migration history from database
 */
export function getMigrationHistory(
  db: SqliteDatabase
): Array<{ version: number; appliedAt: number; description: string | null }> {
  const rows = db
    .prepare('SELECT version, applied_at, description FROM schema_versions ORDER BY version')
    .all() as Array<{ version: number; applied_at: number; description: string | null }>;

  return rows.map((row) => ({
    version: row.version,
    appliedAt: row.applied_at,
    description: row.description,
  }));
}
