import Database from "@tauri-apps/plugin-sql";
import { invoke } from "@tauri-apps/api/core";

let rawInstance: Database | null = null;
let serializedInstance: Database | null = null;

// ---------------------------------------------------------------------------
// DB access serialization (#228 follow-up — fix "database is locked").
//
// tauri-plugin-sql exposes NO transaction primitive to the JS side and loads
// SQLite through a default multi-connection sqlx pool (`Pool::connect` =>
// max_connections = 10). Every db.execute / db.select is an independent
// `pool.acquire()`, so a `BEGIN` issued on one pooled connection and its
// matching `COMMIT` issued on another would strand an open write transaction
// on an idle connection — that connection then holds SQLite's write lock until
// the app restarts, and every later write fails with "database is locked".
//
// Fix: funnel EVERY db operation through a single FIFO async lock. Plain
// reads/writes take the lock per-statement; `withTransaction` holds it across
// the whole BEGIN..COMMIT. With no two operations ever overlapping in time,
// the pool keeps handing back its single idle connection, so a transaction's
// statements all run on the same connection and can never strand. Serializing
// all DB access is a non-issue for a local single-user app where queries are
// tiny and infrequent — and it makes the "atomic" saves genuinely atomic.
// ---------------------------------------------------------------------------

let lockTail: Promise<unknown> = Promise.resolve();

// True only while a `withTransaction` callback is executing. The lock is held
// for that whole callback, so anything it triggers (the threaded handle, or an
// accidental `getDb()` call from a nested service) must run DIRECTLY rather
// than re-entering the non-reentrant lock — otherwise it would deadlock behind
// the transaction that is waiting for it.
let inTransaction = false;

/** Run `fn` after every previously-queued DB operation has settled (FIFO). */
function runExclusive<T>(fn: () => Promise<T>): Promise<T> {
  const result = lockTail.then(fn, fn);
  // Keep the chain alive regardless of fn's outcome; only the tail swallows.
  lockTail = result.then(
    () => undefined,
    () => undefined
  );
  return result;
}

/**
 * Wrap the raw Database so `execute`/`select` are serialized through the lock.
 * While a transaction holds the lock (`inTransaction`), calls run directly so a
 * nested call never deadlocks. The proxy keeps the `Database` type, so call
 * sites (including `db.select<T>(...)`) are unchanged.
 */
function serialize(db: Database): Database {
  return new Proxy(db, {
    get(target, prop, receiver) {
      if (prop === "execute") {
        return (query: string, bindValues?: unknown[]) =>
          inTransaction
            ? target.execute(query, bindValues)
            : runExclusive(() => target.execute(query, bindValues));
      }
      if (prop === "select") {
        return (query: string, bindValues?: unknown[]) =>
          inTransaction
            ? target.select(query, bindValues)
            : runExclusive(() => target.select(query, bindValues));
      }
      const value = Reflect.get(target, prop, receiver);
      return typeof value === "function" ? value.bind(target) : value;
    },
  });
}

export async function getDb(): Promise<Database> {
  if (!serializedInstance) {
    throw new Error("No database connection. Call connectToProfile() first.");
  }
  return serializedInstance;
}

/**
 * Run `fn` as one serialized transaction. The global DB lock is held for the
 * ENTIRE callback so no other db.execute/select can interleave and strand the
 * transaction on a different pooled connection. `fn` receives the db handle and
 * is responsible for issuing BEGIN/COMMIT (and ROLLBACK on error), exactly as
 * the inline pattern did before — `withTransaction` only guarantees exclusive,
 * non-interleaved access for the whole BEGIN..COMMIT.
 */
export async function withTransaction<T>(
  fn: (db: Database) => Promise<T>
): Promise<T> {
  if (!serializedInstance) {
    throw new Error("No database connection. Call connectToProfile() first.");
  }
  const db = serializedInstance;
  return runExclusive(async () => {
    inTransaction = true;
    try {
      return await fn(db);
    } finally {
      inTransaction = false;
    }
  });
}

export async function connectToProfile(dbFilename: string): Promise<void> {
  if (rawInstance) {
    await rawInstance.close();
    rawInstance = null;
    serializedInstance = null;
  }
  // Repair migration checksums before loading (fixes "migration was modified" error)
  try {
    const repaired = await invoke<boolean>("repair_migrations", { dbFilename });
    if (repaired) {
      console.warn("Migration checksums repaired for", dbFilename);
    }
  } catch (e) {
    console.error("Migration repair failed:", e);
  }
  rawInstance = await Database.load(`sqlite:${dbFilename}`);
  serializedInstance = serialize(rawInstance);
}

export async function initializeNewProfileDb(dbFilename: string, sqlStatements: string[]): Promise<void> {
  if (rawInstance) {
    await rawInstance.close();
    rawInstance = null;
    serializedInstance = null;
  }
  rawInstance = await Database.load(`sqlite:${dbFilename}`);
  serializedInstance = serialize(rawInstance);
  for (const sql of sqlStatements) {
    await rawInstance.execute(sql);
  }
}

export async function closeDb(): Promise<void> {
  if (rawInstance) {
    await rawInstance.close();
    rawInstance = null;
    serializedInstance = null;
  }
}
