/// <reference lib="webworker" />
/// <reference types="@serwist/next/typings" />

import {
  Serwist,
  NetworkFirst,
  CacheFirst,
  ExpirationPlugin,
  type RuntimeCaching,
} from 'serwist';

declare const self: ServiceWorkerGlobalScope & {
  __SW_MANIFEST: (string | { url: string; revision: string | null })[];
};

/**
 * VYNDR service worker (Session 27 — deployment-aware rewrite).
 *
 * The PWA stays — it powers push, offline, fast assets, and installs.
 * The bug was the CACHE POLICY, not the SW. Sports data is time-
 * sensitive: a 4-hour-old score is wrong, not stale. So pages + API +
 * everything-dynamic are NetworkFirst (always fresh, cache only as an
 * offline fallback). Only content-hashed static assets (which change
 * URL every build) are CacheFirst.
 *
 * skipWaiting + clientsClaim mean a new build takes over on the very
 * next navigation — no "close all tabs and hard-refresh" ritual.
 */

const OFFLINE_URL = '/offline';

// The complete set of caches this SW version owns. The activate handler
// below deletes anything else (the old defaultCache buckets:
// start-url, next-data, apis, pages-rsc, static-js-assets, …) so a
// returning user isn't served by a stale bucket from a prior version.
const CURRENT_CACHES = [
  'pages',
  'api-responses',
  'next-static',
  'static-media',
  'fallback',
  'offline-fallback',
];

// Navigation NetworkFirst, with a last-resort offline page when both the
// network AND the runtime cache miss (e.g. first visit to a never-cached
// route while offline).
const offlineFallbackPlugin = {
  handlerDidError: async () => (await caches.match(OFFLINE_URL)) || Response.error(),
};

const runtimeCaching: RuntimeCaching[] = [
  // API responses — ALWAYS network-first. Schedule, game lines, odds,
  // streaks must be fresh; the cache is only an offline courtesy.
  {
    matcher: ({ url, sameOrigin }) => sameOrigin && url.pathname.startsWith('/api/'),
    handler: new NetworkFirst({
      cacheName: 'api-responses',
      networkTimeoutSeconds: 5,
      plugins: [new ExpirationPlugin({ maxEntries: 100, maxAgeSeconds: 60 * 60 })],
    }),
  },
  // HTML navigations — network-first so scores/copy are never stale.
  {
    matcher: ({ request }) => request.mode === 'navigate',
    handler: new NetworkFirst({
      cacheName: 'pages',
      networkTimeoutSeconds: 5,
      plugins: [
        new ExpirationPlugin({ maxEntries: 50, maxAgeSeconds: 24 * 60 * 60 }),
        offlineFallbackPlugin,
      ],
    }),
  },
  // Next.js content-hashed static JS/CSS — cache-first is safe AND fast:
  // a new build changes the URL, so old URLs are simply never requested.
  {
    matcher: ({ url, sameOrigin }) => sameOrigin && url.pathname.startsWith('/_next/static/'),
    handler: new CacheFirst({
      cacheName: 'next-static',
      plugins: [new ExpirationPlugin({ maxEntries: 200, maxAgeSeconds: 30 * 24 * 60 * 60 })],
    }),
  },
  // Images + fonts — cache-first (slow to change, expensive to refetch).
  {
    matcher: ({ url }) =>
      url.pathname.startsWith('/images/') ||
      url.pathname.startsWith('/icons/') ||
      /\.(?:png|jpe?g|gif|svg|webp|ico|woff2?)$/.test(url.pathname),
    handler: new CacheFirst({
      cacheName: 'static-media',
      plugins: [new ExpirationPlugin({ maxEntries: 100, maxAgeSeconds: 30 * 24 * 60 * 60 })],
    }),
  },
  // Everything else (incl. RSC / _next/data payloads) — network-first so
  // dynamic content stays fresh; cache is offline insurance only.
  {
    matcher: () => true,
    handler: new NetworkFirst({
      cacheName: 'fallback',
      networkTimeoutSeconds: 5,
      plugins: [new ExpirationPlugin({ maxEntries: 50, maxAgeSeconds: 24 * 60 * 60 })],
    }),
  },
];

const serwist = new Serwist({
  precacheEntries: self.__SW_MANIFEST,
  skipWaiting: true,
  clientsClaim: true,
  navigationPreload: true,
  runtimeCaching,
});

serwist.addEventListeners();

// Pre-cache the offline page so the navigation fallback always has it.
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open('offline-fallback').then((cache) => cache.add(OFFLINE_URL)).catch(() => {}),
  );
});

// On activation, delete cache buckets from prior SW versions. We keep our
// CURRENT_CACHES and anything Serwist manages (its precache cache is
// prefixed `serwist`), and drop the rest — clearing the legacy buckets a
// previous deploy left behind so users never get served from them.
self.addEventListener('activate', (event) => {
  event.waitUntil(
    caches.keys().then((names) =>
      Promise.all(
        names
          .filter((name) => !CURRENT_CACHES.includes(name) && !name.startsWith('serwist'))
          .map((name) => {
            console.log('[SW] deleting stale cache:', name);
            return caches.delete(name);
          }),
      ),
    ),
  );
});

// ---- Web Push (Session 27 keeps the existing handlers) ----
// Pushes are emitted server-side by src/services/distribution/webPush.js.
self.addEventListener('push', (event) => {
  if (!event.data) return;
  let payload: { title?: string; body?: string; icon?: string; url?: string; tag?: string };
  try {
    payload = event.data.json();
  } catch {
    payload = { title: 'VYNDR', body: event.data.text() };
  }
  const {
    title = 'VYNDR',
    body = '',
    icon = '/icons/icon-192.png',
    url = '/',
    tag = 'vyndr-notification',
  } = payload;
  event.waitUntil(
    self.registration.showNotification(title, {
      body,
      icon,
      badge: '/icons/icon-192.png',
      tag,
      data: { url },
    }),
  );
});

self.addEventListener('notificationclick', (event) => {
  event.notification.close();
  const url = (event.notification.data as { url?: string } | undefined)?.url ?? '/';
  event.waitUntil(
    self.clients.matchAll({ type: 'window', includeUncontrolled: true }).then((clients) => {
      const existing = clients.find((c) => c.url.endsWith(url));
      if (existing) return existing.focus();
      return self.clients.openWindow(url);
    }),
  );
});
