const CACHE_NAME = "epicure-v1";
const SHELL_ASSETS = ["/", "/recipes", "/offline"];

self.addEventListener("install", event => {
  event.waitUntil(caches.open(CACHE_NAME).then(cache => cache.addAll(SHELL_ASSETS)));
  self.skipWaiting();
});

self.addEventListener("activate", event => {
  event.waitUntil(
    caches.keys().then(keys =>
      Promise.all(keys.filter(k => k !== CACHE_NAME).map(k => caches.delete(k)))
    )
  );
  self.clients.claim();
});

self.addEventListener("fetch", event => {
  const { request } = event;
  if (request.method !== "GET") return;
  const url = new URL(request.url);
  // Skip API routes
  if (url.pathname.startsWith("/api/")) return;
  // Cache-first for cook mode pages
  if (url.pathname.includes("/cook")) {
    event.respondWith(
      caches.match(request).then(cached =>
        cached ??
        fetch(request).then(res => {
          const clone = res.clone();
          caches.open(CACHE_NAME).then(c => c.put(request, clone));
          return res;
        })
      )
    );
    return;
  }
  // Network-first for other pages, but keep a copy of every successful
  // response — this is what actually makes "recently visited recipes are
  // available offline" (see /offline) true, and what a "Save for offline"
  // action (save-offline-button.tsx) piggybacks on by re-fetching a page
  // it wants pinned.
  event.respondWith(
    fetch(request)
      .then(res => {
        if (res.ok) {
          const clone = res.clone();
          caches.open(CACHE_NAME).then(c => c.put(request, clone));
        }
        return res;
      })
      .catch(() => caches.match(request).then(cached => cached ?? caches.match("/offline")))
  );
});

// --- Background Sync: replay mutations queued while offline ---
// Mirrors lib/offline-queue.ts's flushOfflineQueue, duplicated here because
// this file is registered as a classic (non-module) script and can't import
// app code. Both read/write the same IndexedDB database — keep the store
// names and shapes in sync with lib/offline-db.ts if either changes.
const DB_NAME = "epicure-offline";
const PENDING_ACTIONS_STORE = "pendingActions";
const SYNC_TAG = "epicure-sync";

function openOfflineDb() {
  return new Promise((resolve, reject) => {
    const req = indexedDB.open(DB_NAME, 1);
    req.onsuccess = () => resolve(req.result);
    req.onerror = () => reject(req.error);
  });
}

async function getPendingActions(db) {
  return new Promise((resolve, reject) => {
    const tx = db.transaction(PENDING_ACTIONS_STORE, "readonly");
    const req = tx.objectStore(PENDING_ACTIONS_STORE).getAll();
    req.onsuccess = () => resolve(req.result.sort((a, b) => a.createdAt.localeCompare(b.createdAt)));
    req.onerror = () => reject(req.error);
  });
}

function deletePendingAction(db, id) {
  return new Promise((resolve, reject) => {
    const tx = db.transaction(PENDING_ACTIONS_STORE, "readwrite");
    tx.objectStore(PENDING_ACTIONS_STORE).delete(id);
    tx.oncomplete = () => resolve();
    tx.onerror = () => reject(tx.error);
  });
}

async function replayPendingActions() {
  const db = await openOfflineDb();
  const pending = await getPendingActions(db);
  let synced = 0;
  for (const action of pending) {
    let ok = false;
    try {
      const res = await fetch(action.url, {
        method: action.method,
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(action.body),
      });
      ok = res.ok;
    } catch {
      ok = false;
    }
    if (!ok) break;
    await deletePendingAction(db, action.id);
    synced += 1;
  }
  if (synced > 0) {
    const clients = await self.clients.matchAll();
    for (const client of clients) client.postMessage({ type: "epicure:offline-synced", count: synced });
  }
}

self.addEventListener("sync", event => {
  if (event.tag === SYNC_TAG) event.waitUntil(replayPendingActions());
});

// --- Push: display incoming notifications and handle taps ---
// lib/push.ts sends {title, body, url} as the payload. Without a "push"
// listener, a push message reaches the browser but never displays anything
// — showNotification() must be called explicitly.
self.addEventListener("push", event => {
  let data = {};
  try {
    data = event.data ? event.data.json() : {};
  } catch {
    data = {};
  }
  const title = data.title || "Epicure";
  event.waitUntil(
    self.registration.showNotification(title, {
      body: data.body || "",
      icon: "/icon-192.svg",
      badge: "/icon-192.svg",
      data: { url: data.url || "/" },
    })
  );
});

self.addEventListener("notificationclick", event => {
  event.notification.close();
  const url = event.notification.data && event.notification.data.url ? event.notification.data.url : "/";
  event.waitUntil(
    self.clients.matchAll({ type: "window", includeUncontrolled: true }).then(clientList => {
      const targetUrl = new URL(url, self.location.origin).href;
      const existing = clientList.find(c => c.url === targetUrl);
      if (existing) return existing.focus();
      return self.clients.openWindow(url);
    })
  );
});
