// Service worker for the admin SPA. Strategy is network-first with offline
// fallback (the server sends Cache-Control: no-cache + ETag, so 304s stay fast).
// Cache name is bumped on each release that must invalidate stale client caches:
//   v2 - first network-first version (replaced a cache-first SW that shipped stale JS)
//   v3 - force returning clients to drop the old bucket so the "Add user" admin
//        button (and any client still on a pre-v2 cache-first SW) lands.
//   v4 - stop intercepting media/content/player + range requests. The old handler
//        clone+cache+respond'd every non-API request; a video Range request gets a
//        206 (uncacheable) which broke the handler ("ServiceWorker encountered an
//        unexpected error"), so videos never loaded on pages this SW controls
//        (e.g. the web player, since this SW's scope is '/').
// Changing this string is what makes the browser detect a new SW + run activate,
// which deletes every cache key != CACHE below.
const CACHE = 'rd-admin-v4';

self.addEventListener('install', e => {
  e.waitUntil(caches.open(CACHE).then(c => c.addAll([
    '/', '/index.html', '/css/variables.css', '/css/reset.css', '/css/main.css',
    '/js/app.js', '/js/api.js', '/js/socket.js', '/js/i18n.js',
    '/js/components/toast.js'
  ])));
  self.skipWaiting();
});

self.addEventListener('activate', e => {
  e.waitUntil(caches.keys().then(keys => Promise.all(keys.filter(k => k !== CACHE).map(k => caches.delete(k)))));
  self.clients.claim();
});

self.addEventListener('fetch', e => {
  const req = e.request;
  // Only handle same-origin GET navigations/assets. Everything else hits the
  // network unmediated:
  //  - non-GET: cache.put() rejects on them anyway.
  //  - Range requests (video seeking): the response is 206 Partial Content, which
  //    is uncacheable and breaks clone+cache+respond -> "ServiceWorker encountered
  //    an unexpected error", stalling video playback.
  //  - /uploads/ (content/media), /player (the web player), /api/, /socket.io/:
  //    not ours to cache; the player + server set their own cache headers.
  if (req.method !== 'GET' || req.headers.has('range')) return;
  const url = req.url;
  if (url.includes('/api/') || url.includes('/socket.io/') ||
      url.includes('/uploads/') || url.includes('/player')) return;

  // Network-first: respect the server's Cache-Control: no-cache + ETag (304s stay
  // fast); fall back to cache only when offline. Only cache full, same-origin 200s.
  e.respondWith(
    fetch(req)
      .then(resp => {
        if (resp && resp.status === 200 && resp.type === 'basic') {
          const copy = resp.clone();
          caches.open(CACHE).then(c => c.put(req, copy)).catch(() => {});
        }
        return resp;
      })
      .catch(() => caches.match(req))
  );
});
