/* Service Worker: lightweight caching for a static Astro site.
   - Navigations: network-first (avoid stale HTML indefinitely)
   - Site shell: pre-cache on install
   - Images: cache-first with bounded eviction
*/

const cacheVersionFromUrl = (() => {
  try {
    const v = new URL(self.location.href).searchParams.get("v");
    if (!v) return null;
    if (!/^[a-zA-Z0-9._-]{1,64}$/.test(v)) return null;
    return v;
  } catch {
    return null;
  }
})();

const CACHE_VERSION = cacheVersionFromUrl ? `v-${cacheVersionFromUrl}` : "v4";

const CACHE_SHELL = `shell-${CACHE_VERSION}`;
const CACHE_PAGES = `pages-${CACHE_VERSION}`;
const CACHE_MEDIA = `media-${CACHE_VERSION}`;

const SHELL_ASSETS = ["/", "/favicon.png", "/robots.txt"];

// Cache.addAll() throws if there are any duplicate requests.
const SHELL_ASSETS_UNIQUE = Array.from(new Set(SHELL_ASSETS));

// Keep media cache bounded so we don't grow indefinitely.
const MAX_MEDIA_ENTRIES = 80;

const isGet = (request) => request && request.method === "GET";

const isNavigationRequest = (request) =>
  request.mode === "navigate" || request.destination === "document";

const isImageRequest = (request, url) => {
  if (request.destination === "image") return true;
  const p = url.pathname.toLowerCase();
  return (
    p.endsWith(".png") ||
    p.endsWith(".jpg") ||
    p.endsWith(".jpeg") ||
    p.endsWith(".webp") ||
    p.endsWith(".gif") ||
    p.endsWith(".avif") ||
    p.endsWith(".svg")
  );
};

async function trimCache(cacheName, maxEntries) {
  const cache = await caches.open(cacheName);
  const keys = await cache.keys();
  const extra = keys.length - maxEntries;
  if (extra <= 0) return;
  // Cache keys are returned in insertion order in practice; delete the oldest.
  for (let i = 0; i < extra; i += 1) {
    await cache.delete(keys[i]);
  }
}

async function cachePutSafe(cacheName, request, response) {
  // Only cache successful or opaque responses. Avoid caching 404/500 HTML.
  if (!response) return;
  if (response.type !== "opaque" && !response.ok) return;
  const cache = await caches.open(cacheName);
  await cache.put(request, response);
}

self.addEventListener("install", (event) => {
  event.waitUntil(
    (async () => {
      const cache = await caches.open(CACHE_SHELL);
      await cache.addAll(SHELL_ASSETS_UNIQUE);
      // Activate new worker ASAP to pick up new caching rules.
      await self.skipWaiting();
    })(),
  );
});

self.addEventListener("activate", (event) => {
  event.waitUntil(
    (async () => {
      const keep = new Set([CACHE_SHELL, CACHE_PAGES, CACHE_MEDIA]);
      const keys = await caches.keys();
      await Promise.all(keys.map((k) => (keep.has(k) ? Promise.resolve() : caches.delete(k))));
      await self.clients.claim();
    })(),
  );
});

self.addEventListener("fetch", (event) => {
  const { request } = event;
  if (!isGet(request)) return;

  const url = new URL(request.url);

  // Only handle http(s).
  if (url.protocol !== "http:" && url.protocol !== "https:") return;

  // Network-first for navigations (HTML documents). Cache as fallback only.
  if (isNavigationRequest(request)) {
    event.respondWith(
      (async () => {
        try {
          const fresh = await fetch(request);
          // Cache a clone so we can serve it when offline.
          await cachePutSafe(CACHE_PAGES, request, fresh.clone());
          return fresh;
        } catch {
          const cached = await caches.match(request);
          if (cached) return cached;
          // Fallback: try cached homepage shell.
          const home = await caches.match("/");
          if (home) return home;
          throw new Error("No cached navigation fallback.");
        }
      })(),
    );
    return;
  }

  // Cache-first for images/media with bounded cache size.
  if (isImageRequest(request, url)) {
    event.respondWith(
      (async () => {
        const cached = await caches.match(request);
        if (cached) return cached;

        const res = await fetch(request);
        await cachePutSafe(CACHE_MEDIA, request, res.clone());
        await trimCache(CACHE_MEDIA, MAX_MEDIA_ENTRIES);
        return res;
      })(),
    );
    return;
  }

  // Stale-while-revalidate for styles/scripts/fonts from same-origin.
  if (
    url.origin === self.location.origin &&
    (request.destination === "style" ||
      request.destination === "script" ||
      request.destination === "font")
  ) {
    event.respondWith(
      (async () => {
        const cached = await caches.match(request);
        const networkPromise = fetch(request)
          .then(async (res) => {
            await cachePutSafe(CACHE_SHELL, request, res.clone());
            return res;
          })
          .catch(() => null);

        return cached || (await networkPromise) || fetch(request);
      })(),
    );
  }
});

