const STATIC_CACHE = "md-hub-static-v8";
const DB_NAME = "md-hub-cache";
const DB_VERSION = 3;
const STORE_PAGES = "pages";
const STATIC_ASSETS = [
  "/static/site.css",
  "/static/cache.js",
  "/static/sync.js",
  "/static/realtime.js",
  "/static/editor.js",
  "/static/codemirror.bundle.js",
  "/static/render.js",
  "/static/keyboard-nav.js",
  "/static/tags.js",
  "/static/favicon.png",
  "/static/cairnquire%20logo%402x.webp",
  "/",
  "/docs",
];

self.addEventListener("install", function (event) {
  event.waitUntil(
    caches.open(STATIC_CACHE).then(function (cache) {
      return cache.addAll(STATIC_ASSETS);
    }).then(function () {
      return self.skipWaiting();
    })
  );
});

self.addEventListener("activate", function (event) {
  event.waitUntil(
    caches.keys().then(function (keys) {
      return Promise.all(
        keys.map(function (key) {
          if (key === STATIC_CACHE) {
            return null;
          }
          return caches.delete(key);
        })
      );
    }).then(function () {
      return self.clients.claim();
    })
  );
});

self.addEventListener("fetch", function (event) {
  const request = event.request;
  if (request.method !== "GET") {
    return;
  }

  const url = new URL(request.url);
  if (url.origin !== self.location.origin) {
    return;
  }

  if (request.mode === "navigate") {
    event.respondWith(handleNavigation(request, url));
    return;
  }

  if (url.pathname.startsWith("/static/")) {
    event.respondWith(handleStaticAsset(request));
  }
});

async function handleNavigation(request, url) {
  const cacheKey = url.pathname + url.search;
  try {
    const response = await fetch(request);
    cachePageResponse(cacheKey, response.clone());
    return response;
  } catch (_error) {
    const cachedPage = await getCachedPage(cacheKey);
    if (cachedPage && cachedPage.html) {
      return new Response(markOfflineHTML(cachedPage.html), {
        headers: {
          "Content-Type": "text/html; charset=utf-8",
          "X-MDHub-Cache": "offline",
        },
        status: 200,
      });
    }

    if (cacheKey !== "/") {
      const fallback = await getCachedPage("/");
      if (fallback && fallback.html) {
        return new Response(markOfflineHTML(fallback.html), {
          headers: {
            "Content-Type": "text/html; charset=utf-8",
            "X-MDHub-Cache": "offline",
          },
          status: 200,
        });
      }
    }

    const fallback = await caches.match("/");
    if (fallback) {
      return fallback;
    }

    return new Response("Offline", {
      status: 503,
      headers: { "Content-Type": "text/plain; charset=utf-8" },
    });
  }
}

async function handleStaticAsset(request) {
  const cache = await caches.open(STATIC_CACHE);
  const cached = await cache.match(request);
  if (cached) {
    fetch(request).then(function (response) {
      if (response && response.ok) {
        cache.put(request, response.clone());
      }
    }).catch(function () {});
    return cached;
  }

  const response = await fetch(request);
  if (response && response.ok) {
    cache.put(request, response.clone());
  }
  return response;
}

async function cachePageResponse(pathname, response) {
  if (!response || !response.ok) {
    return;
  }

  const contentType = response.headers.get("Content-Type") || "";
  if (contentType.indexOf("text/html") === -1) {
    return;
  }

  try {
    const html = await response.text();
    await putCachedPage(normalizePath(pathname), {
      path: normalizePath(pathname),
      html: html,
      title: extractTitle(html),
      cachedAt: Date.now(),
    });
  } catch (_error) {}
}

function markOfflineHTML(html) {
  const marker = "<script>window.__MDHUB_OFFLINE_CACHED__=true;<\/script>";
  if (html.indexOf("window.__MDHUB_OFFLINE_CACHED__") !== -1) {
    return html;
  }
  if (html.indexOf("</head>") !== -1) {
    return html.replace("</head>", marker + "</head>");
  }
  return marker + html;
}

function extractTitle(html) {
  const match = html.match(/<title>([^<]*)<\/title>/i);
  return match ? match[1] : "Cairnquire";
}

function normalizePath(path) {
  if (!path || path === "/") {
    return "/";
  }
  return path.replace(/\/$/, "") || "/";
}

function openDB() {
  return new Promise(function (resolve, reject) {
    const request = indexedDB.open(DB_NAME, DB_VERSION);
    request.onerror = function () {
      reject(request.error);
    };
    request.onsuccess = function () {
      resolve(request.result);
    };
    request.onupgradeneeded = function (event) {
      const db = event.target.result;
      if (!db.objectStoreNames.contains(STORE_PAGES)) {
        db.createObjectStore(STORE_PAGES, { keyPath: "path" });
      }
    };
  });
}

async function putCachedPage(path, record) {
  const db = await openDB();
  return new Promise(function (resolve, reject) {
    const tx = db.transaction(STORE_PAGES, "readwrite");
    const store = tx.objectStore(STORE_PAGES);
    store.put(record);
    tx.oncomplete = function () {
      resolve();
    };
    tx.onerror = function () {
      reject(tx.error);
    };
  });
}

async function getCachedPage(path) {
  const db = await openDB();
  return new Promise(function (resolve, reject) {
    const tx = db.transaction(STORE_PAGES, "readonly");
    const store = tx.objectStore(STORE_PAGES);
    const request = store.get(normalizePath(path));
    request.onsuccess = function () {
      resolve(request.result);
    };
    request.onerror = function () {
      reject(request.error);
    };
  });
}
