/* MediCore Offline Module — IndexedDB-backed offline-first layer
 * Provides: product/customer cache, offline queue, auto-sync on reconnect,
 *           online/offline badge, crash-recovery, browser-sync notifications.
 */
(function (w) {
  'use strict';

  const DB_NAME    = 'medicore_offline';
  const DB_VERSION = 1;

  let _db      = null;
  let _online  = navigator.onLine;
  let _syncing = false;
  let _syncTimer = null;

  /* ── Device ID (persistent, used for conflict detection) ─── */
  let DEVICE_ID = localStorage.getItem('mc_device_id');
  if (!DEVICE_ID) {
    DEVICE_ID = 'DEV-' + Math.random().toString(36).slice(2, 10).toUpperCase();
    localStorage.setItem('mc_device_id', DEVICE_ID);
  }

  /* ── UUID v4 ─────────────────────────────────────────────── */
  function uuid() {
    return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
      const r = Math.random() * 16 | 0;
      return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
    });
  }

  /* ── IndexedDB helpers ───────────────────────────────────── */
  function openDB() {
    return new Promise((resolve, reject) => {
      if (_db) { resolve(_db); return; }
      const req = indexedDB.open(DB_NAME, DB_VERSION);
      req.onerror = () => reject(req.error);
      req.onsuccess = e => { _db = e.target.result; resolve(_db); };
      req.onupgradeneeded = e => {
        const d = e.target.result;
        // Products cache
        if (!d.objectStoreNames.contains('products')) {
          const ps = d.createObjectStore('products', { keyPath: 'id' });
          ps.createIndex('name',    'name',    { unique: false });
          ps.createIndex('barcode', 'barcode', { unique: false });
        }
        // Customers cache
        if (!d.objectStoreNames.contains('customers')) {
          const cs = d.createObjectStore('customers', { keyPath: 'id' });
          cs.createIndex('name',  'name',  { unique: false });
          cs.createIndex('phone', 'phone', { unique: false });
        }
        // Suppliers cache
        if (!d.objectStoreNames.contains('suppliers')) {
          const ss = d.createObjectStore('suppliers', { keyPath: 'id' });
          ss.createIndex('name', 'name', { unique: false });
        }
        // Offline queues
        ['queue_sales', 'queue_purchases', 'queue_holds'].forEach(name => {
          if (!d.objectStoreNames.contains(name)) {
            const qs = d.createObjectStore(name, { keyPath: 'uuid' });
            qs.createIndex('status', 'status', { unique: false });
          }
        });
        // Local stock reservation
        if (!d.objectStoreNames.contains('stock_reserve')) {
          d.createObjectStore('stock_reserve', { keyPath: 'product_id' });
        }
      };
    });
  }

  function idbPut(store, data) {
    return openDB().then(d => new Promise((res, rej) => {
      const tx  = d.transaction(store, 'readwrite');
      const req = tx.objectStore(store).put(data);
      req.onsuccess = () => res(req.result);
      req.onerror   = () => rej(req.error);
    }));
  }

  function idbGetAll(store) {
    return openDB().then(d => new Promise((res, rej) => {
      const tx  = d.transaction(store, 'readonly');
      const req = tx.objectStore(store).getAll();
      req.onsuccess = () => res(req.result || []);
      req.onerror   = () => rej(req.error);
    }));
  }

  function idbDelete(store, key) {
    return openDB().then(d => new Promise((res, rej) => {
      const tx  = d.transaction(store, 'readwrite');
      const req = tx.objectStore(store).delete(key);
      req.onsuccess = () => res();
      req.onerror   = () => rej(req.error);
    }));
  }

  /* Full-text search across an object store */
  function idbFullSearch(store, fields, query, limit = 14) {
    return openDB().then(d => new Promise((res, rej) => {
      const results = [];
      const q = query.toLowerCase().trim();
      const req = d.transaction(store, 'readonly').objectStore(store).openCursor();
      req.onsuccess = e => {
        const cursor = e.target.result;
        if (!cursor || results.length >= limit) { res(results); return; }
        const v = cursor.value;
        const hit = fields.some(f => {
          const val = String(v[f] || '').toLowerCase();
          return val.includes(q);
        });
        if (hit) results.push(v);
        cursor.continue();
      };
      req.onerror = () => rej(req.error);
    }));
  }

  /* ── Fetch with timeout ───────────────────────────────────── */
  async function fetchJSON(url, body) {
    const ctrl = new AbortController();
    const timer = setTimeout(() => ctrl.abort(), 5000);
    try {
      const res = await fetch(url, {
        method:  'POST',
        headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' },
        body:    JSON.stringify(body),
        signal:  ctrl.signal,
      });
      clearTimeout(timer);
      return res.json();
    } catch (e) {
      clearTimeout(timer);
      throw e;
    }
  }

  /* ── UI helpers ───────────────────────────────────────────── */
  function updateBadge(status, pending) {
    const badge = document.getElementById('offline-badge');
    if (!badge) return;
    badge.className = 'offline-badge badge-' + status;
    const icons = { online: '🟢', offline: '🔴', syncing: '🟡' };
    const labels = { online: 'Online', offline: 'Offline', syncing: 'Syncing…' };
    const dot  = badge.querySelector('.ob-dot');
    const text = badge.querySelector('.ob-text');
    const cnt  = badge.querySelector('.ob-count');
    if (dot)  dot.textContent  = icons[status]  || '⚪';
    if (text) text.textContent = labels[status] || status;
    if (cnt) {
      cnt.textContent = pending > 0 ? pending : '';
      cnt.style.display = pending > 0 ? 'inline-flex' : 'none';
    }
  }

  function toast(msg, type = 'info', dur = 4500) {
    const colors = {
      success: { bg: '#d4f0e6', cl: '#065f46', bd: '#6ee7b7' },
      error:   { bg: '#fde8e8', cl: '#991b1b', bd: '#f5a0a0' },
      info:    { bg: '#e0f2ff', cl: '#0369a1', bd: '#7dd3fc' },
      warn:    { bg: '#fff3dc', cl: '#78350f', bd: '#f59e0b' },
    };
    const { bg, cl, bd } = colors[type] || colors.info;
    const el = document.createElement('div');
    el.style.cssText = `position:fixed;bottom:24px;right:20px;z-index:99999;
      padding:10px 16px;border-radius:8px;font-size:13px;font-weight:600;
      background:${bg};color:${cl};border:1px solid ${bd};
      box-shadow:0 4px 20px rgba(0,0,0,.18);max-width:320px;
      transition:opacity .4s;cursor:pointer`;
    el.textContent = msg;
    el.onclick = () => el.remove();
    document.body.appendChild(el);
    setTimeout(() => { el.style.opacity = '0'; setTimeout(() => el.remove(), 400); }, dur);
  }

  /* ── Pending count ────────────────────────────────────────── */
  async function getPending() {
    try {
      const [s, p, h] = await Promise.all([
        idbGetAll('queue_sales'),
        idbGetAll('queue_purchases'),
        idbGetAll('queue_holds'),
      ]);
      return [...s, ...p, ...h].filter(r => r.status === 'pending').length;
    } catch { return 0; }
  }

  /* ── Cache refresh ────────────────────────────────────────── */
  async function refreshCache(apiBase) {
    const base = apiBase || w.MEDI_API_BASE || '../api/';

    // Products
    try {
      const r = await fetchJSON(base + 'products.php', { action: 'list', q: '', limit: 9999 });
      if (r.success && r.data) {
        const db = await openDB();
        const tx = db.transaction('products', 'readwrite');
        const st = tx.objectStore('products');
        r.data.forEach(p => st.put(p));
        await new Promise((res, rej) => { tx.oncomplete = res; tx.onerror = rej; });
      }
    } catch (e) { console.warn('[MediOffline] product cache:', e.message); }

    // Customers
    try {
      const r = await fetchJSON(base + 'customers.php', { action: 'list', q: '' });
      if (r.success && r.data) {
        const db = await openDB();
        const tx = db.transaction('customers', 'readwrite');
        const st = tx.objectStore('customers');
        r.data.forEach(c => st.put({ id: c.id, name: c.name, phone: c.phone || '',
          due: c.due || 0, advance: c.advance || 0, is_active: c.is_active }));
        await new Promise((res, rej) => { tx.oncomplete = res; tx.onerror = rej; });
      }
    } catch (e) { console.warn('[MediOffline] customer cache:', e.message); }

    // Suppliers
    try {
      const r = await fetchJSON(base + 'suppliers.php', { action: 'list', q: '' });
      if (r.success && r.data) {
        const db = await openDB();
        const tx = db.transaction('suppliers', 'readwrite');
        const st = tx.objectStore('suppliers');
        r.data.forEach(s => st.put(s));
        await new Promise((res, rej) => { tx.oncomplete = res; tx.onerror = rej; });
      }
    } catch (e) { console.warn('[MediOffline] supplier cache:', e.message); }

    console.log('[MediOffline] Cache refreshed');
  }

  /* ── Sync engine ─────────────────────────────────────────── */
  async function syncAll(apiBase) {
    if (_syncing || !_online) return;
    const pending = await getPending();
    if (!pending) return;

    _syncing = true;
    const base = apiBase || w.MEDI_API_BASE || '../api/';
    updateBadge('syncing', pending);

    let synced = 0, failed = 0;

    // Sync sales
    const sales = (await idbGetAll('queue_sales')).filter(r => r.status === 'pending');
    for (const rec of sales) {
      try {
        const r = await fetchJSON(base + 'sales.php', rec.payload);
        if (r.success) {
          await idbPut('queue_sales', { ...rec, status: 'synced', synced_invoice: r.invoice, synced_at: new Date().toISOString() });
          synced++;
        } else {
          await idbPut('queue_sales', { ...rec, status: 'failed', error: r.error });
          failed++;
        }
      } catch (e) { failed++; }
    }

    // Sync purchases
    const purchases = (await idbGetAll('queue_purchases')).filter(r => r.status === 'pending');
    for (const rec of purchases) {
      try {
        const r = await fetchJSON(base + 'purchases.php', rec.payload);
        if (r.success) {
          await idbPut('queue_purchases', { ...rec, status: 'synced', synced_at: new Date().toISOString() });
          synced++;
        } else {
          await idbPut('queue_purchases', { ...rec, status: 'failed', error: r.error });
          failed++;
        }
      } catch (e) { failed++; }
    }

    // Sync holds
    const holds = (await idbGetAll('queue_holds')).filter(r => r.status === 'pending');
    for (const rec of holds) {
      try {
        const action = rec.hold_type === 'purchase' ? 'save_purchase' : 'save_sale';
        const r = await fetchJSON(base + 'holds.php', { action, ...rec.payload });
        if (r.success) {
          await idbPut('queue_holds', { ...rec, status: 'synced', synced_at: new Date().toISOString() });
          synced++;
        } else {
          await idbPut('queue_holds', { ...rec, status: 'failed', error: r.error });
          failed++;
        }
      } catch (e) { failed++; }
    }

    _syncing = false;
    const remaining = await getPending();
    updateBadge(_online ? 'online' : 'offline', remaining);

    if (synced > 0) toast(`✅ ${synced} transaction(s) synced successfully`, 'success');
    if (failed  > 0) toast(`⚠ ${failed} transaction(s) failed to sync — will retry`, 'error');
  }

  /* ════════════════════════════════════════════════════════════
     Public API
  ════════════════════════════════════════════════════════════ */
  const MediOffline = {

    isOnline: () => _online,

    /* Initialise: open DB, set up listeners, refresh cache */
    async init(apiBase) {
      await openDB();

      window.addEventListener('online', async () => {
        _online = true;
        toast('🟢 Connection restored — syncing…', 'success', 3000);
        await syncAll(apiBase);
        await refreshCache(apiBase);
        updateBadge('online', await getPending());
      });

      window.addEventListener('offline', async () => {
        _online = false;
        toast('🔴 Connection lost — working offline', 'warn', 5000);
        updateBadge('offline', await getPending());
      });

      const pending = await getPending();
      updateBadge(_online ? 'online' : 'offline', pending);

      if (_online) {
        // Refresh cache in background — don't block page load
        setTimeout(() => refreshCache(apiBase), 800);
        // If there are pending items from a previous offline session, sync them
        if (pending > 0) setTimeout(() => syncAll(apiBase), 2000);
      }

      return this;
    },

    /* Cache a single product/customer when loaded online */
    cacheOne(store, obj) { idbPut(store, obj).catch(() => {}); },

    /* Search products locally */
    async searchProducts(q) {
      return idbFullSearch('products', ['name', 'generic_name', 'company_name', 'barcode', 'strength', 'form'], q);
    },

    /* Search customers locally */
    async searchCustomers(q) {
      const results = await idbFullSearch('customers', ['name', 'phone'], q);
      return results.filter(c => c.id > 1);
    },

    /* Search suppliers locally */
    async searchSuppliers(q) {
      return idbFullSearch('suppliers', ['name', 'phone'], q);
    },

    /* Queue an offline sale */
    async queueSale(payload) {
      const rec = { uuid: uuid(), device_id: DEVICE_ID, status: 'pending',
                    hold_type: 'sale', created_at: new Date().toISOString(), payload };
      await idbPut('queue_sales', rec);
      const cnt = await getPending();
      updateBadge('offline', cnt);
      return rec.uuid;
    },

    /* Queue an offline purchase */
    async queuePurchase(payload) {
      const rec = { uuid: uuid(), device_id: DEVICE_ID, status: 'pending',
                    hold_type: 'purchase', created_at: new Date().toISOString(), payload };
      await idbPut('queue_purchases', rec);
      const cnt = await getPending();
      updateBadge('offline', cnt);
      return rec.uuid;
    },

    /* Queue an offline hold */
    async queueHold(payload, holdType = 'sale') {
      const rec = { uuid: uuid(), device_id: DEVICE_ID, status: 'pending',
                    hold_type: holdType, created_at: new Date().toISOString(), payload };
      await idbPut('queue_holds', rec);
      const cnt = await getPending();
      updateBadge('offline', cnt);
      return rec.uuid;
    },

    /* Get pending count */
    getPendingCount: getPending,

    /* Manual sync trigger */
    sync(apiBase) { return syncAll(apiBase); },

    /* Refresh data cache */
    refreshCache,

    /* Utility */
    toast,
    updateBadge,
  };

  w.MediOffline = MediOffline;
})(window);
