/* eslint-disable no-restricted-globals */
/**
 * POS Service Worker — scoped to /pos.
 *
 * Phase 1 of the offline-POS rollout. This worker only handles *caching*
 * of the POS shell (HTML, JS, CSS, fonts, images). Offline checkout
 * queuing, catalog cache and background sync are added in later phases
 * and live in separate modules — keep this file focused.
 *
 * Design rules enforced here:
 *   1. SCOPE is /pos. We never intercept admin routes (/inventory, /sales, …).
 *   2. We only touch GET requests. POST/PUT/PATCH/DELETE hit the network
 *      directly — business writes must never be served from a cache.
 *   3. API calls to /api/* are NETWORK-FIRST, so the cashier always sees
 *      fresh prices/stock when online. Cached response is a last-resort
 *      fallback when offline.
 *   4. Static shell assets are STALE-WHILE-REVALIDATE so the page boots
 *      instantly and updates in the background.
 *   5. Version the caches via CACHE_VERSION so a new deploy evicts old
 *      caches on activate.
 */

const CACHE_VERSION = "pos-v3";
const SHELL_CACHE = `${CACHE_VERSION}-shell`;
const API_CACHE = `${CACHE_VERSION}-api`;
const MEDIA_CACHE = `${CACHE_VERSION}-media`;

// Minimal shell we try to pre-warm on install. The actual JS/CSS
// filenames are hashed by Next.js so we can't precache them by URL —
// they get cached on first fetch via the stale-while-revalidate handler.
const SHELL_URLS = [
  "/pos",
  "/pos/shifts",
  "/login",
  "/icon.svg",
  "/manifest.webmanifest",
];

// -------------------------------------------------------------- helpers

const isApiRequest = (url) =>
  url.pathname.startsWith("/api/") || url.pathname.startsWith("/media/");

const isMediaStreamRequest = (url) =>
  url.pathname.includes("/media/") && url.pathname.endsWith("/stream");

const isNavigationRequest = (req) =>
  req.mode === "navigate" ||
  (req.method === "GET" && req.headers.get("accept")?.includes("text/html"));

async function precacheLinkedAssetsFromHtml(res, cache) {
  try {
    const contentType = res.headers.get("content-type") || "";
    if (!contentType.includes("text/html")) return;

    const html = await res.text();
    const matches = [
      ...html.matchAll(/<script[^>]+src=["']([^"']+)["']/gi),
      ...html.matchAll(/<link[^>]+href=["']([^"']+)["'][^>]*>/gi),
    ];

    await Promise.all(
      matches
        .map((m) => m[1])
        .filter(Boolean)
        .map((href) => {
          try {
            const assetUrl = new URL(href, self.location.origin);
            if (assetUrl.origin !== self.location.origin) return null;
            return assetUrl.toString();
          } catch {
            return null;
          }
        })
        .filter(Boolean)
        .map(async (assetUrl) => {
          const cached = await cache.match(assetUrl);
          if (cached) return;
          try {
            const assetRes = await fetch(assetUrl);
            if (assetRes && assetRes.ok) {
              await cache.put(assetUrl, assetRes.clone());
            }
          } catch {
            // Ignore best-effort pre-cache failures.
          }
        })
    );
  } catch {
    // Ignore parse/precache failures.
  }
}

// Stale-while-revalidate: serve cache immediately, fetch in background, update.
async function staleWhileRevalidate(req, cacheName) {
  const cache = await caches.open(cacheName);
  const cached =
    (await cache.match(req)) ||
    (await cache.match(req, { ignoreSearch: true }));

  if (cached) {
    if (self.navigator?.onLine !== false) {
      void fetch(req)
        .then((res) => {
          if (res && res.ok && req.method === "GET") {
            cache.put(req, res.clone()).catch(() => { });
          }
        })
        .catch(() => { });
    }
    return cached;
  }

  try {
    const res = await fetch(req);
    if (res && res.ok && req.method === "GET") {
      cache.put(req, res.clone()).catch(() => { });
    }
    return res;
  } catch {
    return new Response("", { status: 503, statusText: "Offline and not cached" });
  }
}

// Network-first: try network, fall back to cache when offline.
async function networkFirst(req, cacheName) {
  const cache = await caches.open(cacheName);
  try {
    const res = await fetch(req);
    if (res && res.ok && req.method === "GET") {
      cache.put(req, res.clone()).catch(() => { });
    }
    return res;
  } catch (err) {
    const cached = await cache.match(req);
    if (cached) return cached;
    // Return an explicit offline JSON for API calls so clients can detect.
    if (isApiRequest(new URL(req.url))) {
      return new Response(
        JSON.stringify({ offline: true, error: "offline" }),
        { status: 503, headers: { "content-type": "application/json" } }
      );
    }
    throw err;
  }
}

// Offline fallback for HTML navigations — serve cached /pos shell if present.
async function navigationFallback(req) {
  const cache = await caches.open(SHELL_CACHE);
  try {
    const res = await fetch(req);
    if (res && res.ok) {
      cache.put(req, res.clone()).catch(() => { });
      if (new URL(req.url).pathname.startsWith("/pos")) {
        cache.put("/pos", res.clone()).catch(() => { });
      }
      void precacheLinkedAssetsFromHtml(res.clone(), cache);
    }
    return res;
  } catch {
    const url = new URL(req.url);
    const cached =
      (await cache.match(req)) ||
      (url.pathname.startsWith("/pos") ? await cache.match("/pos") : null) ||
      (await cache.match("/login"));
    if (cached) return cached;
    return new Response(
      "<h1>Offline</h1><p>POS shell not cached yet. Go online once to install.</p>",
      { status: 503, headers: { "content-type": "text/html" } }
    );
  }
}

// ----------------------------------------------------------- lifecycle

self.addEventListener("install", (event) => {
  event.waitUntil(
    caches
      .open(SHELL_CACHE)
      .then(async (cache) => {
        // Cache each shell URL independently so one failure doesn't drop the rest.
        await Promise.all(
          SHELL_URLS.map((url) =>
            fetch(url, { mode: "no-cors" })
              .then((res) => cache.put(url, res))
              .catch(() => { })
          )
        );
      })
      .then(() => self.skipWaiting())
  );
});

self.addEventListener("activate", (event) => {
  event.waitUntil(
    (async () => {
      // Drop old POS caches from previous versions.
      const keys = await caches.keys();
      await Promise.all(
        keys
          .filter((k) => k.startsWith("pos-") && !k.startsWith(CACHE_VERSION))
          .map((k) => caches.delete(k))
      );
      await self.clients.claim();
    })()
  );
});

// ---------------------------------------------------------------- fetch

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

  // Cross-origin? Let the browser handle it (Next.js fonts, analytics, etc.).
  if (url.origin !== self.location.origin) return;

  // Never cache writes — always go to network.
  if (req.method !== "GET") return;

  // Auth & logout endpoints: always network, never cache (tokens!).
  if (url.pathname.startsWith("/api/auth/")) return;

  // Product images: stale-while-revalidate so POS thumbnails render instantly.
  if (isMediaStreamRequest(url)) {
    event.respondWith(staleWhileRevalidate(req, MEDIA_CACHE));
    return;
  }

  // API: network-first so cashier sees fresh data online.
  if (isApiRequest(url)) {
    event.respondWith(networkFirst(req, API_CACHE));
    return;
  }

  // HTML navigation: network-first with shell fallback.
  if (isNavigationRequest(req)) {
    event.respondWith(navigationFallback(req));
    return;
  }

  // Everything else (JS, CSS, fonts, images): stale-while-revalidate.
  event.respondWith(staleWhileRevalidate(req, SHELL_CACHE));
});

// --------------------------------------------------------- message bus

// Allow the page to trigger an immediate SW update — useful after deploy.
self.addEventListener("message", (event) => {
  if (event.data === "SKIP_WAITING") self.skipWaiting();
});
