// storage.jsx — IndexedDB persistence for sensor readings.
// Survives page reloads, updates, and browser restarts (until the user
// clears site data). Replaces nothing in the data layer — SensorStore
// reads from here on boot and writes every minute.
//
// Schema (db: "datasite", v1):
//   • readings    keyPath="id" autoIncrement
//                 index "by_sensor_t" on [sensorId, t]
//                 each row: { id, sensorId: 'temp'|'humidity'|'co2', t: ms, v: number }
//   • meta        keyPath="key"  rows: { key, value }
//                 used keys: "schemaVersion", "seeded"
//
// Public API (all async):
//   await Storage.open()
//   await Storage.appendReading(sensorId, t, v)
//   await Storage.appendMany([{sensorId,t,v}, …])      // bulk
//   await Storage.readingsSince(sensorId, sinceMs)     // sorted ascending
//   await Storage.allReadings(sensorId)
//   await Storage.lastReading(sensorId)
//   await Storage.countReadings()
//   await Storage.purgeOlderThan(ms)                   // ring-buffer trim
//   await Storage.clearAll()
//   await Storage.getMeta(key) / setMeta(key, value)

const DB_NAME = 'datasite';
const DB_VERSION = 1;

let _db = null;
function open() {
  if (_db) return Promise.resolve(_db);
  return new Promise((resolve, reject) => {
    const req = indexedDB.open(DB_NAME, DB_VERSION);
    req.onupgradeneeded = () => {
      const db = req.result;
      if (!db.objectStoreNames.contains('readings')) {
        const os = db.createObjectStore('readings', { keyPath: 'id', autoIncrement: true });
        os.createIndex('by_sensor_t', ['sensorId', 't']);
      }
      if (!db.objectStoreNames.contains('meta')) {
        db.createObjectStore('meta', { keyPath: 'key' });
      }
    };
    req.onsuccess = () => { _db = req.result; resolve(_db); };
    req.onerror = () => reject(req.error);
  });
}

function tx(store, mode) {
  return open().then(db => db.transaction(store, mode).objectStore(store));
}

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

async function appendReading(sensorId, t, v) {
  const os = await tx('readings', 'readwrite');
  return reqP(os.add({ sensorId, t, v }));
}

async function appendMany(rows) {
  if (!rows.length) return;
  const db = await open();
  return new Promise((resolve, reject) => {
    const t = db.transaction('readings', 'readwrite');
    const os = t.objectStore('readings');
    rows.forEach(r => os.add(r));
    t.oncomplete = () => resolve();
    t.onerror = () => reject(t.error);
    t.onabort = () => reject(t.error);
  });
}

async function readingsSince(sensorId, sinceMs) {
  // getAll() against the compound index range is dramatically faster than a
  // cursor walk for large stores — a single batched read instead of N round
  // trips. Refreshes used to take seconds with 100k+ rows.
  const os = await tx('readings', 'readonly');
  const idx = os.index('by_sensor_t');
  const range = IDBKeyRange.bound([sensorId, sinceMs], [sensorId, Number.MAX_SAFE_INTEGER]);
  return new Promise((resolve, reject) => {
    const req = idx.getAll(range);
    req.onsuccess = () => resolve(req.result.map(r => ({ t: r.t, v: r.v })));
    req.onerror = () => reject(req.error);
  });
}

async function allReadings(sensorId) {
  return readingsSince(sensorId, 0);
}

async function lastReading(sensorId) {
  const os = await tx('readings', 'readonly');
  const idx = os.index('by_sensor_t');
  const range = IDBKeyRange.bound([sensorId, 0], [sensorId, Number.MAX_SAFE_INTEGER]);
  return new Promise((resolve, reject) => {
    const cur = idx.openCursor(range, 'prev');
    cur.onsuccess = e => {
      const c = e.target.result;
      resolve(c ? { t: c.value.t, v: c.value.v } : null);
    };
    cur.onerror = () => reject(cur.error);
  });
}

async function countReadings() {
  const os = await tx('readings', 'readonly');
  return reqP(os.count());
}

async function purgeOlderThan(cutoffMs) {
  const db = await open();
  return new Promise((resolve, reject) => {
    const t = db.transaction('readings', 'readwrite');
    const os = t.objectStore('readings');
    const idx = os.index('by_sensor_t');
    // walk every row whose t < cutoff (across all sensors)
    const cur = idx.openCursor();
    let removed = 0;
    cur.onsuccess = e => {
      const c = e.target.result;
      if (!c) return;
      if (c.value.t < cutoffMs) { c.delete(); removed++; }
      c.continue();
    };
    t.oncomplete = () => resolve(removed);
    t.onerror = () => reject(t.error);
  });
}

async function clearAll() {
  const db = await open();
  return new Promise((resolve, reject) => {
    const t = db.transaction(['readings', 'meta'], 'readwrite');
    t.objectStore('readings').clear();
    t.objectStore('meta').clear();
    t.oncomplete = resolve;
    t.onerror = () => reject(t.error);
  });
}

async function getMeta(key) {
  const os = await tx('meta', 'readonly');
  const r = await reqP(os.get(key));
  return r ? r.value : null;
}

async function setMeta(key, value) {
  const os = await tx('meta', 'readwrite');
  return reqP(os.put({ key, value }));
}

window.Storage = {
  open, appendReading, appendMany, readingsSince, allReadings,
  lastReading, countReadings, purgeOlderThan, clearAll, getMeta, setMeta,
};
