/* @jsx React.createElement */
// ─────────────────────────────────────────────────────────────────────
// METASPEAK Telemetry
// ─────────────────────────────────────────────────────────────────────
// Universal event collection. The chatbot fires events; the dashboard
// reads them. Events are stored in IndexedDB (large capacity, survives
// reload) under store name 'events', keyed by timestamp.
//
// Schema is intentionally generic — every event has:
//   { id, ts, type, payload }
//
// Where `type` is a string like 'session_start' / 'topic_visited' and
// `payload` carries event-specific fields. The dashboard slices by type
// and aggregates payloads dynamically — no hardcoded topic list.
//
// Privacy note: no PII is stored. We log topic IDs (e.g. 'beach_langkawi')
// not the actual user dialogue text. No timestamps are tied to users.
//
// Public API:
//   Telemetry.log(type, payload)
//   Telemetry.startSession()  → returns sessionId, tracks duration
//   Telemetry.endSession()
//   await Telemetry.queryAll()              → all events
//   await Telemetry.queryRange(fromTs, toTs)  → events in window
//   await Telemetry.aggregate(query)        → reduce events to summary
//   await Telemetry.clear()                 → wipe all events (privacy)
//   await Telemetry.exportJSON()            → download as JSON
//
// The aggregation system uses cursor-based scanning (efficient for
// time-range queries) so the dashboard stays responsive even with
// thousands of events.

(function() {

const DB_NAME = 'metaspeak';
// IMPORTANT: bump version whenever schema changes. We share the metaspeak
// DB with presets.jsx (which uses v2). To add the events store without
// stomping their schema, we use v3 and an additive upgrade.
const DB_VERSION = 3;
const STORE_EVENTS = 'events';

let _dbPromise = null;
function openDB() {
  if (_dbPromise) return _dbPromise;
  _dbPromise = new Promise((resolve, reject) => {
    const req = indexedDB.open(DB_NAME, DB_VERSION);
    req.onupgradeneeded = (e) => {
      const db = req.result;
      // Create stores from previous versions if they don't exist
      // (this handles the case where the user's IDB was at v1 or v2)
      if (!db.objectStoreNames.contains('glb')) {
        db.createObjectStore('glb');
      }
      if (!db.objectStoreNames.contains('anim')) {
        db.createObjectStore('anim');
      }
      if (!db.objectStoreNames.contains(STORE_EVENTS)) {
        const store = db.createObjectStore(STORE_EVENTS, { keyPath: 'id', autoIncrement: true });
        // Index by `ts` (timestamp) for efficient range queries
        store.createIndex('ts', 'ts', { unique: false });
        // Index by `type` for filtering by event type
        store.createIndex('type', 'type', { unique: false });
      }
    };
    req.onsuccess = () => resolve(req.result);
    req.onerror = () => reject(req.error);
  });
  return _dbPromise;
}

// ── Session tracking ───────────────────────────────────────────────
// One sessionId per "start session" → "end session" lifecycle. Used to
// link events together (e.g. all topic_visited events in a session can
// be chained to compute drop-off rates).
let currentSessionId = null;
let sessionStartTs = null;

function uuid() {
  // Compact unique ID — timestamp + random. Good enough for session
  // grouping; not cryptographic.
  return Date.now().toString(36) + Math.random().toString(36).substring(2, 8);
}

async function log(type, payload = {}) {
  if (!type) return;
  const event = {
    ts: Date.now(),
    type,
    sessionId: currentSessionId,
    payload,
  };
  try {
    const db = await openDB();
    const tx = db.transaction(STORE_EVENTS, 'readwrite');
    tx.objectStore(STORE_EVENTS).add(event);
    return new Promise((resolve, reject) => {
      tx.oncomplete = () => resolve();
      tx.onerror = () => reject(tx.error);
    });
  } catch (e) {
    console.warn('[Telemetry] log failed', type, e);
  }
}

function startSession(meta = {}) {
  currentSessionId = uuid();
  sessionStartTs = Date.now();
  log('session_start', { ...meta, sessionId: currentSessionId });
  return currentSessionId;
}

function endSession() {
  if (!currentSessionId) return;
  const duration = Date.now() - sessionStartTs;
  log('session_end', { sessionId: currentSessionId, durationMs: duration });
  currentSessionId = null;
  sessionStartTs = null;
}

function getCurrentSessionId() { return currentSessionId; }

// ── Querying ──────────────────────────────────────────────────────
async function queryAll() {
  const db = await openDB();
  return new Promise((resolve, reject) => {
    const tx = db.transaction(STORE_EVENTS, 'readonly');
    const req = tx.objectStore(STORE_EVENTS).getAll();
    req.onsuccess = () => resolve(req.result || []);
    req.onerror = () => reject(req.error);
  });
}

async function queryRange(fromTs, toTs) {
  const db = await openDB();
  return new Promise((resolve, reject) => {
    const tx = db.transaction(STORE_EVENTS, 'readonly');
    const idx = tx.objectStore(STORE_EVENTS).index('ts');
    const range = IDBKeyRange.bound(fromTs, toTs);
    const out = [];
    idx.openCursor(range).onsuccess = (e) => {
      const cursor = e.target.result;
      if (cursor) { out.push(cursor.value); cursor.continue(); }
      else resolve(out);
    };
    tx.onerror = () => reject(tx.error);
  });
}

async function queryByType(type, limit = Infinity) {
  const db = await openDB();
  return new Promise((resolve, reject) => {
    const tx = db.transaction(STORE_EVENTS, 'readonly');
    const idx = tx.objectStore(STORE_EVENTS).index('type');
    const range = IDBKeyRange.only(type);
    const out = [];
    idx.openCursor(range).onsuccess = (e) => {
      const cursor = e.target.result;
      if (cursor && out.length < limit) {
        out.push(cursor.value);
        cursor.continue();
      } else resolve(out);
    };
    tx.onerror = () => reject(tx.error);
  });
}

// Aggregate events into common summary shapes used by the dashboard.
// Each query is a string defining the aggregation kind. Adding new
// kinds is just adding a case here — the chatbot's logging contract
// stays the same.
async function aggregate(query, opts = {}) {
  switch (query) {
    case 'session_count': {
      // Count of session_start events, optionally within a time window
      const events = opts.fromTs
        ? await queryRange(opts.fromTs, opts.toTs || Date.now())
        : await queryAll();
      return events.filter(e => e.type === 'session_start').length;
    }

    case 'sessions_by_day': {
      // Returns array of { date: 'YYYY-MM-DD', count }
      // Default window = last 7 days
      const fromTs = opts.fromTs || (Date.now() - 7 * 24 * 60 * 60 * 1000);
      const events = await queryRange(fromTs, Date.now());
      const buckets = {};
      // Pre-fill empty days so the chart shows continuity
      const days = Math.ceil((Date.now() - fromTs) / (24 * 60 * 60 * 1000));
      for (let i = 0; i < days; i++) {
        const d = new Date(fromTs + i * 24 * 60 * 60 * 1000);
        buckets[ymd(d)] = 0;
      }
      events.filter(e => e.type === 'session_start').forEach(e => {
        const day = ymd(new Date(e.ts));
        buckets[day] = (buckets[day] || 0) + 1;
      });
      return Object.entries(buckets)
        .map(([date, count]) => ({ date, count }))
        .sort((a, b) => a.date.localeCompare(b.date));
    }

    case 'top_topics': {
      // Universal — counts any topic ID seen in topic_visited events.
      // Returns array of { topic, count, label } sorted desc.
      const events = await queryByType('topic_visited');
      const counts = {};
      const labels = {};
      for (const e of events) {
        const t = e.payload?.topic;
        if (!t) continue;
        counts[t] = (counts[t] || 0) + 1;
        // Capture label if present so dashboard shows human-readable name
        if (e.payload?.title) labels[t] = e.payload.title;
      }
      const limit = opts.limit ?? 10;
      return Object.entries(counts)
        .map(([topic, count]) => ({ topic, count, label: labels[topic] || topic }))
        .sort((a, b) => b.count - a.count)
        .slice(0, limit);
    }

    case 'voice_usage': {
      // Sum chars/calls per provider. Provider is anything chatbot logged
      // — no hardcoded list.
      const events = await queryByType('voice_call');
      const byProvider = {};
      for (const e of events) {
        const p = e.payload?.provider || 'unknown';
        if (!byProvider[p]) byProvider[p] = { calls: 0, chars: 0 };
        byProvider[p].calls += 1;
        byProvider[p].chars += (e.payload?.chars || 0);
      }
      return Object.entries(byProvider).map(([provider, stats]) => ({ provider, ...stats }));
    }

    case 'image_sources': {
      // Count fetches per source (cloudinary / wikimedia / pixabay / placeholder)
      const events = await queryByType('image_fetched');
      const counts = {};
      for (const e of events) {
        const src = e.payload?.source || 'unknown';
        counts[src] = (counts[src] || 0) + 1;
      }
      return Object.entries(counts).map(([source, count]) => ({ source, count }));
    }

    case 'recent_errors': {
      const events = await queryByType('error', opts.limit || 20);
      return events
        .map(e => ({ ts: e.ts, ...e.payload }))
        .sort((a, b) => b.ts - a.ts);
    }

    case 'session_durations': {
      // Pair session_start/session_end by sessionId. Returns durationMs.
      const all = await queryAll();
      const ends = {};
      for (const e of all) {
        if (e.type === 'session_end' && e.payload?.sessionId) {
          ends[e.payload.sessionId] = e.payload.durationMs;
        }
      }
      const durations = Object.values(ends);
      if (!durations.length) return { count: 0, avgMs: 0, medianMs: 0 };
      durations.sort((a, b) => a - b);
      const sum = durations.reduce((a, b) => a + b, 0);
      return {
        count: durations.length,
        avgMs: Math.round(sum / durations.length),
        medianMs: durations[Math.floor(durations.length / 2)],
      };
    }

    default:
      console.warn('[Telemetry] unknown aggregate query:', query);
      return null;
  }
}

function ymd(d) {
  const y = d.getFullYear();
  const m = String(d.getMonth() + 1).padStart(2, '0');
  const day = String(d.getDate()).padStart(2, '0');
  return `${y}-${m}-${day}`;
}

// ── Maintenance ────────────────────────────────────────────────────
async function clear() {
  const db = await openDB();
  return new Promise((resolve, reject) => {
    const tx = db.transaction(STORE_EVENTS, 'readwrite');
    tx.objectStore(STORE_EVENTS).clear();
    tx.oncomplete = () => resolve();
    tx.onerror = () => reject(tx.error);
  });
}

async function exportJSON() {
  const events = await queryAll();
  const blob = new Blob([JSON.stringify(events, null, 2)], { type: 'application/json' });
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = `metaspeak-telemetry-${ymd(new Date())}.json`;
  document.body.appendChild(a);
  a.click();
  document.body.removeChild(a);
  setTimeout(() => URL.revokeObjectURL(url), 1000);
}

async function getStats() {
  // Quick health check used by dashboard headers
  const all = await queryAll();
  return {
    eventCount: all.length,
    firstTs: all[0]?.ts || null,
    lastTs: all[all.length - 1]?.ts || null,
  };
}

// Auto-end session on page unload so durations are accurate
window.addEventListener('pagehide', () => {
  if (currentSessionId) {
    // pagehide fires sync — best-effort write
    try {
      navigator.sendBeacon?.('/_telemetry_end', JSON.stringify({ sessionId: currentSessionId }));
    } catch (_) { /* nothing */ }
    endSession();
  }
});

window.MetaspeakTelemetry = {
  log,
  startSession,
  endSession,
  getCurrentSessionId,
  queryAll,
  queryRange,
  queryByType,
  aggregate,
  clear,
  exportJSON,
  getStats,
};

})();
