// ═════════════════════════════════════════════════════════════════════════════
// MindForge · Layer 1 · core/object-db.jsx
// ─────────────────────────────────────────────────────────────────────────────
// Role     : Generic schemaless JSON document store backed by localStorage,
//            with pub/sub + a React hook for live binding.
// Loaded   : #2
// Reads    : window.React (CDN-loaded in index.html)
// Exposes  : window.{ ObjectDB, useCollection }
// See also : docs/ARCHITECTURE.md §"Layered module map"
// ═════════════════════════════════════════════════════════════════════════════
//
// Note: the *domain* store (Library / Deck / Card) does NOT use this — it
// rolls its own localStorage handling because it needs custom migrations and
// blob-cleanup hooks. ObjectDB is provided as a clean primitive for future
// collections (settings, sessions, etc.). See decks-store.jsx for the
// rationale.
//
// MindForge — Object database (legacy header preserved below)
//
// Generic, schema-less JSON object store backed by localStorage.
// Documents live in named collections, receive an auto id + timestamps,
// and stay in sync across React components via a small pub/sub.
//
// Usage:
//   const users = ObjectDB.collection('users');
//   const u = users.put({ name: 'Lx', score: 12 });   // -> { id, name, score, createdAt, updatedAt }
//   users.get(u.id);
//   users.find(d => d.score > 10);
//   users.remove(u.id);
//
//   // React:
//   const { items, put, remove } = useCollection('users');

const DB_PREFIX = 'mindforge.db.';

// Short, sortable id: base36 timestamp + 16 random hex chars.
const newId = () => {
  const t = Date.now().toString(36);
  const buf = new Uint8Array(8);
  (window.crypto || window.msCrypto).getRandomValues(buf);
  let r = '';
  for (const b of buf) r += b.toString(16).padStart(2, '0');
  return `${t}-${r}`;
};

// name -> { byId: Map<id, doc> }
const _cache = new Map();
// name -> Set<listener>
const _subs = new Map();

const _keyFor = (name) => DB_PREFIX + name;

const _load = (name) => {
  if (_cache.has(name)) return _cache.get(name);
  let docs = [];
  try {
    const raw = localStorage.getItem(_keyFor(name));
    if (raw) {
      const parsed = JSON.parse(raw);
      if (Array.isArray(parsed)) docs = parsed;
    }
  } catch {}
  const entry = { byId: new Map(docs.filter(d => d && d.id != null).map(d => [d.id, d])) };
  _cache.set(name, entry);
  return entry;
};

const _save = (name) => {
  const { byId } = _load(name);
  try {
    localStorage.setItem(_keyFor(name), JSON.stringify([...byId.values()]));
  } catch (e) { console.warn('ObjectDB save failed for', name, e); }
};

const _notify = (name) => {
  const set = _subs.get(name);
  if (set) set.forEach(fn => { try { fn(); } catch {} });
};

const collection = (name) => ({
  name,

  // Insert or upsert. If doc.id exists in the collection, it is replaced.
  put(doc) {
    const c = _load(name);
    const now = Date.now();
    const id = doc?.id ?? newId();
    const prev = c.byId.get(id);
    const next = {
      ...doc,
      id,
      createdAt: prev?.createdAt ?? doc?.createdAt ?? now,
      updatedAt: now,
    };
    c.byId.set(id, next);
    _save(name);
    _notify(name);
    return next;
  },

  // Shallow merge patch into an existing doc. No-op if id is unknown.
  patch(id, patch) {
    const c = _load(name);
    const prev = c.byId.get(id);
    if (!prev) return null;
    const next = { ...prev, ...patch, id, createdAt: prev.createdAt, updatedAt: Date.now() };
    c.byId.set(id, next);
    _save(name);
    _notify(name);
    return next;
  },

  get(id)            { return _load(name).byId.get(id) ?? null; },
  list()             { return [..._load(name).byId.values()]; },
  find(predicate)    { return this.list().filter(predicate); },
  findOne(predicate) { return this.list().find(predicate) ?? null; },
  count()            { return _load(name).byId.size; },

  remove(id) {
    const c = _load(name);
    const ok = c.byId.delete(id);
    if (ok) { _save(name); _notify(name); }
    return ok;
  },

  clear() {
    const c = _load(name);
    if (!c.byId.size) return;
    c.byId.clear();
    _save(name);
    _notify(name);
  },

  // Returns an unsubscribe function.
  subscribe(fn) {
    if (!_subs.has(name)) _subs.set(name, new Set());
    _subs.get(name).add(fn);
    return () => _subs.get(name)?.delete(fn);
  },
});

// All collections present in storage or already loaded.
const listCollections = () => {
  const names = new Set(_cache.keys());
  for (let i = 0; i < localStorage.length; i++) {
    const k = localStorage.key(i);
    if (k && k.startsWith(DB_PREFIX)) names.add(k.slice(DB_PREFIX.length));
  }
  return [...names];
};

const dropCollection = (name) => {
  _cache.delete(name);
  try { localStorage.removeItem(_keyFor(name)); } catch {}
  _notify(name);
};

// React hook: live list of documents in a collection.
const useCollection = (name) => {
  const col = React.useMemo(() => collection(name), [name]);
  const [items, setItems] = React.useState(() => col.list());
  React.useEffect(() => {
    const refresh = () => setItems(col.list());
    refresh();
    return col.subscribe(refresh);
  }, [col]);
  return {
    items,
    count:   items.length,
    put:     React.useCallback((doc)     => col.put(doc),       [col]),
    patch:   React.useCallback((id, p)   => col.patch(id, p),   [col]),
    get:     React.useCallback((id)      => col.get(id),        [col]),
    find:    React.useCallback((pred)    => col.find(pred),     [col]),
    findOne: React.useCallback((pred)    => col.findOne(pred),  [col]),
    remove:  React.useCallback((id)      => col.remove(id),     [col]),
    clear:   React.useCallback(()        => col.clear(),        [col]),
  };
};

const ObjectDB = { collection, useCollection, listCollections, dropCollection, newId };

Object.assign(window, { ObjectDB, useCollection });
