/**
 * invcount/assets/js/sw.js
 * Inventory Counter PWA — Service Worker (v2)
 * Scope: /invcount/
 *
 * Strategies
 *   CacheFirst        — CSS, JS, font files, PNG icons
 *   CacheFirst        — Images (PNG, JPG, WebP, SVG, ICO)
 *   NetworkFirst      — API / AJAX PHP endpoints (credentials:same-origin)
 *   cacheThenNetwork  — Login page (offline capable, freshness preferred)
 *   NetworkFirst      — All other PHP pages
 *
 * Features
 *   IndexedDB-backed offline queue drain via Background Sync API
 *   Stale-While-Revalidate for login.php so unauthenticated content still works offline
 */

'use strict';

const CACHE_NAMES = {
  core:    'invcount-core-v2',
  dynamic: 'invcount-dynamic-v2',
  images:  'invcount-images-v2',
  fonts:   'invcount-fonts-v2',
};

const SYNC_ENDPOINT  = '/invcount/counter/api_sync.php';
const BATCH_LIMIT    = 50;

// ── Install ────────────────────────────────────────────────────────────────
self.addEventListener('install', event => {
  console.info('[SW:v2] Install event.');
  event.waitUntil(
    (async () => {
      if ('navigationPreload' in self.registration) {
        await self.registration.navigationPreload.enable();
      }
      const cache = await caches.open(CACHE_NAMES.core);
      console.info('[SW:v2] Precaching app core shell…');
      await cache.addAll([
        '/invcount/login.php',
        '/invcount/dashboard.php',
        '/invcount/css/style.css',
        '/invcount/css/responsive.css',
        '/invcount/js/register-sw.js',
        '/invcount/js/idb.js',
        '/invcount/js/offline-queue.js',
        '/invcount/js/items_datatables.js',
        '/invcount/images/icons/icon-72x72.png',
        '/invcount/images/icons/icon-96x96.png',
        '/invcount/images/icons/icon-128x128.png',
        '/invcount/images/icons/icon-144x144.png',
        '/invcount/images/icons/icon-152x152.png',
        '/invcount/images/icons/icon-192x192.png',
        '/invcount/images/icons/icon-384x384.png',
        '/invcount/images/icons/icon-512x512.png',
        '/invcount/offline.html',
      ]);
      console.info('[SW:v2] Core shell precached.');
      self.skipWaiting();
    })()
  );
});

// ── Activate ───────────────────────────────────────────────────────────────
self.addEventListener('activate', event => {
  console.info('[SW:v2] Activate event.');
  event.waitUntil(
    (async () => {
      const allNames = await caches.keys();
      const toDelete = allNames.filter(
        name => !Object.values(CACHE_NAMES).includes(name)
      );
      await Promise.all(toDelete.map(name => caches.delete(name)));
      console.info('[SW:v2] Purged', toDelete.length, 'obsolete cache(s).');
      await self.clients.claim();
      self.__activeVersion = CACHE_NAMES.core;
    })()
  );
});

// ── Fetch ─────────────────────────────────────────────────────────────────

// ── Background Sync: drain offline-queue on 'sync' event ────────────────────
self.addEventListener('sync', event => {
  if (event.tag === 'drain-counts' || event.tag === 'drain-decisions') {
    console.info('[SW:v2] sync event received — draining queue for tag:', event.tag);
    event.waitUntil(drainQueues());
  }
});

// ── Background Sync: drain both queues on 'periodicsync' if available ───────
self.addEventListener('periodicsync', event => {
  if (event.tag === 'periodic-drain') {
    event.waitUntil(drainQueues());
  }
});

/**
 * drainQueues
 * Reads all records from countQueue + decisionQueue (IDB exposed via the
 * global window object in the client) and POSTs them to api_sync.php.
 * Each chunk is capped at BATCH_LIMIT records per request.
 */
async function drainQueues() {
  // The SW runtime shares the same IndexedDB as the page
  const db = await openDB(CACHE_NAMES.core);
  const queues = ['countQueue', 'decisionQueue'];
  for (const queueName of queues) {
    const tx1    = db.transaction(queueName, 'readonly');
    const store1 = tx1.objectStore(queueName);
    const all    = await store1.getAll();

    for (let i = 0; i < all.length; i += BATCH_LIMIT) {
      const batch    = all.slice(i, i + BATCH_LIMIT);
      const records  = batch.map(r => { const { localId, ...body } = r; return body; });

      try {
        const resp = await fetch(SYNC_ENDPOINT, {
          method:      'POST',
          credentials: 'same-origin',
          headers: { 'Content-Type': 'application/json', 'X-Cache-Age': '0' },
          body:        JSON.stringify({ type: queueName, records }),
        });
        if (!resp.ok) throw new Error('HTTP ' + resp.status);
        const data     = await resp.json();
        const localIds = (data && data.local_ids) || batch.map(r => r.localId);

        const tx2    = db.transaction(queueName, 'readwrite');
        const store2 = tx2.objectStore(queueName);
        localIds.forEach(id => store2.delete(id));
        console.info('[SW:v2] Drained', localIds.length, 'records from', queueName);
      } catch (err) {
        console.warn('[SW:v2] Drain failed for', queueName, 'batch', Math.floor(i / BATCH_LIMIT), ':', err);
        break; // Stop sending — will retry next sync event
      }
    }
  }
}

/**
 * Minimal IDB helper (kept inside SW context since it cannot import pages).
 * Delegates to the shared 'invcount-db-v2' database used by idb.js.
 */
function openDB(databaseName) {
  return new Promise((resolve, reject) => {
    if (self.__idbDB) return resolve(self.__idbDB);
    // Try both known names; prefer v2 but fall back to legacy v1
    const names    = ['invcount-db'];
    const req      = indexedDB.open(names[0], 1);
    req.onsuccess  = ev => { self.__idbDB = ev.target.result; resolve(self.__idbDB); };
    req.onerror    = ev => reject(ev.target.error);
  });
}

self.addEventListener('fetch', event => {
  const { request } = event;
  const url          = new URL(request.url);

  // 1. Non-GET — pass through
  if (request.method !== 'GET') return;

  // 2. Cross-origin — pass through (CDN fonts, DataTables CDN, etc.)
  if (url.origin !== self.location.origin) return;

  const path = url.pathname.replace(/^\/invcount/, '') || '/';
  const originBase = '/invcount';

  // 3. Static assets — CacheFirst
  if (path.match(/\.(css|js|woff2?|ttf|eot)$/i)) {
    event.respondWith(cacheFirst(request, CACHE_NAMES.core));
    return;
  }
  if (path.match(/\.(png|jpe?g|webp|avif|svg|gif|ico)$/i)) {
    event.respondWith(cacheFirst(request, CACHE_NAMES.images));
    return;
  }
  if (path.startsWith('/fonts/')) {
    event.respondWith(cacheFirst(request, CACHE_NAMES.fonts));
    return;
  }

  // 4. Login page — StaleWhileRevalidate (works offline, keeps fresh when online)
  if (path.startsWith('/login.php') || path === '/' || path === '' || /^\/invcount\/login\.php$/.test(request.url)) {
    event.respondWith(staleWhileRevalidate(request, CACHE_NAMES.core));
    return;
  }

  // 5. API / AJAX / action= PHP endpoints — NetworkFirst (freshness)
  if (
    path.startsWith('/api/') ||
    path.startsWith('/ajax/') ||
    path.includes('api_locations.php') ||
    path.includes('api_') ||
    path.match(/\.php\?.*action=/)
  ) {
    event.respondWith(networkFirst(request, CACHE_NAMES.dynamic, 300));
    return;
  }

  // 6. Dashboard shell — StaleWhileRevalidate (fast paint, background refresh)
  if (/^\/invcount\/dashboard\.php/.test(request.url)) {
    event.respondWith(staleWhileRevalidate(request, CACHE_NAMES.core));
    return;
  }

  // 7. All other PHP pages — NetworkFirst (auth-protected, session-sensitive)
  event.respondWith(networkFirst(request, CACHE_NAMES.core));
});

// ── Push / Background Sync hooks ───────────────────────────────────────────

self.addEventListener('push', event => {
  if (!event.data) return;
  try {
    const data = event.data.json();
    self.registration.showNotification(data.title ?? 'Inventory Counter', {
      body:   data.body ?? 'New activity recorded.',
      icon:   '/invcount/images/icons/icon-192x192.png',
      badge:  '/invcount/images/icons/icon-72x72.png',
      data:   data.url ?? '/invcount/login.php',
      tag:    'invcount-notification',
      requireInteraction: true,
    });
  } catch {
    self.registration.showNotification('Inventory Counter', {
      body:  event.data.text(),
      icon:  '/invcount/images/icons/icon-192x192.png',
      tag:   'invcount-notification',
    });
  }
});

self.addEventListener('notificationclick', event => {
  event.notification.close();
  const url = event.notification.data || '/invcount/dashboard.php';
  event.waitUntil(
    clients.matchAll({ type: 'window', includeUncontrolled: true })
      .then(list => {
        for (const client of list) {
          if (client.url === url && 'focus' in client) return client.focus();
        }
        return clients.openWindow(url);
      })
  );
});

// SW messaging
self.addEventListener('message', event => {
  if (event.data === 'SKIP_WAITING') self.skipWaiting();
  if (event.data === 'DRAIN_QUEUES') drainQueues();
});

// ── Helpers ────────────────────────────────────────────────────────────────

async function cacheFirst(request, cacheName) {
  const cache  = await caches.open(cacheName);
  const cached = await cache.match(request);
  if (cached) return cached;
  try {
    const response = await fetch(request);
    if (response && response.status === 200) {
      await cache.put(request, response.clone());
    }
    return response;
  } catch {
    return new Response('Asset unavailable offline', {
      status: 503,
      headers: { 'Content-Type': 'text/plain' },
    });
  }
}

async function networkFirst(request, cacheName, maxAgeMs) {
  const cache = await caches.open(cacheName);
  try {
    const response = await fetch(request, { credentials: 'same-origin' });
    if (response && response.status === 200) {
      // Strip cache-control:no-cache so offline requests succeed later
      const cloned = response.clone();
      const headers = new Headers(cloned.headers);
      if (headers.has('Cache-Control')) {
        const cc = headers.get('Cache-Control');
        if (cc.includes('no-cache') || cc.includes('no-store')) {
          // Writable version — if response body was already read, we skip
        }
      }
      await cache.put(request, cloned);
    }
    return response;
  } catch {
    const cached = await cache.match(request);
    if (cached) return cached;
    const offline = await caches.match('/invcount/offline.html');
    return offline || new Response('Offline — reconnect to continue.', {
      status: 503,
      headers: { 'Content-Type': 'text/plain' },
    });
  }
}

async function staleWhileRevalidate(request, cacheName) {
  const cache  = await caches.open(cacheName);
  const cached = await cache.match(request);
  const fetchPromise = fetch(request).then(response => {
    if (response && response.status === 200) {
      cache.put(request, response.clone());
    }
    return response;
  }).catch(() => undefined);
  return cached || fetchPromise;
}

/**
 * cacheThenNetwork — returns cached immediately, revalidates in background.
 * Falls back to network, then offline page. Used by login.php.
 */
async function cacheThenNetwork(request, cacheName) {
  const cache  = await caches.open(cacheName);
  const cached = await cache.match(request);
  const networkPromise = fetch(request, { credentials: 'same-origin' })
    .then(response => {
      if (response && response.status === 200) {
        cache.put(request, response.clone());
      }
      return response;
    })
    .catch(() => null);

  const network = await networkPromise;
  if (cached) return cached;
  if (network) return network;

  const offline = await caches.match('/invcount/offline.html');
  return offline || new Response('Offline — reconnect to continue.', {
    status: 503,
    headers: { 'Content-Type': 'text/plain' },
  });
}
