import { Database } from "jsr:@db/sqlite";
import log from "@shared/utils/logger.ts";

const MIGRATION_TABLE = "_keyborgMigrations";

interface MigrationRecord {
    name: string;
    description: string;
    step: number;
    appliedAt: string;
}

export class Migration {
    constructor(
        public readonly name: string,
        public readonly description: string,
        public readonly upQueries: string[],
        public readonly downQueries: string[],
        public readonly step: number,
    ) {}

    static create = (() => {
        let step = 0;

        return (
            name: string,
            description: string,
            upQueries: string[],
            downQueries: string[],
        ) => new Migration(name, description, upQueries, downQueries, step++);
    })();
}

export class MigrationManager {
    constructor(
        private readonly migrations: Migration[],
        private readonly db: Database,
    ) {}

    public init() {
        if (!this.doesMigrationTableExist()) {
            if (this.hasExistingTables()) {
                log.critical(
                    "Attempting to initialize migrations on a non-empty database.",
                );
                Deno.exit(1);
            }

            this.createMigrationsTable();
        }

        if (!this.areMigrationsSequential()) {
            log.critical("Migrations are not applied in sequential order!");
            Deno.exit(1);
        }
        this.applyPendingMigrations();
    }

    private createMigrationsTable(): void {
        const sql = `
            CREATE TABLE IF NOT EXISTS ${MIGRATION_TABLE} (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                name TEXT,
                description TEXT,
                step INTEGER NOT NULL UNIQUE,
                appliedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP
            )
        `;

        this.executeSQLOrExit(sql);
    }

    private doesMigrationTableExist() {
        const sql = `
            SELECT name FROM sqlite_master WHERE type='table' AND name='${MIGRATION_TABLE}';
        `;

        return !!this.getOrExit(sql);
    }

    private hasExistingTables() {
        const sql = `
            SELECT name FROM sqlite_master 
            WHERE type='table' 
            AND name NOT IN ('sqlite_sequence', '${MIGRATION_TABLE}');
        `;

        const tables = this.allOrExit(sql);
        return tables.length > 0;
    }

    private areMigrationsSequential(): boolean {
        const sql = `
            SELECT step FROM ${MIGRATION_TABLE} ORDER BY step ASC;
        `;

        const records = this.allOrExit<{ step: number }>(sql);

        // check if array is dense
        for (let i = 0; i < records.length; i++) {
            if (records[i].step !== i) {
                return false;
            }
        }

        return true;
    }

    private getCurrentMigrationStep(): number {
        const sql = `
            SELECT MAX(step) AS step FROM ${MIGRATION_TABLE};
        `;

        const latestMigrationStep = this.getOrExit<Record<"step", number>>(sql);

        return latestMigrationStep?.step || -1;
    }

    private applyPendingMigrations(): void {
        const currentStep = this.getCurrentMigrationStep();
        const pendingMigrations = this.migrations.filter(
            (migration) => migration.step > currentStep,
        );

        for (const migration of pendingMigrations) {
            this.applyMigration(migration);
        }
    }

    private applyMigration(migration: Migration): void {
        if (
            !this.getOrExit(
                `SELECT step FROM ${MIGRATION_TABLE} WHERE step = ?`,
                migration.step,
            )
        ) {
            for (const query of migration.upQueries) {
                this.executeSQLOrExit(query);
            }

            const sql = `
                INSERT INTO ${MIGRATION_TABLE} (name, description, step)
                VALUES (?, ?, ?);
            `;

            this.executeSQLOrExit(
                sql,
                migration.name,
                migration.description,
                migration.step,
            );

            console.info(`Applied migratoin: ${migration.name}`);
        }
    }

    public rollbackMigration(migration: Migration): void {
        try {
            for (const query of migration.downQueries.reverse()) {
                this.executeSQLOrExit(query);
            }

            const deleteSQL = `
                DELETE FROM ${MIGRATION_TABLE} 
                WHERE step = ?;
            `;
            this.executeSQLOrExit(deleteSQL, migration.step);

            console.log(`Rolled back migration: ${migration.name}`);
        } catch (error) {
            console.error(
                `Failed to rollback migration: ${migration.name}`,
                error,
            );
            Deno.exit(1);
        }
    }

    private executeSQLOrExit(
        sql: string,
        ...params: (string | number)[]
    ): void {
        try {
            this.db.exec(sql, params);
        } catch (e) {
            log.critical(
                `Failed to execute sql while applying migrations! Error: ${e}`,
            );
            Deno.exit(1);
        }
    }

    private allOrExit<T extends Record<string, string | number>>(
        sql: string,
        ...params: (string | number)[]
    ): T[] {
        try {
            return this.db.prepare(sql).all<T>(params);
        } catch (e) {
            log.critical(
                `Failed to execute sql while applying migrations! Error: ${e}`,
            );
            Deno.exit(1);
        }
    }

    private getOrExit<T extends Record<string, string | number>>(
        sql: string,
        ...params: (string | number)[]
    ): T | undefined {
        try {
            return this.db.prepare(sql).get<T>(params);
        } catch (e) {
            log.critical(
                `Failed to execute sql while applying migrations! Error: ${e}`,
            );
            Deno.exit(1);
        }
    }
}
