#!/usr/bin/env node
/**
 * Apply Postgres migrations from `db/migrations/*.sql` in lexical order.
 * Wraps each new file in a transaction; tracks applied files in
 * `_migrations` so re-runs are safe and idempotent.
 *
 * The 0001_init.sql (and 0002) are now fully re-runnable (IF NOT EXISTS +
 * trigger guards) + this script has repair logic for DBs that were
 * initialized before tracking was introduced.
 *
 * Usage:
 *   npm run migrate
 *   npm run migrate:one     # same as above (applies all pending)
 */
require("dotenv").config({ path: ".env.local" });

const fs = require("node:fs");
const path = require("node:path");
const { Client } = require("pg");

const MIGRATIONS_DIR = path.join(__dirname, "..", "db", "migrations");

async function main() {
  const url = process.env.DATABASE_ADMIN_URL ?? process.env.DATABASE_URL;
  if (!url) {
    console.error("❌ DATABASE_URL (or DATABASE_ADMIN_URL) is not set in .env.local");
    process.exit(1);
  }

  const client = new Client({ connectionString: url });
  await client.connect();

  try {
    await client.query(`
      CREATE TABLE IF NOT EXISTS _migrations (
        filename TEXT PRIMARY KEY,
        applied_at TIMESTAMPTZ NOT NULL DEFAULT now()
      )
    `);

    const files = fs
      .readdirSync(MIGRATIONS_DIR)
      .filter((f) => f.endsWith(".sql"))
      .sort();

    if (files.length === 0) {
      console.log("No migration files found in db/migrations/");
      return;
    }

    const { rows: applied } = await client.query(
      `SELECT filename FROM _migrations`,
    );
    const appliedSet = new Set(applied.map((r) => r.filename));

    // Repair for historical DBs (applied before _migrations tracking existed,
    // or via direct psql / earlier tooling). If the core objects are present
    // we record the filename so future deploys (and server-side recovery runs)
    // treat 0001/0002 as done without re-executing the large init script.
    async function ensureTracked(filename, existenceCheckSql) {
      if (appliedSet.has(filename)) return;
      try {
        const { rows } = await client.query(existenceCheckSql);
        if (rows.length > 0) {
          await client.query(
            `INSERT INTO _migrations (filename) VALUES ($1) ON CONFLICT (filename) DO NOTHING`,
            [filename]
          );
          console.log(`✓ ${filename} (objects present in DB; repaired tracking)`);
          appliedSet.add(filename);
        }
      } catch (e) {
        // Non-fatal: the check may fail on a brand-new DB or with limited perms.
        // We'll let the normal apply path handle it.
      }
    }

    await ensureTracked(
      "0001_init.sql",
      "SELECT 1 FROM information_schema.tables WHERE table_name = 'admin_users' LIMIT 1"
    );
    await ensureTracked(
      "0002_admin_password.sql",
      "SELECT 1 FROM information_schema.columns WHERE table_name = 'users' AND column_name = 'password_hash' LIMIT 1"
    );

    let appliedNow = 0;
    for (const file of files) {
      if (appliedSet.has(file)) {
        console.log(`✓ ${file} (already applied)`);
        continue;
      }
      const sql = fs.readFileSync(path.join(MIGRATIONS_DIR, file), "utf8");
      console.log(`→ Applying ${file}...`);
      try {
        await client.query("BEGIN");
        await client.query(sql);
        await client.query(`INSERT INTO _migrations (filename) VALUES ($1)`, [
          file,
        ]);
        await client.query("COMMIT");
        appliedNow += 1;
        console.log(`✓ ${file}`);
      } catch (err) {
        await client.query("ROLLBACK");
        console.error(`✗ ${file} failed:`, err.message);
        throw err;
      }
    }

    console.log(
      `\n✅ Done. ${appliedNow} new migration(s) applied. ${
        files.length - appliedNow
      } already current.`,
    );
  } finally {
    await client.end();
  }
}

main().catch((err) => {
  console.error(err);
  process.exit(1);
});
