// Migration runner — applies plugin-declared migrations once each, tracked in
// public.schema_migrations. The plugin manager only *surfaces* migrations()
// (file URLs); this is what actually runs them, at boot.
//
// Design for a lean / DB-optional core:
//  * No declared migrations (e.g. kgva: no plugins) → returns immediately, never
//    opens a DB connection and never imports `pg`.
//  * `exec` is injectable — an async (text, params?) => rows function — so the
//    logic unit-tests without a real database. Omit it and the runner connects
//    via `pg` to `databaseUrl` (lazy import; clear error if `pg` is missing).
//  * Each migration runs once (id = "<plugin>/<file>") inside its own
//    transaction; the file SQL is itself idempotent (see 001_dialog.sql), so a
//    pre-existing DB (openbureau) is baselined cleanly on first run.
import { readFile } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';

export async function runMigrations(manager, { databaseUrl, exec, log = console } = {}) {
  const migs = manager.migrations();
  if (!migs.length) return { applied: [], skipped: [], pending: false };

  let close = async () => {};
  if (!exec) {
    if (!databaseUrl) {
      log.warn?.(`migrate: ${migs.length} Migration(en) deklariert, aber DATABASE_URL fehlt — übersprungen.`);
      return { applied: [], skipped: migs.map((m) => `${m.plugin}/${m.file}`), pending: true };
    }
    ({ exec, close } = await pgExecutor(databaseUrl));
  }

  try {
    await exec(`CREATE TABLE IF NOT EXISTS public.schema_migrations (
      id text PRIMARY KEY,
      plugin text NOT NULL,
      applied_at timestamptz NOT NULL DEFAULT now()
    )`);

    const applied = [], skipped = [];
    for (const m of migs) {
      const id = `${m.plugin}/${m.file}`;
      const done = await exec('SELECT 1 FROM public.schema_migrations WHERE id = $1', [id]);
      if (done?.length) { skipped.push(id); continue; }
      try {
        const sql = await readFile(fileURLToPath(m.url), 'utf8');
        await exec('BEGIN');
        try {
          await exec(sql);
          await exec('INSERT INTO public.schema_migrations (id, plugin) VALUES ($1, $2)', [id, m.plugin]);
          await exec('COMMIT');
        } catch (e) {
          await exec('ROLLBACK');
          throw e;
        }
      } catch (e) {
        throw new Error(`Migration ${id} fehlgeschlagen: ${e.message}`);
      }
      applied.push(id);
      log.log?.(`migrate: angewandt ${id}`);
    }
    return { applied, skipped, pending: false };
  } finally {
    await close();
  }
}

// Default executor: one pg client over databaseUrl. `pg` is imported lazily so a
// DB-less instance never pulls it in at runtime.
async function pgExecutor(databaseUrl) {
  let pg;
  try { pg = (await import('pg')).default; }
  catch { throw new Error("migrate: Paket 'pg' fehlt (npm i pg) — für Plugin-Migrationen nötig."); }
  const client = new pg.Client({ connectionString: databaseUrl });
  await client.connect();
  const exec = async (text, params) => (await client.query(text, params)).rows;
  return { exec, close: () => client.end() };
}
