#!/usr/bin/env node
// build-sw.mjs — regenerates sw.js, the flove service worker.
//
// Full-offline PWA: the worker precaches the whole runnable site so every app
// works with no network once installed. Source of truth = the tracked files
// (`git ls-files`), same principle as build-flove-zip.sh, so the precache list
// always matches what flove.org serves. Run this before publishing (or from
// build-flove-zip.sh) whenever site files change, then commit sw.js.
import { execSync } from 'node:child_process';
import { readFileSync, writeFileSync } from 'node:fs';
import { createHash } from 'node:crypto';
import { join } from 'node:path';

const ROOT = new URL('.', import.meta.url).pathname;   // solo/ — sw.js lives here
const REPO = join(ROOT, '..');                         // repo root

// Runtime asset types worth caching for offline. (No .sh/.mjs/.md/dev config.)
const ALLOW = new Set([
  'html', 'css', 'js', 'json', 'svg', 'png', 'jpg', 'jpeg', 'gif', 'webp',
  'ico', 'webmanifest', 'woff', 'woff2', 'ttf', 'otf', 'mp3', 'mp4', 'webm',
]);

// Never precache these (build outputs, dev config, machine data, huge non-runtime).
const DENY_EXACT = new Set([
  'sw.js', 'build-sw.mjs', 'build-flove-zip.sh', 'build-aliases.mjs',
  'publish-lowai.sh', 'flove.zip', 'flove-solo.zip', '404.html',
  '.htmlvalidate.json',
]);
// Web-only extras kept live but out of the offline app (mirrors the zip).
const DENY_DIRS = ['../docs/docs-files/', 'flove-apk/'];
const DENY_PREFIX_FILES = ['economy/dealy/rewardy.html', 'appy/nety.html', 'appy/profily.html'];

// Offline package = exactly solo/ (the app) + a minimal docs/ (just index.html
// + paradigms.html, the two pages the site links to), mirrored in
// build-flove-zip.sh. Nothing comes from repo-root central/ anymore, and the
// app ships no shared multimedia — audio/ and images/ are NOT in solo anymore
// (the live site and the download are self-contained text). sw.js lives in
// solo/ (live site, scope /solo/) and at the package root (download, scope /):
// a file's scope-relative entry is its path relative to solo/, so repo-root
// docs/ enters as ../docs/… — resolved against the registration scope they land
// at the same URLs the app actually requests (/docs/…).
const tracked = execSync('git ls-files solo docs', { cwd: REPO, encoding: 'utf8' })
  .split('\n').map(s => s.trim()).filter(Boolean)
  .filter(p => !p.startsWith('docs/') || p === 'docs/index.html' || p === 'docs/paradigms.html');

const relOf = (p) => {
  if (p.startsWith('solo/'))                 return p.slice('solo/'.length);
  if (p.startsWith('docs/'))                 return '../' + p;
  return null;                                 // anything else doesn't ship
};
const repoPath = (p) => p.startsWith('../') ? p.slice(3) : `solo/${p}`;

const files = tracked.map(relOf).filter(Boolean).filter(p => {
  const ext = p.split('.').pop().toLowerCase();
  if (!ALLOW.has(ext)) return false;
  if (DENY_EXACT.has(p)) return false;
  if (DENY_DIRS.some(d => p.startsWith(d))) return false;
  if (DENY_PREFIX_FILES.includes(p)) return false;
  return true;
});

// Version hash from file contents → bumps whenever any cached file changes,
// which triggers the worker's activate cleanup + re-cache on next publish.
// Bump SW_REV when the worker's *logic* changes (not just cached files), so
// existing installs get a new cache name and drop the stale one on activate.
const SW_REV = 3;
const hash = createHash('sha256');
hash.update('rev' + SW_REV);
for (const p of files.sort()) hash.update(p).update(readFileSync(join(REPO, repoPath(p))));
const VERSION = 'flove-' + hash.digest('hex').slice(0, 10);

// Scope-relative entries; BASE is resolved at runtime from the worker's own
// registration scope, so the SAME generated sw.js works for the live site
// (registered at /solo/sw.js → scope /solo/) and for the local download package
// (registered at /sw.js on localhost → scope /) with no path rewriting.
// '' = the scope root, which aliases index.html.
const rel = ['', ...files];
const sw = `// sw.js — GENERATED by build-sw.mjs. Do not edit by hand.
// flove service worker: full-offline precache (${rel.length} entries).
const VERSION = ${JSON.stringify(VERSION)};
const BASE = self.registration.scope;
const PRECACHE = ${JSON.stringify(rel, null, 0)};

self.addEventListener('install', (e) => {
  e.waitUntil((async () => {
    const cache = await caches.open(VERSION);
    // Cache each entry independently so one odd file can't abort the whole install.
    await Promise.allSettled(PRECACHE.map((p) => cache.add(BASE + p)));
    await self.skipWaiting();
  })());
});

self.addEventListener('activate', (e) => {
  e.waitUntil((async () => {
    const keys = await caches.keys();
    await Promise.all(keys.filter((k) => k !== VERSION).map((k) => caches.delete(k)));
    await self.clients.claim();
  })());
});

self.addEventListener('fetch', (e) => {
  const req = e.request;
  if (req.method !== 'GET') return;
  if (new URL(req.url).origin !== location.origin) return;

  const isPage = req.mode === 'navigate' ||
    (req.headers.get('accept') || '').includes('text/html');

  if (isPage) {
    // Pages: NETWORK-FIRST so an online visit always shows the latest content;
    // fall back to cache (then the gate) only when offline. Prevents the
    // "I published but still see the old page" service-worker trap.
    e.respondWith((async () => {
      try {
        const res = await fetch(req);
        if (res && res.ok && res.type === 'basic') {
          const cache = await caches.open(VERSION);
          cache.put(req, res.clone());
        }
        return res;
      } catch (err) {
        const cached = await caches.match(req, { ignoreSearch: true });
        if (cached) return cached;
        const gate = await caches.match(BASE + 'start.html');
        if (gate) return gate;
        throw err;
      }
    })());
    return;
  }

  // Assets (css/js/img/…): CACHE-FIRST for speed; fill the cache on a miss.
  e.respondWith((async () => {
    const cached = await caches.match(req, { ignoreSearch: true });
    if (cached) return cached;
    try {
      const res = await fetch(req);
      if (res && res.ok && res.type === 'basic') {
        const cache = await caches.open(VERSION);
        cache.put(req, res.clone());
      }
      return res;
    } catch (err) {
      throw err;
    }
  })());
});
`;

writeFileSync(`${ROOT}sw.js`, sw);
console.log(`sw.js written — ${VERSION}, ${rel.length} precache entries`);
