/**
 * Migrations Doctor
 *
 * Diagnoses and (with --rebuild-journal) repairs drift between
 * `quickback/drizzle/<dir>/*.sql`, `_journal.json`, and `<idx>_snapshot.json`.
 *
 * The compiler refuses to emit new migrations when this state is inconsistent
 * (see apps/compiler/src/utils/validate-migration-state.ts) because drizzle-kit
 * silently skips emission against a stale diff base, producing a compile that
 * appears successful while shipping a Worker that 500s on the new tables.
 *
 * Usage:
 *   quickback migrations doctor                    # diagnose only
 *   quickback migrations doctor --rebuild-journal  # rebuild _journal.json from on-disk .sql filenames
 */
import { join } from 'path';
import fs from 'fs/promises';
import pc from 'picocolors';
const DRIZZLE_DIRS = ['auth', 'features', 'files', 'webhooks', 'audit'];
const DRIZZLE_KIT_DIRS = new Set(['auth', 'features']);
const SQL_FILENAME_RE = /^(\d+)_(.+)\.sql$/;
const SNAPSHOT_FILENAME_RE = /^(\d+)_snapshot\.json$/;
export async function migrations(args) {
    const sub = args[0];
    if (!sub || sub === 'help' || sub === '--help' || sub === '-h') {
        printHelp();
        return;
    }
    if (sub !== 'doctor') {
        console.error(pc.red(`Unknown subcommand: migrations ${sub}`));
        printHelp();
        process.exit(1);
    }
    const rest = args.slice(1);
    const rebuild = rest.includes('--rebuild-journal');
    const yes = rest.includes('--yes') || rest.includes('-y');
    const projectRoot = await findProjectRoot();
    if (!projectRoot) {
        console.error(pc.red('No quickback project found in this directory.'));
        console.log(pc.gray('Run from a directory containing quickback/quickback.config.ts'));
        process.exit(1);
    }
    const drizzleRoot = join(projectRoot, 'quickback', 'drizzle');
    if (!(await pathExists(drizzleRoot))) {
        console.log(pc.gray('No quickback/drizzle/ directory — nothing to diagnose.'));
        return;
    }
    console.log(pc.bold('\nQuickback Migrations Doctor\n'));
    console.log(pc.gray(`  Scanning ${drizzleRoot}\n`));
    const dirs = await collectDirStates(drizzleRoot);
    if (dirs.length === 0) {
        console.log(pc.gray('No drizzle migration dirs found.'));
        return;
    }
    let anyIssues = false;
    const repairable = [];
    for (const dir of dirs) {
        const issues = inspectDir(dir);
        printDirReport(dir, issues);
        if (issues.length > 0)
            anyIssues = true;
        // The journal-rebuild repair fixes "orphan .sql files" and "missing
        // journal" — it can't fix snapshot drift on drizzle-kit dirs.
        if (issues.some((i) => i.message.includes('no matching journal entry') || i.message.includes('journal missing'))) {
            repairable.push(dir);
        }
    }
    if (!anyIssues) {
        console.log(pc.green('✓ Migration state is consistent.\n'));
        return;
    }
    if (!rebuild) {
        console.log(pc.bold(pc.yellow('\nRepair options:\n')));
        if (repairable.length > 0) {
            console.log('  ' + pc.cyan('quickback migrations doctor --rebuild-journal'));
            console.log(pc.gray(`    Rebuilds _journal.json from on-disk .sql filenames in: ${repairable.map((d) => d.name).join(', ')}\n`));
        }
        // Snapshot-drift dirs (drizzle-kit dirs missing _snapshot.json files)
        const snapshotDriftDirs = dirs.filter((d) => DRIZZLE_KIT_DIRS.has(d.name) &&
            d.snapshotFiles.length < d.sqlFiles.length);
        if (snapshotDriftDirs.length > 0) {
            console.log('  ' + pc.cyan('npx drizzle-kit introspect --config drizzle.config.ts'));
            console.log(pc.gray(`    Regenerates the snapshot chain from your live DB schema for: ${snapshotDriftDirs.map((d) => d.name).join(', ')}`));
            console.log(pc.gray('    This is the only safe fix for snapshot drift on a database with applied migrations.\n'));
        }
        process.exit(1);
    }
    // --rebuild-journal path
    if (repairable.length === 0) {
        console.log(pc.yellow('Nothing the journal rebuild can fix. Snapshot drift requires `drizzle-kit introspect` — see above.\n'));
        process.exit(1);
    }
    if (!yes) {
        console.log(pc.yellow(`About to rewrite _journal.json in ${repairable.length} dir(s):`));
        for (const d of repairable) {
            console.log(pc.gray(`  ${d.metaPath.replace(projectRoot + '/', '')}/_journal.json`));
        }
        console.log(pc.gray('\nPass --yes to skip this prompt. Continuing in 3s — Ctrl+C to abort.'));
        await new Promise((r) => setTimeout(r, 3000));
    }
    for (const d of repairable) {
        await rebuildJournal(d);
        console.log(pc.green(`✓ Rebuilt ${d.metaPath.replace(projectRoot + '/', '')}/_journal.json (${d.sqlFiles.length} entries)`));
    }
    console.log();
    console.log(pc.bold('Next steps:'));
    console.log(pc.gray('  1. Review the rewritten journals (diff against your VCS to confirm).'));
    const snapshotDriftDirs = dirs.filter((d) => DRIZZLE_KIT_DIRS.has(d.name) &&
        d.snapshotFiles.length < d.sqlFiles.length);
    if (snapshotDriftDirs.length > 0) {
        console.log(pc.gray('  2. If snapshot drift remains, run `npx drizzle-kit introspect` against your live DB.'));
        console.log(pc.gray('  3. Run `quickback compile` — it should now emit new migrations.'));
    }
    else {
        console.log(pc.gray('  2. Run `quickback compile` — it should now emit new migrations.'));
    }
    console.log();
}
function printHelp() {
    console.log(`
${pc.bold('quickback migrations <subcommand>')}

${pc.bold('SUBCOMMANDS')}
  ${pc.cyan('doctor')}                    Diagnose drift in quickback/drizzle/
  ${pc.cyan('doctor --rebuild-journal')}  Rebuild _journal.json from on-disk .sql filenames

${pc.bold('FLAGS')}
  ${pc.cyan('--yes, -y')}                 Skip the confirmation pause when repairing
`);
}
async function findProjectRoot() {
    // Walk up looking for a directory containing quickback/quickback.config.{ts,js}
    let dir = process.cwd();
    for (let i = 0; i < 6; i++) {
        if ((await pathExists(join(dir, 'quickback', 'quickback.config.ts'))) ||
            (await pathExists(join(dir, 'quickback', 'quickback.config.js'))) ||
            (await pathExists(join(dir, 'quickback.config.ts')))) {
            return dir;
        }
        const parent = join(dir, '..');
        if (parent === dir)
            break;
        dir = parent;
    }
    return null;
}
async function pathExists(path) {
    try {
        await fs.access(path);
        return true;
    }
    catch {
        return false;
    }
}
async function collectDirStates(drizzleRoot) {
    const states = [];
    for (const name of DRIZZLE_DIRS) {
        const absolutePath = join(drizzleRoot, name);
        if (!(await pathExists(absolutePath)))
            continue;
        const metaPath = join(absolutePath, 'meta');
        const sqlFiles = [];
        const snapshotFiles = [];
        let hasJournal = false;
        let journalEntries = [];
        let journalVersion;
        const entries = await fs.readdir(absolutePath);
        for (const entry of entries) {
            const m = entry.match(SQL_FILENAME_RE);
            if (m) {
                const stat = await fs.stat(join(absolutePath, entry));
                sqlFiles.push({
                    filename: entry,
                    idx: parseInt(m[1], 10),
                    tag: entry.replace(/\.sql$/, ''),
                    mtimeMs: stat.mtimeMs,
                });
            }
        }
        sqlFiles.sort((a, b) => a.idx - b.idx);
        if (await pathExists(metaPath)) {
            const metaEntries = await fs.readdir(metaPath);
            for (const entry of metaEntries) {
                const m = entry.match(SNAPSHOT_FILENAME_RE);
                if (m) {
                    snapshotFiles.push({ filename: entry, idx: parseInt(m[1], 10) });
                }
            }
            snapshotFiles.sort((a, b) => a.idx - b.idx);
            const journalAbsolutePath = join(metaPath, '_journal.json');
            if (await pathExists(journalAbsolutePath)) {
                hasJournal = true;
                try {
                    const parsed = JSON.parse(await fs.readFile(journalAbsolutePath, 'utf-8'));
                    journalVersion = typeof parsed.version === 'string' ? parsed.version : undefined;
                    const raw = Array.isArray(parsed.entries) ? parsed.entries : [];
                    journalEntries = raw
                        .filter((e) => typeof e?.idx === 'number' && typeof e?.tag === 'string')
                        .map((e) => ({
                        idx: e.idx,
                        version: e.version,
                        when: e.when,
                        tag: e.tag,
                        breakpoints: e.breakpoints,
                    }))
                        .sort((a, b) => a.idx - b.idx);
                }
                catch {
                    // Parse failure handled by inspectDir
                }
            }
        }
        states.push({
            name,
            absolutePath,
            metaPath,
            hasJournal,
            journalEntries,
            journalVersion,
            sqlFiles,
            snapshotFiles,
        });
    }
    return states;
}
function inspectDir(dir) {
    const issues = [];
    if (!dir.hasJournal && dir.sqlFiles.length > 0) {
        issues.push({
            severity: 'error',
            message: `journal missing but ${dir.sqlFiles.length} .sql file(s) exist on disk`,
        });
        return issues; // every other check requires a journal
    }
    if (dir.sqlFiles.length === 0 && dir.journalEntries.length === 0) {
        return issues; // empty dir, nothing to check
    }
    const sqlTags = new Set(dir.sqlFiles.map((f) => f.tag));
    const journalTags = new Set(dir.journalEntries.map((e) => e.tag));
    const orphanJournal = dir.journalEntries.filter((e) => !sqlTags.has(e.tag));
    if (orphanJournal.length > 0) {
        issues.push({
            severity: 'error',
            message: `${orphanJournal.length} journal entry(ies) reference missing .sql: ${orphanJournal.map((e) => e.tag).slice(0, 5).join(', ')}${orphanJournal.length > 5 ? ', …' : ''}`,
        });
    }
    const orphanSql = dir.sqlFiles.filter((f) => !journalTags.has(f.tag));
    if (orphanSql.length > 0) {
        issues.push({
            severity: 'error',
            message: `${orphanSql.length} .sql file(s) have no matching journal entry: ${orphanSql.map((f) => f.filename).slice(0, 5).join(', ')}${orphanSql.length > 5 ? ', …' : ''}`,
        });
    }
    for (let i = 0; i < dir.journalEntries.length; i++) {
        if (dir.journalEntries[i].idx !== i) {
            issues.push({
                severity: 'error',
                message: `journal entry at position ${i} has idx=${dir.journalEntries[i].idx} (expected ${i})`,
            });
            break;
        }
    }
    const idxCounts = new Map();
    for (const f of dir.sqlFiles) {
        const list = idxCounts.get(f.idx) ?? [];
        list.push(f.filename);
        idxCounts.set(f.idx, list);
    }
    for (const [idx, files] of idxCounts) {
        if (files.length > 1) {
            issues.push({
                severity: 'error',
                message: `${files.length} .sql files share idx=${idx}: ${files.join(', ')}`,
            });
        }
    }
    if (DRIZZLE_KIT_DIRS.has(dir.name)) {
        const snapshotIdxs = new Set(dir.snapshotFiles.map((s) => s.idx));
        const missing = dir.journalEntries.filter((e) => !snapshotIdxs.has(e.idx));
        if (missing.length > 0) {
            issues.push({
                severity: 'error',
                message: `${missing.length} journal entry(ies) have no matching <idx>_snapshot.json (missing idx: ${missing.map((m) => m.idx).slice(0, 5).join(', ')}${missing.length > 5 ? ', …' : ''})`,
            });
        }
    }
    return issues;
}
function printDirReport(dir, issues) {
    const journalLatest = dir.journalEntries[dir.journalEntries.length - 1]?.tag ?? '(none)';
    const sqlLatest = dir.sqlFiles[dir.sqlFiles.length - 1]?.filename ?? '(none)';
    const snapshotLatest = dir.snapshotFiles[dir.snapshotFiles.length - 1]?.filename ?? '(none)';
    const status = issues.length === 0 ? pc.green('OK') : pc.red(`${issues.length} issue(s)`);
    console.log(`  ${pc.bold(dir.name.padEnd(10))} ${status}`);
    console.log(pc.gray(`    .sql files:    ${dir.sqlFiles.length} (latest: ${sqlLatest})`));
    console.log(pc.gray(`    journal:       ${dir.journalEntries.length} entries (latest: ${journalLatest})`));
    if (DRIZZLE_KIT_DIRS.has(dir.name)) {
        console.log(pc.gray(`    snapshots:     ${dir.snapshotFiles.length} files (latest: ${snapshotLatest})`));
    }
    for (const i of issues) {
        const tag = i.severity === 'error' ? pc.red('✖') : pc.yellow('⚠');
        console.log(`    ${tag} ${i.message}`);
    }
    console.log();
}
async function rebuildJournal(dir) {
    // Preserve version from existing journal if present, else default to '6'
    // (drizzle-kit's current SQLite journal version as of writing — drizzle-kit
    // bumps this when its on-disk format changes).
    const version = dir.journalVersion ?? '6';
    const dialect = dir.name === 'features' || dir.name === 'auth' ? 'sqlite' : 'sqlite';
    const entries = dir.sqlFiles.map((f, i) => ({
        idx: i,
        version,
        when: Math.floor(f.mtimeMs),
        tag: f.tag,
        breakpoints: true,
    }));
    const journal = {
        version,
        dialect,
        entries,
    };
    await fs.mkdir(dir.metaPath, { recursive: true });
    await fs.writeFile(join(dir.metaPath, '_journal.json'), JSON.stringify(journal, null, 2) + '\n', 'utf-8');
}
//# sourceMappingURL=migrations.js.map