// Service Worker for MAESTRO. Two roles:
//   1. PWA install eligibility (a fetch handler must be registered).
//   2. V2 Web Push delivery — receives push events from the server and
//      either suppresses them when a visible tab already handled the
//      notification (ACK protocol) or shows an OS notification.
//
// Spec: docs/superpowers/specs/2026-05-28-browser-notifications-v2-webpush.md.

const VERSION = 'v2';
const ACK_TIMEOUT_MS = 200;

self.addEventListener('install', (event) => {
  self.skipWaiting();
});

self.addEventListener('activate', (event) => {
  event.waitUntil(self.clients.claim());
});

self.addEventListener('fetch', (event) => {
  const req = event.request;
  if (req.method !== 'GET') return;

  const url = new URL(req.url);
  // Only handle same-origin /ui/ requests; let the network handle everything else.
  if (url.origin !== self.location.origin) return;
  if (!url.pathname.startsWith('/ui/')) return;

  // Network-first, fall back to cache only when offline. Never serve stale
  // HTML/JS by default — users always get the latest deploy when online.
  event.respondWith(
    fetch(req)
      .then((res) => {
        // Opportunistically cache successful responses for offline fallback.
        if (res.ok) {
          const clone = res.clone();
          caches.open(VERSION).then((cache) => cache.put(req, clone)).catch(() => {});
        }
        return res;
      })
      .catch(() => caches.match(req).then((hit) => hit || Response.error()))
  );
});

// ── Push notification handler ───────────────────────────────────────────
// Strategy: when one or more open tabs exist, broadcast a "notify-request"
// and wait up to ACK_TIMEOUT_MS for any visible tab to ACK. ACK means the
// page is showing the user state changes in-app, so we suppress the OS
// notification. No ACK → tab is hidden or closed (race conditions included)
// → we show the OS notification. This is more robust than visibilityState
// polling because the page decides whether IT will handle the user-facing
// surfacing.

self.addEventListener('push', (event) => {
  if (!event.data) return;
  let payload;
  try {
    payload = event.data.json();
  } catch {
    payload = { title: 'MAESTRO', body: event.data.text(), tag: 'maestro' };
  }

  const showOptions = {
    body: payload.body ?? '',
    tag: payload.tag,
    icon: payload.icon ?? '/ui/icon-192.png',
    badge: '/ui/icon-192.png',
    data: payload.data ?? {},
  };

  event.waitUntil((async () => {
    const clients = await self.clients.matchAll({
      type: 'window',
      includeUncontrolled: true,
    });
    if (clients.length === 0) {
      await self.registration.showNotification(payload.title ?? 'MAESTRO', showOptions);
      return;
    }
    const handled = await waitForAck(clients, payload);
    if (!handled) {
      await self.registration.showNotification(payload.title ?? 'MAESTRO', showOptions);
    }
  })());
});

function waitForAck(clients, payload) {
  return new Promise((resolve) => {
    let settled = false;
    const handler = (e) => {
      if (settled) return;
      if (e.data && e.data.type === 'notification-handled' && e.data.tag === payload.tag) {
        settled = true;
        self.removeEventListener('message', handler);
        resolve(true);
      }
    };
    self.addEventListener('message', handler);
    setTimeout(() => {
      if (settled) return;
      settled = true;
      self.removeEventListener('message', handler);
      resolve(false);
    }, ACK_TIMEOUT_MS);
    for (const client of clients) {
      client.postMessage({ type: 'notify-request', tag: payload.tag, payload });
    }
  });
}

// ── Notification click handler ──────────────────────────────────────────
// Focus an existing /ui/ tab if present (and tell it which task to open),
// otherwise open a new one at /ui/?task=N.

self.addEventListener('notificationclick', (event) => {
  event.notification.close();
  const taskId = event.notification.data && event.notification.data.taskId;
  const targetUrl = taskId ? `/ui/?task=${taskId}` : '/ui/';
  event.waitUntil((async () => {
    const clients = await self.clients.matchAll({
      type: 'window',
      includeUncontrolled: true,
    });
    for (const client of clients) {
      if (client.url.includes('/ui/')) {
        try {
          await client.focus();
        } catch {
          // some browsers throw if focus is not allowed — fall through
        }
        client.postMessage({ type: 'open-task', taskId });
        return;
      }
    }
    await self.clients.openWindow(targetUrl);
  })());
});
