// Pete's service worker: an installable-PWA shell, an offline reader, and the
// Web Push receiver. Served from the root (/sw.js) so its scope is the whole
// origin — it can intercept navigations and /api/article the same as any page.
//
// Bump CACHE_VERSION whenever the precached shell assets change; activate()
// drops every cache that doesn't match the current version.
var CACHE_VERSION = "v5";
var SHELL_CACHE = "pete-shell-" + CACHE_VERSION;
var RUNTIME_CACHE = "pete-runtime-" + CACHE_VERSION;

// App shell: the static assets every page needs. Versioned by URL content only
// loosely, so we lean on network-first for HTML and cache-first for these.
var SHELL_ASSETS = [
  "/static/css/output.css",
  "/static/js/prefs.js",
  "/static/js/weather-2d.js",
  "/static/js/weather-gl.js",
  "/static/js/weather.js",
  "/static/js/weather-forecast.js",
  "/static/js/search.js",
  "/static/js/settings.js",
  "/static/js/reader.js",
  "/static/js/pwa.js",
  "/static/img/pete.avif",
  "/static/img/icon-192.png",
  "/static/img/icon-512.png",
];

// How many visited-article responses to keep for offline reading before the
// oldest are evicted. Reader articles are small JSON blobs.
var RUNTIME_MAX = 60;

self.addEventListener("install", function (event) {
  event.waitUntil(
    caches.open(SHELL_CACHE).then(function (cache) {
      // addAll is atomic-ish: if one asset 404s the whole install fails, so keep
      // this list to assets we know are served. Individual failures are tolerated
      // by falling back to per-asset puts.
      return Promise.all(
        SHELL_ASSETS.map(function (url) {
          return cache.add(url).catch(function () {});
        })
      );
    }).then(function () {
      return self.skipWaiting();
    })
  );
});

self.addEventListener("activate", function (event) {
  event.waitUntil(
    caches.keys().then(function (keys) {
      return Promise.all(
        keys.map(function (k) {
          if (k !== SHELL_CACHE && k !== RUNTIME_CACHE) return caches.delete(k);
        })
      );
    }).then(function () {
      return self.clients.claim();
    })
  );
});

// trimCache evicts the oldest entries once a runtime cache passes its cap.
function trimCache(cacheName, max) {
  caches.open(cacheName).then(function (cache) {
    cache.keys().then(function (keys) {
      if (keys.length <= max) return;
      for (var i = 0; i < keys.length - max; i++) cache.delete(keys[i]);
    });
  });
}

// A minimal offline page for navigations we have nothing cached for.
function offlineFallback() {
  return new Response(
    "<!doctype html><meta charset=utf-8><meta name=viewport content='width=device-width,initial-scale=1'>" +
      "<title>Offline · Pete</title>" +
      "<div style=\"font-family:system-ui,sans-serif;max-width:32rem;margin:20vh auto;padding:0 1.5rem;text-align:center;color:#3a2f1a\">" +
      "<div style=font-size:3rem>🦆</div>" +
      "<h1 style=font-size:1.4rem>You're offline</h1>" +
      "<p style=opacity:.7>Pete can't reach the news right now. Articles you've already opened are still readable, so head back and try one of those.</p>" +
      "</div>",
    { headers: { "Content-Type": "text/html; charset=utf-8" }, status: 503 }
  );
}

self.addEventListener("fetch", function (event) {
  var req = event.request;
  if (req.method !== "GET") return;

  var url = new URL(req.url);
  if (url.origin !== self.location.origin) return; // never touch cross-origin

  // Visited articles: network-first so text stays fresh, but cache every success
  // so the reader still works offline for stories the user has already opened.
  if (url.pathname === "/api/article") {
    event.respondWith(
      fetch(req).then(function (res) {
        if (res && res.ok) {
          var copy = res.clone();
          caches.open(RUNTIME_CACHE).then(function (cache) {
            cache.put(req, copy);
            trimCache(RUNTIME_CACHE, RUNTIME_MAX);
          });
        }
        return res;
      }).catch(function () {
        return caches.match(req).then(function (hit) {
          return hit || new Response(JSON.stringify({ error: "offline" }), {
            status: 503,
            headers: { "Content-Type": "application/json" },
          });
        });
      })
    );
    return;
  }

  // Static assets: network-first. Our asset URLs are NOT content-hashed
  // (/static/js/weather-gl.js stays the same URL across deploys), so a
  // cache-first strategy would pin whatever bytes were first cached and never
  // notice a redeployed file changed — leaving stale JS/CSS in place until the
  // CACHE_VERSION bump below. Go's FileServer sets ETag/Last-Modified, so an
  // online refetch is a cheap 304 when nothing changed. We still fill the cache
  // on every success and fall back to it when the network is unreachable, so
  // offline reading keeps the shell it needs.
  if (url.pathname.indexOf("/static/") === 0) {
    event.respondWith(
      fetch(req).then(function (res) {
        if (res && res.ok) {
          var copy = res.clone();
          caches.open(SHELL_CACHE).then(function (cache) { cache.put(req, copy); });
        }
        return res;
      }).catch(function () {
        return caches.match(req);
      })
    );
    return;
  }

  // Page navigations: network-only, falling back to the offline card when the
  // network is unreachable. We deliberately do NOT cache HTML responses: pages
  // are personalized (they embed the signed-in user's name/email and a "For you"
  // rail), and the runtime cache is shared across everyone who uses this
  // installed PWA. Caching a navigation would let a signed-out visitor — or a
  // second person on the same device — be served the previous user's identity
  // and personalized stories offline. Offline reading still works: the reader
  // fetches cached /api/article JSON on top of the cached static shell.
  if (req.mode === "navigate") {
    event.respondWith(
      fetch(req).catch(function () {
        return offlineFallback();
      })
    );
    return;
  }

  // Everything else (other /api/* calls): straight to the network. These are
  // personalized/stateful and must not be served stale.
});

// ---- Web Push -------------------------------------------------------------
// The server sends a JSON payload {title, body, url, tag}. Missing fields fall
// back to sensible defaults so a malformed push still shows something useful.
self.addEventListener("push", function (event) {
  var data = {};
  if (event.data) {
    try { data = event.data.json(); } catch (e) { data = { body: event.data.text() }; }
  }
  var title = data.title || "Pete";
  var options = {
    body: data.body || "New stories are waiting.",
    icon: "/static/img/icon-192.png",
    badge: "/static/img/icon-192.png",
    tag: data.tag || "pete-digest",
    renotify: true,
    data: { url: data.url || "/" },
  };
  event.waitUntil(self.registration.showNotification(title, options));
});

self.addEventListener("notificationclick", function (event) {
  event.notification.close();
  var target = (event.notification.data && event.notification.data.url) || "/";
  event.waitUntil(
    self.clients.matchAll({ type: "window", includeUncontrolled: true }).then(function (clients) {
      for (var i = 0; i < clients.length; i++) {
        var c = clients[i];
        // Focus an existing Pete tab and route it to the target if we can.
        if ("focus" in c) {
          c.focus();
          if ("navigate" in c && target !== "/") { try { c.navigate(target); } catch (e) {} }
          return;
        }
      }
      if (self.clients.openWindow) return self.clients.openWindow(target);
    })
  );
});
