/* ============================================================================
 * sw.js — YT Player Service Worker
 *
 * Strategy:
 *   App shell (HTML/CSS/JS)    → cache-first, versioned cache
 *   /api/* requests            → network-only (never cache yt-dlp results)
 *   /api/download/*            → network-only (streamed binary, never cache)
 *   YouTube thumbnails (i.ytimg.com) → stale-while-revalidate
 *   Everything else            → network, fallback to cache
 *
 * Auto-update flow:
 *   1. New SW installs alongside the old one.
 *   2. activate: broadcast SW_UPDATE_AVAILABLE to all clients.
 *   3. Client shows "Update ready" banner.
 *   4. User clicks → client sends { type: 'SKIP_WAITING' }.
 *   5. SW calls skipWaiting() → takes over → client reloads.
 * ========================================================================== */

const VERSION = 'v1.0.1';   // ← bump this on every deploy to bust the cache
const CACHE   = 'ytplayer-' + VERSION;

// Files that form the installable app shell.
const SHELL = [
  '/',
  '/index.html',
  '/styles.css',
  '/async-guard.js',
  '/fingerprint.js',
  '/opfs.js',
  '/app.js',
  '/manifest.webmanifest',
  '/icons/icon-192.png',
  '/icons/icon-512.png',
];

// ---- Install: pre-cache the app shell ----
self.addEventListener('install', (e) => {
  e.waitUntil(
    caches.open(CACHE).then((cache) => cache.addAll(SHELL))
    // skipWaiting() is NOT called here — we wait for the client to confirm
    // before activating, so the update banner can appear first.
  );
});

// ---- Activate: evict old caches, claim clients, notify about update ----
self.addEventListener('activate', (e) => {
  e.waitUntil((async () => {
    // Delete every cache that isn't the current version
    const keys = await caches.keys();
    await Promise.all(
      keys.filter((k) => k !== CACHE).map((k) => caches.delete(k))
    );
    // Claim all open clients immediately (new installs)
    await self.clients.claim();
    // Broadcast to every open window so the app can show an update banner
    const all = await self.clients.matchAll({ type: 'window', includeUncontrolled: true });
    all.forEach((c) => c.postMessage({ type: 'SW_UPDATE_AVAILABLE', version: VERSION }));
  })());
});

// ---- Fetch: routing logic ----
self.addEventListener('fetch', (e) => {
  const { request } = e;
  const url = new URL(request.url);

  // Only intercept GET/HEAD — let POST (sync endpoint) go through unmodified
  if (request.method !== 'GET' && request.method !== 'HEAD') return;

  // API calls and binary downloads → network only, no caching
  if (url.pathname.startsWith('/api/')) {
    e.respondWith(fetch(request));
    return;
  }

  // YouTube thumbnails → stale-while-revalidate (fast load, fresh in background)
  if (url.hostname === 'i.ytimg.com') {
    e.respondWith(staleWhileRevalidate(request, 'ytplayer-thumbs'));
    return;
  }

  // Google Fonts CSS — stale-while-revalidate so offline doesn't break type
  if (url.hostname === 'fonts.googleapis.com' || url.hostname === 'fonts.gstatic.com') {
    e.respondWith(staleWhileRevalidate(request, 'ytplayer-fonts'));
    return;
  }

  // App shell → cache-first, then network, then generic offline fallback
  e.respondWith(cacheFirst(request));
});

// ---- Message: handle SKIP_WAITING from the client ----
self.addEventListener('message', (e) => {
  if (e.data && e.data.type === 'SKIP_WAITING') {
    self.skipWaiting();
  }
});

// ============================================================================
// Fetch helpers
// ============================================================================

// Cache-first: serve from cache; if missing, fetch, cache, return.
async function cacheFirst(request) {
  const cache  = await caches.open(CACHE);
  const cached = await cache.match(request);
  if (cached) return cached;
  try {
    const response = await fetch(request);
    // Only cache successful, non-opaque responses
    if (response && response.status === 200 && response.type !== 'opaque') {
      cache.put(request, response.clone());
    }
    return response;
  } catch {
    // Network failed and nothing in cache — return a minimal offline page
    // for navigation requests; let sub-resources fail naturally.
    if (request.mode === 'navigate') {
      const nav = await cache.match('/index.html');
      if (nav) return nav;
    }
    return new Response('Offline', { status: 503, statusText: 'Service Unavailable' });
  }
}

// Stale-while-revalidate: return cached immediately, update in background.
async function staleWhileRevalidate(request, cacheName) {
  const cache  = await caches.open(cacheName);
  const cached = await cache.match(request);
  // Start a background revalidation — don't await it before responding
  const networkFetch = fetch(request).then((r) => {
    if (r && r.status === 200) cache.put(request, r.clone());
    return r;
  }).catch(() => null);

  return cached || networkFetch;
}
