/* MediCore Live Stock Sync
 * File: assets/stock-sync.js
 *
 * Polls api/stock-sync.php every ~600ms so every open terminal picks up
 * stock changes made by OTHER terminals (sales, purchases, sale
 * returns, purchase returns) within well under a second, without a
 * page reload. Deliberately plain short-interval polling, not SSE/
 * WebSockets — matching the approach already used for the top status
 * bar's notification counts (see assets/topbar.js), after SSE proved
 * unreliable on XAMPP's default Windows Apache setup: every open tab
 * holding one PHP request open continuously (even a bounded, auto-
 * reconnecting SSE stream) can exhaust its small worker-thread pool,
 * making unrelated requests across the WHOLE app hang with no error.
 * A one-shot poll completes and releases its worker in milliseconds
 * either way, no matter how many tabs/terminals are open — structurally
 * immune to that failure mode.
 *
 * Polling this fast is only cheap because the server side is now a
 * strictly-monotonic cursor scan (`stock_events.id > ?`, see
 * api/stock-sync.php + includes/db.php's publish_stock_event()) rather
 * than the old broad `products.updated_at > ?` scan — a request that
 * finds nothing new is a trivial indexed lookup, not a table scan.
 *
 * What it does automatically, with zero per-page setup:
 *   - Any element with data-stock-id="<product id>" gets its
 *     textContent replaced with the fresh stock_pcs number the
 *     moment it changes, plus a brief highlight flash and a
 *     .stock-out / .stock-low class so pages can colour-code it for
 *     free (see .mp-hidden/.stock-flash rules in assets/style.css).
 *   - Any element with data-price-id="<product id>" gets patched the
 *     same way with the fresh sale_price (formatted to 2 decimals,
 *     WITHOUT a currency symbol — the page's own markup already has
 *     one sitting next to it), plus the same flash highlight.
 *
 * What a page opts into itself for anything more involved (e.g.
 * re-validating a shopping cart, rebuilding a dashboard widget's
 * markup): listen for the `medi:stock-sync` CustomEvent on `window`.
 * Only fires when something actually changed — never on an empty poll.
 *   window.addEventListener('medi:stock-sync', e => {
 *     e.detail.changed          // [{id, name, stock_pcs, low_stock_alert, sale_price, event_type, source_type}, ...]
 *     e.detail.lowStockCount    // always a number once the first real change has been seen
 *     e.detail.outOfStockCount
 *     e.detail.stockValue
 *   });
 */
(function (w, d) {
  'use strict';

  const ENDPOINT = () => (w.MEDI_API_BASE || '../api/') + 'stock-sync.php';
  const POLL_MS  = 600;

  let sinceId  = '';   // '' = not established yet; server jumps us straight to "now" on the first poll
  let timer    = null;
  let inFlight = false;

  // Aggregates only arrive from the server when something actually
  // changed (see api/stock-sync.php) — cached here so every dispatched
  // event always carries real numbers, never null, and consumers
  // (dashboard.php) never need to know about the "unchanged" case.
  let lastLowStock   = 0;
  let lastOutStock   = 0;
  let lastStockValue = 0;

  function flashPatch(el, text) {
    el.textContent = text;
    el.classList.remove('stock-flash');
    void el.offsetWidth; // force reflow so the class re-triggers even if already mid-run
    el.classList.add('stock-flash');
  }

  function applyDomPatches(changed) {
    changed.forEach(p => {
      const stockEls = d.querySelectorAll('[data-stock-id="' + p.id + '"]');
      if (stockEls.length) {
        const isOut = p.stock_pcs <= 0;
        const isLow = !isOut && p.low_stock_alert > 0 && p.stock_pcs < p.low_stock_alert;
        stockEls.forEach(el => {
          flashPatch(el, p.stock_pcs.toLocaleString());
          el.classList.toggle('stock-out', isOut);
          el.classList.toggle('stock-low', isLow);
        });
      }

      if (typeof p.sale_price === 'number') {
        const priceEls = d.querySelectorAll('[data-price-id="' + p.id + '"]');
        priceEls.forEach(el => flashPatch(el, p.sale_price.toFixed(2)));
      }
    });
  }

  async function poll() {
    if (inFlight || d.hidden) return; // pause while tab is backgrounded — no point polling what nobody can see
    inFlight = true;
    try {
      const url = ENDPOINT() + '?since_id=' + encodeURIComponent(sinceId);
      const res = await fetch(url, { headers: { 'X-Requested-With': 'XMLHttpRequest' } });
      if (!res.ok) return;
      const data = await res.json();
      if (!data || !data.success) return;

      sinceId = (typeof data.since_id !== 'undefined') ? data.since_id : sinceId;

      const changed = data.changed || [];
      if (!changed.length) return; // nothing new — skip the DOM patch + event dispatch entirely

      applyDomPatches(changed);

      if (data.low_stock    !== null) lastLowStock   = data.low_stock;
      if (data.out_of_stock !== null) lastOutStock   = data.out_of_stock;
      if (data.stock_value  !== null) lastStockValue = data.stock_value;

      w.dispatchEvent(new CustomEvent('medi:stock-sync', {
        detail: {
          changed,
          lowStockCount:    lastLowStock,
          outOfStockCount:  lastOutStock,
          stockValue:       lastStockValue,
        },
      }));
    } catch (e) {
      // Silent — a missed poll is caught up by the next one a fraction
      // of a second later; no need to surface a transient network blip.
    } finally {
      inFlight = false;
    }
  }

  function start() {
    if (timer) return;
    poll(); // establish the baseline cursor right away, don't wait a full interval
    timer = setInterval(poll, POLL_MS);
  }

  // The moment a backgrounded tab regains focus, poll immediately
  // instead of waiting for the next tick — the user just switched
  // back to this terminal and wants current numbers now.
  d.addEventListener('visibilitychange', () => { if (!d.hidden) poll(); });

  if (d.readyState === 'loading') {
    d.addEventListener('DOMContentLoaded', start);
  } else {
    start();
  }
})(window, document);
