// engine/db/migrate.js — minimal versioned migration runner.
//
// Convention:
//   migrations/<NNN>-<slug>.js exports { version, description, up(db, ctx) }
//   - version is an integer matching the file's leading NNN
//   - description is shown in the [db-migrate] log line
//   - up(db, ctx) runs any DDL + data backfill; throwing rolls back the
//     enclosing transaction (the rest of `up` never runs, schema_version
//     is NOT bumped, the next startup retries).
//
// All migrations are wrapped in a single SQLite transaction. Either every
// statement in `up` lands AND the schema_version row is written, or
// nothing does. No partial-migration state can stick on disk.

const path = require('path');
const fs = require('fs');

function _loadMigrations() {
  const dir = path.join(__dirname, 'migrations');
  if (!fs.existsSync(dir)) return [];
  return fs.readdirSync(dir)
    .filter(f => /^\d{3,}-[\w-]+\.js$/.test(f))
    .sort()  // lexical sort works because of the fixed-width NNN prefix
    .map(f => require(path.join(dir, f)));
}

function runMigrations(db) {
  // schema_version is itself created via raw exec (chicken-and-egg with
  // the migrations themselves).
  db.exec(`
    CREATE TABLE IF NOT EXISTS schema_version (
      version INTEGER PRIMARY KEY,
      description TEXT,
      applied_at INTEGER NOT NULL
    )
  `);

  const migrations = _loadMigrations();

  for (const m of migrations) {
    if (typeof m.version !== 'number' || typeof m.up !== 'function') {
      throw new Error(`engine/db/migrate: migration missing { version, up }: ${JSON.stringify(Object.keys(m))}`);
    }
    // Concurrent-startup safety: take the SQLite write lock BEFORE re-reading
    // schema_version. With plain `BEGIN` (deferred), two processes booting at
    // the same time can both observe schema_version=N, both decide to apply
    // N+1, and one then fails with "table already exists" while the other
    // succeeds — leaving the loser stuck on `_dbInitError` for the lifetime
    // of its process. `BEGIN IMMEDIATE` serializes the version check itself
    // so the second arrival simply sees the updated schema_version and skips.
    db.exec('BEGIN IMMEDIATE');
    try {
      const currentRow = db.prepare('SELECT COALESCE(MAX(version), 0) AS v FROM schema_version').get();
      const current = Number(currentRow.v) || 0;
      if (m.version <= current) { db.exec('COMMIT'); continue; }
      // eslint-disable-next-line no-console
      console.log(`[db-migrate] Applying v${m.version}: ${m.description || '(no description)'}`);
      m.up(db, { fs, path });
      db.prepare(
        'INSERT INTO schema_version (version, description, applied_at) VALUES (?, ?, ?)'
      ).run(m.version, m.description || '', Date.now());
      db.exec('COMMIT');
    } catch (e) {
      db.exec('ROLLBACK');
      // eslint-disable-next-line no-console
      console.error(`[db-migrate] v${m.version} FAILED, rolled back: ${e.message}`);
      throw e;
    }
  }
}

module.exports = { runMigrations };
