import { promises as fs } from 'node:fs';
import { exists } from 'node:fs/promises';
import * as path from 'node:path';
import { join } from 'node:path';
import { PGlite } from '@electric-sql/pglite';
import {
  CamelCasePlugin,
  FileMigrationProvider,
  Kysely,
  Migrator,
} from 'kysely';
import codegen from 'kysely-codegen';
import { KyselyPGlite } from 'kysely-pglite';
import type { DB } from '../v1.generated';

async function migrate(root: string, client: PGlite) {
  console.log('migrating pglite ....');
  const glob = new Bun.Glob('initial/*.sql');
  for await (const migration of glob.scan(root)) {
    await client.exec(await Bun.file(join(root, migration)).text());
  }
}

async function migrateKysely(root: string, kysely: Kysely<any>) {
  console.log('init migrating kysely ....');
  const migrator = new Migrator({
    db: kysely,
    migrationTableSchema: 'core',
    migrationTableName: 'kysely_migration',
    migrationLockTableName: 'kysely_migration_lock',
    provider: new FileMigrationProvider({
      fs,
      path,
      // This needs to be an absolute path.
      migrationFolder: path.join(root, 'migrations'),
    }),
  });

  console.log('migrating kysely ....');
  const { error, results } = await migrator.migrateToLatest().catch(() => {
    console.error('failed to migrate');
    process.exit(1);
  });

  for (const it of results ?? []) {
    if (it.status === 'Success') {
      console.log(`migration "${it.migrationName}" was executed successfully`);
    } else if (it.status === 'Error') {
      console.error(`failed to execute migration "${it.migrationName}"`);
    }
  }

  if (error) {
    console.error('failed to migrate');
    console.error(error);
    process.exit(1);
  }
}

async function generateDump(root: string, dumpPath: string) {
  const dataExists = await exists(dumpPath).catch(() => false);
  if (!dataExists) {
    console.log('Creating initial dump');
    const main = new PGlite();
    await migrate(root, main).catch(async (err) => {
      console.error('failed to migrate initial dump');
      await Bun.write(
        '../logs/migrate-initial-dump.json',
        JSON.stringify(err as any, null, 2)
      );
      process.exit(1);
    });
    const { dialect } = new KyselyPGlite(main);
    const kysely = new Kysely<DB>({
      dialect,
      plugins: [new CamelCasePlugin()],
    });
    await migrateKysely(root, kysely).catch(() => {
      console.error('failed to migrate kysely');
      process.exit(1);
    });
    await codegen
      .generate({
        db: kysely,
        outFile: path.join(root, 'src/v1.generated.ts'),
        defaultSchemas: ['xxx'],
        camelCase: true,
        dialect: codegen.getDialect('postgres'),
      })
      .catch(() => {
        console.error('failed to generate kysely');
        process.exit(1);
      });
    const content = await main.dumpDataDir('none').catch(() => {
      console.error('failed to dump data');
      process.exit(1);
    });
    await Bun.write(dumpPath, content).catch(() => {
      console.error('failed to write dump');
      process.exit(1);
    });
    return content;
  }
  return Bun.file(dumpPath);
}

export async function createLocalKysely(root: string, dumpPath: string) {
  let isDestroyed = false;
  const now = performance.now();

  const dump = await generateDump(root, dumpPath);

  const pglite = new PGlite({ loadDataDir: dump });
  // warm up
  await pglite.sql`SELECT 1`;
  const { dialect } = new KyselyPGlite(pglite);
  const kysely = new Kysely<DB>({
    dialect,
    plugins: [new CamelCasePlugin()],
  });

  const originalDestroy = kysely.destroy.bind(kysely);
  kysely.destroy = async () => {
    if (isDestroyed) return;
    isDestroyed = true;
    const destroyTime = performance.now();
    await originalDestroy().catch(() => undefined);
    await pglite.close().catch(() => undefined);
    console.log(
      `Database destroyed: ${Math.round(performance.now() - destroyTime)}ms`
    );
  };

  console.log(`Database initialized: ${Math.round(performance.now() - now)}ms`);

  return kysely;
}
