// OrbitCosmo IndexedDB layer — the thin wrapper over the `orbit` database
// (specs/phase1-event-log.md). Nine stores split witnessed from derived:
//
//   events         — append-only visit/manual-link log (autoincrement, url index)
//   snapshots      — latest-wins page content per canonical URL
//   embeddings     — Float32Array vectors keyed by url, gated by contentHash
//   projections    — the persisted derived graph (single "graph" record)
//   dirty          — persisted related-edge rebuild queue (consumed in PR C)
//   wiki           — LLM-maintained wiki pages keyed by slug (v2)
//   wikiVersions   — prior page versions, [slug, version] composite key (v2)
//   wikiEmbeddings — MiniLM vectors for wiki pages, gated by contentHash (v2)
//   wikiState      — single "state" record: ingest ledger + monthly spend (v2)
//
// Witnessed data (events, snapshots, embeddings — and the wiki stores: LLM
// output is not deterministically re-derivable, so wiki pages are witnessed,
// never touched by PROJECTION_VERSION replay) is never regenerated; derived
// data (projections) is always regenerable. IndexedDB is extension-scoped
// and covered by unlimitedStorage. This wrapper is deliberately dumb —
// promises around IDB requests, nothing more. Pure logic lives in
// migrate.js / the projection builders / wiki-schema.js.

(function (root) {
  "use strict";

  const DB_NAME = "orbit";
  const DB_VERSION = 2;
  const STORES = [
    "events", "snapshots", "embeddings", "projections", "dirty",
    "wiki", "wikiVersions", "wikiEmbeddings", "wikiState",
  ];

  let dbPromise = null;

  function openDb() {
    if (dbPromise) return dbPromise;
    dbPromise = new Promise((resolve, reject) => {
      const req = indexedDB.open(DB_NAME, DB_VERSION);
      req.onupgradeneeded = () => {
        // Guarded creates: runs for fresh installs (v0→2) and upgrades
        // (v1→2) alike — each store is created only if missing.
        const db = req.result;
        if (!db.objectStoreNames.contains("events")) {
          const events = db.createObjectStore("events", { autoIncrement: true });
          events.createIndex("url", "url", { unique: false });
        }
        if (!db.objectStoreNames.contains("snapshots")) db.createObjectStore("snapshots", { keyPath: "url" });
        if (!db.objectStoreNames.contains("embeddings")) db.createObjectStore("embeddings", { keyPath: "url" });
        if (!db.objectStoreNames.contains("projections")) db.createObjectStore("projections", { keyPath: "id" });
        if (!db.objectStoreNames.contains("dirty")) db.createObjectStore("dirty", { keyPath: "url" });
        if (!db.objectStoreNames.contains("wiki")) db.createObjectStore("wiki", { keyPath: "slug" });
        if (!db.objectStoreNames.contains("wikiVersions")) {
          db.createObjectStore("wikiVersions", { keyPath: ["slug", "version"] });
        }
        if (!db.objectStoreNames.contains("wikiEmbeddings")) db.createObjectStore("wikiEmbeddings", { keyPath: "slug" });
        if (!db.objectStoreNames.contains("wikiState")) db.createObjectStore("wikiState", { keyPath: "id" });
      };
      req.onsuccess = () => resolve(req.result);
      req.onerror = () => reject(req.error);
    });
    return dbPromise;
  }

  // One transaction, one promise. `fn` receives the store objects in the
  // order named and issues plain IDB requests; the promise settles when the
  // transaction commits (so writes are durable when it resolves).
  async function tx(storeNames, mode, fn) {
    const db = await openDb();
    return new Promise((resolve, reject) => {
      const t = db.transaction(storeNames, mode);
      let result;
      t.oncomplete = () => resolve(result);
      t.onerror = () => reject(t.error);
      t.onabort = () => reject(t.error);
      result = fn(...storeNames.map((n) => t.objectStore(n)));
    });
  }

  function reqValue(request) {
    return new Promise((resolve, reject) => {
      request.onsuccess = () => resolve(request.result);
      request.onerror = () => reject(request.error);
    });
  }

  // Reads resolve with the request's value; writes resolve on transaction
  // commit (durable when the promise settles).
  const api = {
    dbGet: async (store, key) => {
      const db = await openDb();
      return reqValue(db.transaction(store, "readonly").objectStore(store).get(key));
    },
    dbGetAll: async (store) => {
      const db = await openDb();
      return reqValue(db.transaction(store, "readonly").objectStore(store).getAll());
    },
    // { key, value } pairs — for stores with out-of-line autoincrement keys
    // (events), where getAll() alone loses the key needed to delete rows.
    dbGetAllWithKeys: async (store) => {
      const db = await openDb();
      return new Promise((resolve, reject) => {
        const out = [];
        const req = db.transaction(store, "readonly").objectStore(store).openCursor();
        req.onsuccess = () => {
          const cur = req.result;
          if (!cur) return resolve(out);
          out.push({ key: cur.key, value: cur.value });
          cur.continue();
        };
        req.onerror = () => reject(req.error);
      });
    },
    dbPut: (store, value) => tx([store], "readwrite", (s) => { s.put(value); }),
    dbAdd: (store, value) => tx([store], "readwrite", (s) => { s.add(value); }),
    dbDelete: (store, key) => tx([store], "readwrite", (s) => { s.delete(key); }),
    dbDeleteMany: (store, keys) =>
      tx([store], "readwrite", (s) => { for (const k of keys) s.delete(k); }),
    // One atomic multi-store write: `fn` receives the stores in the order
    // named and the promise resolves on commit — all-or-nothing (the blob
    // migration depends on this: a partial write must never survive).
    dbAtomicWrite: (storeNames, fn) => tx(storeNames, "readwrite", fn),
    DB_STORES: STORES,
  };

  Object.assign(root, api);
})(typeof globalThis !== "undefined" ? globalThis : self);
