import tailwindcss from "@tailwindcss/vite";
import { createHash } from "node:crypto";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { defineConfig, type Plugin } from "vite";
import solid from "vite-plugin-solid";

const outputDirectory = process.env.WASMELD_WEB_OUT_DIR ?? "dist";

export default defineConfig({
  server: {
    port: 3000,
  },
  plugins: [solid(), tailwindcss(), pwaWorker()],
  build: {
    outDir: outputDirectory,
    emptyOutDir: true,
    rollupOptions: {
      // Host and Page remain separate HTML documents. Dynamic page imports
      // then create one lazy chunk per primary management view.
      input: {
        host: resolve(import.meta.dirname, "index.html"),
        page: resolve(import.meta.dirname, "page.html"),
      },
    },
  },
});

function pwaWorker(): Plugin {
  return {
    name: "wasmeld-pwa-worker",
    apply: "build",
    generateBundle(_, bundle) {
      // Derive the cache name from emitted filenames and unhashed public
      // assets. A changed build activates a new cache without another PWA
      // dependency or a generated source file in the repository.
      const publicFiles = ["icon.svg", "manifest.webmanifest", "og.png"];
      const generatedFiles = Object.keys(bundle).filter((file) =>
        /\.(?:html|js|css|svg|png|webmanifest)$/.test(file),
      );
      const files = [...new Set([...generatedFiles, ...publicFiles])].sort();
      const digest = createHash("sha256");
      for (const file of files) digest.update(file);
      for (const file of publicFiles) {
        digest.update(readFileSync(resolve(import.meta.dirname, "public", file)));
      }
      const cacheName = `wasmeld-${digest.digest("hex").slice(0, 16)}`;
      const urls = files.map((file) => `/${file}`);
      this.emitFile({
        type: "asset",
        fileName: "sw.js",
        source: serviceWorkerSource(cacheName, urls),
      });
    },
  };
}

function serviceWorkerSource(cacheName: string, urls: string[]): string {
  // Runtime state must never be satisfied from an old cache. Only documents
  // and build assets participate in offline behavior.
  return `const CACHE = ${JSON.stringify(cacheName)};
const PRECACHE = ${JSON.stringify(urls)};
self.addEventListener("install", (event) => {
  event.waitUntil(caches.open(CACHE).then((cache) => cache.addAll(PRECACHE)).then(() => self.skipWaiting()));
});
self.addEventListener("activate", (event) => {
  event.waitUntil(caches.keys().then((keys) => Promise.all(keys.filter((key) => key.startsWith("wasmeld-") && key !== CACHE).map((key) => caches.delete(key)))).then(() => self.clients.claim()));
});
self.addEventListener("fetch", (event) => {
  if (event.request.method !== "GET") return;
  const url = new URL(event.request.url);
  if (url.origin !== self.location.origin || url.pathname === "/healthz" || url.pathname.startsWith("/api/")) return;
  if (event.request.mode === "navigate") {
    event.respondWith(fetch(event.request).then((response) => {
      const copy = response.clone();
      caches.open(CACHE).then((cache) => cache.put(event.request, copy));
      return response;
    }).catch(async () => {
      const cached = await caches.match(event.request);
      if (cached) return cached;
      return caches.match(url.pathname === "/page.html" ? "/page.html" : "/index.html");
    }));
    return;
  }
  event.respondWith(caches.match(event.request).then((cached) => cached || fetch(event.request)));
});
`;
}
