/* ============================================================================
 * sw-update — applies a waiting service-worker update in place.
 *
 * Framework-free and dependency-free on purpose (same pattern as
 * async-guard.js):
 *   • Loads as a plain <script> under CSP `script-src 'self'` (browser
 *     global `window.SwUpdate`).
 *   • `require`-able by `node --test` (CommonJS `module.exports`).
 *
 * Bug this fixes (Task #61): "Update ready" kept reappearing right after the
 * user clicked "Reload now" / "Refresh UI". The old flow called
 * hardReloadUI(), which unregisters the service worker and wipes every cache
 * before navigating — forcing a brand-new install on the next load. That
 * fresh install briefly has `reg.waiting` truthy again (a normal but
 * transient SW lifecycle state), which registerServiceWorker() misread as a
 * genuinely new update and re-showed the banner immediately.
 *
 * Fix: activate the *already-installed* waiting worker in place —
 * postMessage SKIP_WAITING to it, wait for it to actually take control
 * (`controllerchange`), and only then reload. The reloaded page is served by
 * the new SW from its very first request, and no fresh install/registration
 * cycle happens, so the banner has nothing to spuriously re-trigger on.
 * ========================================================================== */
(function (root) {
  'use strict';

  /**
   * Applies a pending SW update: messages the waiting worker to skipWaiting(),
   * waits for controllerchange, then reloads exactly once.
   *
   * @param {object} opts
   * @param {ServiceWorkerRegistration|null} opts.reg  the current registration
   * @param {ServiceWorkerContainer} opts.container     navigator.serviceWorker
   * @param {() => void} opts.reload                    called at most once
   * @param {(fn: () => void, ms: number) => any} [opts.setTimeout] injectable for tests
   * @returns {Promise<void>}
   */
  async function applyUpdate({ reg, container, reload, setTimeout: setTimeoutFn }) {
    const scheduleTimeout = setTimeoutFn || (typeof setTimeout !== 'undefined' ? setTimeout : null);
    let waiting = reg && reg.waiting;

    if (!waiting && reg && typeof reg.update === 'function') {
      // The banner can be triggered by the server buildTag poll before the
      // browser has fetched the new sw.js at all. With no waiting worker, a
      // bare reload would be served the OLD cache-first shell, the new SW
      // would then install in the background, and the banner would reappear
      // — the "update available keeps showing" loop. Fetch the update now
      // and wait (bounded) for it to reach `installed` so a single click
      // activates the new version.
      try { await reg.update(); } catch { /* offline / fetch failed — fall through */ }
      waiting = reg.waiting || (await waitForInstalled(reg, scheduleTimeout, 8000));
    }

    if (!waiting) {
      // Nothing to activate (e.g. banner was shown from a broadcast message
      // rather than an actual waiting worker) — just reload.
      reload();
      return;
    }

    let reloaded = false;
    const reloadOnce = () => {
      if (reloaded) return;
      reloaded = true;
      reload();
    };

    container.addEventListener('controllerchange', reloadOnce, { once: true });
    // Safety net in case controllerchange never fires (e.g. no controller yet).
    if (scheduleTimeout) scheduleTimeout(reloadOnce, 3000);

    waiting.postMessage({ type: 'SKIP_WAITING' });
  }

  /**
   * Waits for reg.installing to reach the `installed` state (at which point
   * it becomes reg.waiting), bounded by a timeout. Resolves with the waiting
   * worker or null.
   */
  function waitForInstalled(reg, scheduleTimeout, ms) {
    return new Promise((resolve) => {
      const sw = reg.installing;
      if (!sw || typeof sw.addEventListener !== 'function') { resolve(null); return; }
      let settled = false;
      const settle = (v) => { if (!settled) { settled = true; resolve(v); } };
      sw.addEventListener('statechange', () => {
        if (sw.state === 'installed') settle(reg.waiting || sw);
        else if (sw.state === 'redundant') settle(null);
      });
      if (scheduleTimeout) scheduleTimeout(() => settle(reg.waiting || null), ms);
    });
  }

  const SwUpdate = { applyUpdate };

  if (typeof module !== 'undefined' && module.exports) {
    module.exports = SwUpdate;
  } else {
    root.SwUpdate = SwUpdate;
  }
})(typeof globalThis !== 'undefined' ? globalThis : this);
