/**
 * Migration runner.
 *
 *   Usage:
 *     npm run migrate:up             -> apply pending migrations
 *     npm run migrate:status         -> list applied vs pending
 *     npm run migrate:baseline       -> mark every existing migration as already applied
 *
 * The runner is intentionally minimal: it discovers `.ts` files inside
 * `scripts/migrations/`, ignores anything starting with `_` (template files,
 * shared helpers), sorts them lexicographically (filename-based ordering ==
 * timestamp ordering when authors follow the convention), and applies each
 * file whose `name` is not yet recorded in the `migrations` table.
 *
 * Each migration file must `export default` an object exposing
 *   async up({ sequelize, queryInterface }): Promise<void>
 * See `scripts/migrations/_template.ts` for the contract.
 */

import 'dotenv/config';
import { fileURLToPath, pathToFileURL } from 'url';
import { createHash } from 'crypto';
import { promises as fs } from 'fs';
import path from 'path';
import db from '../src/database/models/index.js';

const MIGRATIONS_DIR = path.resolve(
    path.dirname(fileURLToPath(import.meta.url)),
    'migrations'
);

type Mode = 'up' | 'status' | 'baseline';

function parseMode(argv: string[]): Mode {
    if (argv.includes('--status')) return 'status';
    if (argv.includes('--baseline')) return 'baseline';
    return 'up';
}

async function discoverMigrationFiles(): Promise<string[]> {
    const entries = await fs.readdir(MIGRATIONS_DIR);
    return entries
        .filter((name) => name.endsWith('.ts') && !name.startsWith('_'))
        .sort();
}

async function ensureMigrationsTable(): Promise<void> {
    // The Migration model has the canonical schema. We create the table only
    // if it does not yet exist so this script remains safe to run on any
    // environment (fresh sync OR already-migrated DB).
    await db.Migration.sync();
}

async function loadMigration(file: string): Promise<{ up: (ctx: any) => Promise<void> }> {
    const fullPath = path.join(MIGRATIONS_DIR, file);
    const mod: any = await import(pathToFileURL(fullPath).href);
    const migration = mod.default ?? mod;
    if (!migration || typeof migration.up !== 'function') {
        throw new Error(`Migration ${file} does not export a default { up } function.`);
    }
    return migration;
}

async function fileChecksum(file: string): Promise<string> {
    const buf = await fs.readFile(path.join(MIGRATIONS_DIR, file));
    return createHash('sha256').update(buf).digest('hex');
}

async function listApplied(): Promise<Set<string>> {
    const rows = await db.Migration.findAll({ attributes: ['name'] });
    return new Set(rows.map((r: any) => r.name as string));
}

function stripExt(file: string): string {
    return file.replace(/\.ts$/, '');
}

async function runUp(): Promise<void> {
    const files = await discoverMigrationFiles();
    const applied = await listApplied();

    const pending = files.filter((f) => !applied.has(stripExt(f)));
    if (pending.length === 0) {
        console.log('All migrations are already applied. Nothing to do.');
        return;
    }

    console.log(`Applying ${pending.length} migration(s)...\n`);
    for (const file of pending) {
        const name = stripExt(file);
        process.stdout.write(`  → ${name} ... `);
        try {
            const migration = await loadMigration(file);
            await db.sequelize.transaction(async (transaction: any) => {
                await migration.up({
                    sequelize: db.sequelize,
                    queryInterface: db.sequelize.getQueryInterface(),
                    transaction
                });
            });
            const checksum = await fileChecksum(file);
            await db.Migration.create({ name, checksum });
            console.log('OK');
        } catch (err: any) {
            console.log('FAILED');
            console.error(`\n${file} failed:`, err.message || err);
            throw err;
        }
    }
    console.log('\nMigrations complete.');
}

async function runStatus(): Promise<void> {
    const files = await discoverMigrationFiles();
    const applied = await listApplied();

    if (files.length === 0) {
        console.log('No migration files in scripts/migrations/.');
        return;
    }

    console.log('Migration status:');
    for (const file of files) {
        const name = stripExt(file);
        const mark = applied.has(name) ? '✓' : ' ';
        console.log(`  [${mark}] ${name}`);
    }

    const pendingCount = files.filter((f) => !applied.has(stripExt(f))).length;
    console.log(`\nApplied: ${files.length - pendingCount}   Pending: ${pendingCount}`);
}

async function runBaseline(): Promise<void> {
    const files = await discoverMigrationFiles();
    const applied = await listApplied();

    const toStamp = files.filter((f) => !applied.has(stripExt(f)));
    if (toStamp.length === 0) {
        console.log('Baseline: every migration is already recorded. Nothing to do.');
        return;
    }

    console.log(`Stamping ${toStamp.length} migration(s) as already-applied (no SQL executed):\n`);
    for (const file of toStamp) {
        const name = stripExt(file);
        const checksum = await fileChecksum(file);
        await db.Migration.create({ name, checksum });
        console.log(`  + ${name}`);
    }
    console.log('\nBaseline complete.');
}

async function main(): Promise<void> {
    const mode = parseMode(process.argv.slice(2));

    try {
        await db.sequelize.authenticate();
        await ensureMigrationsTable();

        if (mode === 'status') {
            await runStatus();
        } else if (mode === 'baseline') {
            await runBaseline();
        } else {
            await runUp();
        }

        await db.sequelize.close();
        process.exit(0);
    } catch (err: any) {
        console.error('\nMigration runner aborted:', err?.message || err);
        try {
            await db.sequelize.close();
        } catch {
            /* ignore */
        }
        process.exit(1);
    }
}

main();
