import { runMigrations } from "graphile-worker";
import type { PrismaClient } from "~/db.server";
import { prisma } from "~/db.server";
import { env } from "~/env.server";
import { logger } from "~/services/logger.server";
import { PgNotifyService } from "./pgNotify.server";
import { z } from "zod";

export class GraphileMigrationHelperService {
  #prismaClient: PrismaClient;

  constructor(prismaClient: PrismaClient = prisma) {
    this.#prismaClient = prismaClient;
  }

  public async call() {
    this.#logDebug("GraphileMigrationHelperService.call");

    await this.#detectAndPrepareForMigrations();

    await runMigrations({
      connectionString: env.DATABASE_URL,
      schema: env.WORKER_SCHEMA,
    });
  }

  #logDebug(message: string, args?: any) {
    logger.debug(`[migrationHelper] ${message}`, args);
  }

  async #getLatestMigration() {
    const migrationQueryResult = await this.#prismaClient.$queryRawUnsafe(`
      SELECT id FROM ${env.WORKER_SCHEMA}.migrations
      ORDER BY id DESC LIMIT 1
    `);

    const MigrationQueryResultSchema = z.array(z.object({ id: z.number() }));

    const migrationResults = MigrationQueryResultSchema.parse(migrationQueryResult);

    if (!migrationResults.length) {
      // no migrations applied yet
      return -1;
    }

    return migrationResults[0].id;
  }

  async #graphileSchemaExists() {
    const schemaCount = await this.#prismaClient.$executeRaw`
      SELECT schema_name FROM information_schema.schemata
      WHERE schema_name = ${env.WORKER_SCHEMA}
    `;

    return schemaCount === 1;
  }

  /** Helper for graphile-worker v0.14.0 migration. No-op if already migrated. */
  async #detectAndPrepareForMigrations() {
    if (!(await this.#graphileSchemaExists())) {
      // no schema yet, likely first start
      return;
    }

    const latestMigration = await this.#getLatestMigration();

    if (latestMigration < 0) {
      // no migrations found
      return;
    }

    // the first v0.14.0 migration has ID 11
    if (latestMigration > 10) {
      // already migrated
      return;
    }

    // add 15s to graceful shutdown timeout, just to be safe
    const migrationDelayInMs = env.GRACEFUL_SHUTDOWN_TIMEOUT + 15000;

    this.#logDebug("Delaying worker startup due to pending migration", {
      latestMigration,
      migrationDelayInMs,
    });

    console.log(`⚠️  detected pending graphile migration`);
    console.log(`⚠️  notifying running workers`);

    const pgNotify = new PgNotifyService();
    await pgNotify.call("trigger:graphile:migrate", { latestMigration });

    console.log(`⚠️  delaying worker startup by ${migrationDelayInMs}ms`);

    await new Promise((resolve) => setTimeout(resolve, migrationDelayInMs));
  }
}
