// ==========================================================================
// sx-sw.js — SX Service Worker
//
// Provides offline-capable caching for the SX platform:
//   /sx/data/*  — Network-first, IndexedDB fallback (page data)
//   /sx/io/*    — Network-first, IndexedDB fallback (IO proxy)
//   /static/*   — Stale-while-revalidate (CSS, JS, images)
//   Other       — Network-only (pass through)
//
// IndexedDB stores response text + content-type with timestamps.
// Stale IDB entries are served when offline but refreshed when online.
// ==========================================================================

var IDB_NAME = "sx-offline";
var IDB_VERSION = 1;
var IDB_STORE = "responses";
var STATIC_CACHE = "sx-static-v2";

// ---------------------------------------------------------------------------
// IndexedDB helpers
// ---------------------------------------------------------------------------

function openDB() {
  return new Promise(function(resolve, reject) {
    var req = indexedDB.open(IDB_NAME, IDB_VERSION);
    req.onupgradeneeded = function() {
      if (!req.result.objectStoreNames.contains(IDB_STORE)) {
        req.result.createObjectStore(IDB_STORE);
      }
    };
    req.onsuccess = function() { resolve(req.result); };
    req.onerror = function() { reject(req.error); };
  });
}

function idbGet(key) {
  return openDB().then(function(db) {
    return new Promise(function(resolve, reject) {
      var tx = db.transaction(IDB_STORE, "readonly");
      var req = tx.objectStore(IDB_STORE).get(key);
      req.onsuccess = function() { resolve(req.result || null); };
      req.onerror = function() { reject(req.error); };
    });
  });
}

function idbSet(key, value) {
  return openDB().then(function(db) {
    return new Promise(function(resolve, reject) {
      var tx = db.transaction(IDB_STORE, "readwrite");
      tx.objectStore(IDB_STORE).put(value, key);
      tx.oncomplete = function() { resolve(); };
      tx.onerror = function() { reject(tx.error); };
    });
  });
}

function idbDelete(key) {
  return openDB().then(function(db) {
    return new Promise(function(resolve, reject) {
      var tx = db.transaction(IDB_STORE, "readwrite");
      tx.objectStore(IDB_STORE).delete(key);
      tx.oncomplete = function() { resolve(); };
      tx.onerror = function() { reject(tx.error); };
    });
  });
}

function idbClear() {
  return openDB().then(function(db) {
    return new Promise(function(resolve, reject) {
      var tx = db.transaction(IDB_STORE, "readwrite");
      tx.objectStore(IDB_STORE).clear();
      tx.oncomplete = function() { resolve(); };
      tx.onerror = function() { reject(tx.error); };
    });
  });
}

// ---------------------------------------------------------------------------
// Fetch strategies
// ---------------------------------------------------------------------------

// Network-first with IndexedDB fallback.
// Used for /sx/data/ and /sx/io/ — dynamic data that should be fresh
// when online but available from cache when offline.
function networkFirstIDB(request) {
  var key = request.url;
  return fetch(request.clone()).then(function(resp) {
    if (resp.ok) {
      var ct = resp.headers.get("Content-Type") || "text/sx";
      resp.clone().text().then(function(text) {
        idbSet(key, { text: text, contentType: ct, ts: Date.now() });
      });
    }
    return resp;
  }).catch(function() {
    // Network failed — try IndexedDB
    return idbGet(key).then(function(cached) {
      if (cached) {
        return new Response(cached.text, {
          status: 200,
          headers: {
            "Content-Type": cached.contentType || "text/sx",
            "X-SX-Offline": "1",
            "X-SX-Cached-At": String(cached.ts)
          }
        });
      }
      return new Response("", { status: 503, statusText: "Offline" });
    });
  });
}

// Stale-while-revalidate for static assets.
// Serve from Cache API immediately, update in background.
function staleWhileRevalidate(request) {
  return caches.open(STATIC_CACHE).then(function(cache) {
    return cache.match(request).then(function(cached) {
      var fetchPromise = fetch(request.clone()).then(function(resp) {
        if (resp.ok) {
          cache.put(request, resp.clone());
        }
        return resp;
      }).catch(function() {
        return cached || new Response("", { status: 503 });
      });
      return cached || fetchPromise;
    });
  });
}

// ---------------------------------------------------------------------------
// Service Worker lifecycle
// ---------------------------------------------------------------------------

self.addEventListener("install", function(event) {
  // Activate immediately — don't wait for old SW to stop
  self.skipWaiting();
});

self.addEventListener("activate", function(event) {
  // Claim all clients immediately
  event.waitUntil(
    Promise.all([
      self.clients.claim(),
      // Clean old static caches
      caches.keys().then(function(names) {
        return Promise.all(
          names.filter(function(n) { return n !== STATIC_CACHE; })
               .map(function(n) { return caches.delete(n); })
        );
      })
    ])
  );
});

// ---------------------------------------------------------------------------
// Fetch routing
// ---------------------------------------------------------------------------

self.addEventListener("fetch", function(event) {
  var url = new URL(event.request.url);

  // Only handle same-origin GET requests
  if (event.request.method !== "GET") return;
  if (url.origin !== self.location.origin) return;

  // SX data endpoints — network-first with IDB fallback
  if (url.pathname.indexOf("/sx/data/") === 0 ||
      url.pathname.indexOf("/sx/io/") === 0) {
    event.respondWith(networkFirstIDB(event.request));
    return;
  }

  // Static assets — stale-while-revalidate
  if (url.pathname.indexOf("/static/") === 0) {
    event.respondWith(staleWhileRevalidate(event.request));
    return;
  }

  // Everything else: pass through to network (default behavior)
});

// ---------------------------------------------------------------------------
// Message handling — cache control from main thread
// ---------------------------------------------------------------------------

self.addEventListener("message", function(event) {
  var msg = event.data;
  if (!msg || !msg.type) return;

  if (msg.type === "invalidate") {
    // Invalidate specific page data from IDB
    if (msg.page === "*") {
      idbClear();
    } else if (msg.page) {
      // Remove all IDB entries matching this page
      openDB().then(function(db) {
        var tx = db.transaction(IDB_STORE, "readwrite");
        var store = tx.objectStore(IDB_STORE);
        var req = store.openCursor();
        req.onsuccess = function() {
          var cursor = req.result;
          if (cursor) {
            if (cursor.key.indexOf("/sx/data/" + msg.page) >= 0) {
              cursor.delete();
            }
            cursor.continue();
          }
        };
      });
    }
  }

  if (msg.type === "clear-static") {
    caches.delete(STATIC_CACHE);
  }
});
