// Service worker — app-shell caching + push notifications
// Caching strategy:
//   Hashed assets (/assets/*-AbCd12.js) → cache-first (immutable)
//   Navigation (HTML)                   → network-first (cache = offline fallback only)
//   Static assets / JS / CSS modules    → network-first (cache = offline fallback only)
//   API, WebSocket, Vite internals      → network-only (no cache)
// Network-first on navigations + modules so a live-edit always shows the current app and its
// build errors, never a stale cached shell. Cache is a pure offline fallback. (Mirror of the
// supervisor's SW_JS in supervisor/index.ts — keep in sync.)

const CACHE = 'bloby-v25';

// Precache the HTML shell on install so the cache is never empty.
// Without this, the first navigation isn't intercepted (SW wasn't
// controlling yet), so refresh would find an empty cache → white screen.
self.addEventListener('install', (e) => {
  e.waitUntil(
    caches.open(CACHE)
      .then(c => c.add('/'))
      .then(() => self.skipWaiting())
      .catch(err => { console.error('[SW] install failed:', err); throw err; })
  );
});

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

self.addEventListener('message', (e) => {
  if (e.data?.type === 'SKIP_WAITING') self.skipWaiting();
});

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

  // ── Network-only: never cache these ──────────────────────────────
  if (
    request.method !== 'GET' ||
    url.pathname.startsWith('/api/') ||
    url.pathname.startsWith('/app/api/') ||
    url.pathname.endsWith('/ws') ||
    url.pathname === '/sw.js' ||
    url.pathname === '/bloby/sw.js' ||
    url.pathname.startsWith('/@') ||
    url.pathname.startsWith('/__')
  ) return;

  // ── Hashed assets (immutable, content-addressed) → cache-first ──
  if (/\/assets\/.+-[a-zA-Z0-9_-]{6,}\.(js|css)$/.test(url.pathname)) {
    event.respondWith(caches.open(CACHE).then(c =>
      c.match(request).then(hit =>
        hit || fetch(request).then(r => { if (r.ok) c.put(request, r.clone()); return r; })
      )
    ));
    return;
  }

  // ── Vite optimized deps (?v=<hash>, immutable) → cache-first ──
  // These fell into network-first, re-crossing the tunnel whenever the HTTP cache evicted
  // them. Cache-first + prune older ?v= versions. Source modules (?t=, /@*) stay network-only.
  if (url.pathname.indexOf('/node_modules/.vite/deps/') === 0 && url.searchParams.has('v')) {
    event.respondWith(caches.open(CACHE).then(c =>
      c.match(request).then(hit =>
        hit || fetch(request).then(r => {
          if (r.ok) {
            c.put(request, r.clone());
            c.keys().then(keys => {
              for (const k of keys) {
                const u = new URL(k.url);
                if (u.pathname === url.pathname && u.search !== url.search) c.delete(k);
              }
            });
          }
          return r;
        })
      )
    ));
    return;
  }

  // ── Navigation: only stale-while-revalidate the dashboard root (/) ──
  // /bloby/* is a separate app — let it go to network to avoid
  // caching the wrong HTML under the / key.
  if (request.mode === 'navigate') {
    if (url.pathname === '/' || url.pathname === '/index.html') {
      // Network-first: always fetch the live dashboard; cache only as offline fallback.
      event.respondWith(caches.open(CACHE).then(c =>
        fetch(request)
          .then(r => { if (r.ok) c.put('/', r.clone()); return r; })
          .catch(() => c.match('/'))
      ));
      return;
    }
    // Other navigations (/bloby/*, etc.) — network only
    return;
  }

  // ── Everything else → network-first, cache as offline fallback ───
  // Covers: JS/CSS modules, images, video, fonts, manifest, etc.
  event.respondWith(caches.open(CACHE).then(c =>
    fetch(request)
      .then(r => { if (r.ok) c.put(request, r.clone()); return r; })
      .catch(() => c.match(request))
  ));
});

// ── Push notifications ─────────────────────────────────────────────
self.addEventListener('push', (event) => {
  let data = { title: 'Bloby', body: 'New message' };
  try { data = event.data.json(); } catch {}

  event.waitUntil(
    self.registration.showNotification(data.title || 'Bloby', {
      body: data.body || '',
      icon: '/morphy-icon-192.png',
      badge: '/morphy-badge.png',
      vibrate: [100, 50, 100],
      tag: data.tag || 'bloby-default',
      data: { url: data.url || '/' },
    })
  );
});

// Notification click — focus or open app
self.addEventListener('notificationclick', (event) => {
  event.notification.close();
  const url = event.notification.data?.url || '/';

  event.waitUntil(
    self.clients.matchAll({ type: 'window', includeUncontrolled: true }).then((clients) => {
      for (const client of clients) {
        if (client.url.includes('/bloby') && 'focus' in client) return client.focus();
      }
      return self.clients.openWindow(url);
    })
  );
});
