import initSqlJs from "@jlongster/sql.js";
import { SQLiteFS } from "absurd-sql";
import IndexedDBBackend from "absurd-sql/dist/indexeddb-backend";

import { runMigrations } from "../db-sql/migration";
import type { DatabaseService, QueryExecResult } from "../interfaces/database";
import { logger } from "@/utils/logger";

interface QueuedOperation {
  type: "run" | "query";
  sql: string;
  params: unknown[];
  resolve: (value: unknown) => void;
  reject: (reason: unknown) => void;
}

interface AbsurdSqlDatabase {
  exec: (sql: string, params?: unknown[]) => Promise<QueryExecResult[]>;
  run: (
    sql: string,
    params?: unknown[],
  ) => Promise<{ changes: number; lastId?: number }>;
}

class AbsurdSqlDatabaseService implements DatabaseService {
  private static instance: AbsurdSqlDatabaseService | null = null;
  private db: AbsurdSqlDatabase | null;
  private initialized: boolean;
  private initializationPromise: Promise<void> | null = null;
  private operationQueue: Array<QueuedOperation> = [];
  private isProcessingQueue: boolean = false;

  private constructor() {
    this.db = null;
    this.initialized = false;
  }

  static getInstance(): AbsurdSqlDatabaseService {
    if (!AbsurdSqlDatabaseService.instance) {
      AbsurdSqlDatabaseService.instance = new AbsurdSqlDatabaseService();
    }
    return AbsurdSqlDatabaseService.instance;
  }

  async initialize(): Promise<void> {
    // If already initialized, return immediately
    if (this.initialized) {
      return;
    }

    // If initialization is in progress, wait for it
    if (this.initializationPromise) {
      return this.initializationPromise;
    }

    // Start initialization
    this.initializationPromise = this._initialize();
    try {
      await this.initializationPromise;
    } catch (error) {
      logger.error(`AbsurdSqlDatabaseService initialize method failed:`, error);
      this.initializationPromise = null; // Reset on failure
      throw error;
    }
  }

  private async _initialize(): Promise<void> {
    if (this.initialized) {
      return;
    }

    const SQL = await initSqlJs({
      locateFile: (file: string) => {
        return new URL(
          `/node_modules/@jlongster/sql.js/dist/${file}`,
          import.meta.url,
        ).href;
      },
    });

    const sqlFS = new SQLiteFS(SQL.FS, new IndexedDBBackend());
    SQL.register_for_idb(sqlFS);

    SQL.FS.mkdir("/sql");
    SQL.FS.mount(sqlFS, {}, "/sql");

    const path = "/sql/timesafari.absurd-sql";
    if (typeof SharedArrayBuffer === "undefined") {
      const stream = SQL.FS.open(path, "a+");
      await stream.node.contents.readIfFallback();
      SQL.FS.close(stream);
    }

    this.db = new SQL.Database(path, { filename: true });
    if (!this.db) {
      throw new Error(
        "The database initialization failed. We recommend you restart or reinstall.",
      );
    }

    // An error is thrown without this pragma: "File has invalid page size. (the first block of a new file must be written first)"
    await this.db.exec(`PRAGMA journal_mode=MEMORY;`);
    const sqlExec = this.db.run.bind(this.db);
    const sqlQuery = this.db.exec.bind(this.db);

    // Extract the migration names for the absurd-sql format
    const extractMigrationNames: (result: QueryExecResult[]) => Set<string> = (
      result,
    ) => {
      // Even with the "select name" query, the QueryExecResult may be [] (which doesn't make sense to me).
      const names = result?.[0]?.values.map((row) => row[0] as string) || [];
      return new Set(names);
    };

    // Run migrations
    await runMigrations(sqlExec, sqlQuery, extractMigrationNames);

    this.initialized = true;

    // Start processing the queue after initialization
    this.processQueue();
  }

  private async processQueue(): Promise<void> {
    if (this.isProcessingQueue || !this.initialized || !this.db) {
      return;
    }

    this.isProcessingQueue = true;

    while (this.operationQueue.length > 0) {
      const operation = this.operationQueue.shift();
      if (!operation) continue;

      try {
        let result: unknown;
        switch (operation.type) {
          case "run":
            result = await this.db.run(operation.sql, operation.params);
            break;
          case "query":
            result = await this.db.exec(operation.sql, operation.params);
            break;
        }
        operation.resolve(result);
      } catch (error) {
        logger.error(
          "Error while processing SQL queue:",
          error,
          " ... for sql:",
          operation.sql,
          " ... with params:",
          operation.params,
        );
        operation.reject(error);
      }
    }

    this.isProcessingQueue = false;
  }

  private async queueOperation<R>(
    type: QueuedOperation["type"],
    sql: string,
    params: unknown[] = [],
  ): Promise<R> {
    return new Promise<R>((resolve, reject) => {
      const operation: QueuedOperation = {
        type,
        sql,
        params,
        resolve: (value: unknown) => resolve(value as R),
        reject,
      };
      this.operationQueue.push(operation);

      // If we're already initialized, start processing the queue
      if (this.initialized && this.db) {
        this.processQueue();
      }
    });
  }

  private async waitForInitialization(): Promise<void> {
    // If we have an initialization promise, wait for it
    if (this.initializationPromise) {
      await this.initializationPromise;
      return;
    }

    // If not initialized and no promise, start initialization
    if (!this.initialized) {
      await this.initialize();
      return;
    }

    // If initialized but no db, something went wrong
    if (!this.db) {
      logger.error(
        `Database not properly initialized after await waitForInitialization() - initialized flag is true but db is null`,
      );
      throw new Error(
        `The database could not be initialized. We recommend you restart or reinstall.`,
      );
    }
  }

  // Used for inserts, updates, and deletes
  async run(
    sql: string,
    params: unknown[] = [],
  ): Promise<{ changes: number; lastId?: number }> {
    await this.waitForInitialization();
    return this.queueOperation<{ changes: number; lastId?: number }>(
      "run",
      sql,
      params,
    );
  }

  // Note that the resulting array may be empty if there are no results from the query
  async query(sql: string, params: unknown[] = []): Promise<QueryExecResult[]> {
    await this.waitForInitialization();
    return this.queueOperation<QueryExecResult[]>("query", sql, params);
  }
}

// Create a singleton instance
const databaseService = AbsurdSqlDatabaseService.getInstance();

export default databaseService;
