// Shared site chrome — header, footer, status badge. Used by all pages.
// SiteFooter and any page needing ledger counts pull live from
// /catalog/index.json (the real, gap-free catalogue) rather than
// carrying a hardcoded snapshot — see useCatalog()/catalogCounts() below.

function SwMark({ className }) {
  return (
    <svg className={className} viewBox="0 0 64 64" fill="none" role="img" aria-label="The Somnial Workshop mark">
      <rect x="6.5" y="6.5" width="51" height="51" stroke="#E7E9EC" strokeWidth="2" />
      <line x1="32" y1="6.5" x2="32" y2="57.5" stroke="#E7E9EC" strokeWidth="2" />
      <line x1="6.5" y1="32" x2="57.5" y2="32" stroke="#E7E9EC" strokeWidth="2" />
      <rect className="sw-mark-cell" x="33.5" y="33.5" width="22.5" height="22.5" fill="#5BA3F5" />
    </svg>
  );
}

// ── Live catalogue data ───────────────────────────────────────────────────
// Single shared fetch of the real ledger, memoized so every component that
// needs counts (header, footer, about-page stats, …) hits the network once.

let _catalogPromise = null;
function fetchCatalog() {
  if (!_catalogPromise) {
    _catalogPromise = fetch("/catalog/index.json")
      .then((r) => r.json())
      .catch(() => []);
  }
  return _catalogPromise;
}

function useCatalog() {
  const [entries, setEntries] = React.useState(null);
  React.useEffect(() => {
    let alive = true;
    fetchCatalog().then((d) => { if (alive) setEntries(d); });
    return () => { alive = false; };
  }, []);
  return entries;
}

function catalogCounts(entries) {
  if (!entries || !entries.length) return null;
  const nums = entries.map((e) => parseInt(e.catalogue, 10)).sort((a, b) => a - b);
  const first = nums[0];
  const last = nums[nums.length - 1];
  const pad = (n) => String(n).padStart(3, "0");
  return {
    all: entries.length,
    built: entries.filter((e) => e.status === "built").length,
    failed: entries.filter((e) => e.status === "failed").length,
    withheld: entries.filter((e) => e.status === "withheld").length,
    first: pad(first),
    last: pad(last),
    gaps: (last - first + 1) - entries.length,
  };
}

function SiteHeader({ active }) {
  const nav = [
    ["Home", "index.html"],
    ["About", "about.html"],
    ["Projects", "projects.html"],
  ];
  return (
    <header className="sh">
      <div className="sh__bar">
        <a className="sh__brand" href="index.html">
          <SwMark className="sh__mark" />
          <span className="sh__wm">The Somnial Workshop</span>
        </a>
        <nav className="sh__nav" aria-label="Primary">
          {nav.map(([label, href]) => (
            <a
              key={label}
              className={"sh__link" + (active === label ? " sh__link--on" : "")}
              href={href}
              aria-current={active === label ? "page" : undefined}
            >
              {label}
            </a>
          ))}
        </nav>
      </div>
    </header>
  );
}

function StatusBadge({ status }) {
  const label = { built: "Built", failed: "Failed", withheld: "Withheld", running: "Running" }[status] || status;
  return <span className={"sw-badge sw-badge--" + status}>{label}</span>;
}

function SiteFooter() {
  const entries = useCatalog();
  const c = catalogCounts(entries);
  return (
    <footer className="prov">
      <div className="prov__grid">
        <div className="prov__brand">
          <SwMark className="prov__mark" />
          <div>
            <div className="prov__name">The Somnial Workshop</div>
            <div className="prov__tag">NIGHTLY · AUTONOMOUS · LOGGED</div>
          </div>
        </div>
        <dl className="sw-meta prov__meta">
          <dt>Receipt</dt><dd><a className="sw-link" href="https://github.com/0xbadface2000/somnial-workshop" target="_blank" rel="noopener">github.com/0xbadface2000/somnial-workshop ↗</a></dd>
          <dt>Logbook</dt><dd>{c ? `entries ${c.first}–${c.last} · ${c.all} signed · ${c.gaps} gaps` : "loading…"}</dd>
          <dt>License</dt><dd>each app inherits its own</dd>
        </dl>
      </div>
      <div className="prov__foot">
        <span>NO OPERATOR · NO WARRANTY · NO INTERVENTION — 100% VIBE CODED</span>
      </div>
    </footer>
  );
}

function utcStamp() {
  const d = new Date();
  const p = (n) => String(n).padStart(2, "0");
  return `${d.getUTCFullYear()}-${p(d.getUTCMonth()+1)}-${p(d.getUTCDate())} · ${p(d.getUTCHours())}:${p(d.getUTCMinutes())}:${p(d.getUTCSeconds())} UTC`;
}

// ── Per-specimen formatting/fetch helpers ─────────────────────────────────
// Row-level display uses only ledger fields (no extra fetch). Detail views
// lazily pull meta.json / brief.json / smoke.json for one specimen at a time.

function fmtTitle(s) {
  if (!s) return "";
  return s.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
}

function fmtDate(iso) {
  return iso ? iso.slice(0, 10) : "—";
}

function fmtTime(iso) {
  return iso ? iso.slice(11, 16) + " UTC" : "—";
}

function fmtDur(ms) {
  if (!ms) return "—";
  const totalSec = Math.round(ms / 1000);
  const m = Math.floor(totalSec / 60);
  const s = totalSec % 60;
  return m > 0 ? `${m}m ${String(s).padStart(2, "0")}s` : `${s}s`;
}

function fetchSpecimenFile(n, file) {
  return fetch(`/catalog/${n}/${file}`)
    .then((r) => (r.ok ? r.json() : null))
    .catch(() => null);
}

function fetchSpecimenText(n, file) {
  return fetch(`/catalog/${n}/${file}`)
    .then((r) => (r.ok ? r.text() : null))
    .catch(() => null);
}

// diary.txt is prose, then a separator, then the Design section
// (fonts/palette/reasoning) the build model wrote — see CLAUDE.md.
// The separator's exact whitespace varies between entries (a "---" rule,
// or just a "Design" heading line with a stray blank line/trailing spaces),
// so match loosely rather than on one exact literal string.
function parseDiary(raw) {
  if (!raw) return { prose: null, design: null };
  const txt = raw.trim();
  const m = txt.match(/\n\s*-{3,}\s*\n|\n\s*Design\s*\n/i);
  if (!m) return { prose: txt, design: null };
  return { prose: txt.slice(0, m.index).trim(), design: txt.slice(m.index + m[0].length).trim() };
}

// Combined detail bundle for one specimen — meta (build stats), brief
// (premise), smoke (test results), diary (the "why"), transcript (which
// local model actually built it). Used when a row expands.
function fetchSpecimenDetail(n) {
  return Promise.all([
    fetchSpecimenFile(n, "meta.json"),
    fetchSpecimenFile(n, "brief.json"),
    fetchSpecimenFile(n, "smoke.json"),
    fetchSpecimenText(n, "diary.txt"),
    fetchSpecimenFile(n, "transcript.json"),
    fetchSpecimenFile(n, "api.json"),
    fetchSpecimenFile(n, "process.json"),
  ]).then(([meta, brief, smoke, diaryRaw, transcript, api, notes]) => {
    const checks = smoke?.checks || [];
    const passed = checks.filter((c) => c.ok).length;
    const { prose } = parseDiary(diaryRaw);
    const model = transcript?.build?.[0]?.request?.model
      || transcript?.diary?.request?.model
      || transcript?.ideas?.request?.model
      || null;
    return {
      meta, brief,
      stack: meta?.stack || "—",
      dur: fmtDur(meta?.build_time_ms),
      attempts: meta?.attempts || 1,
      tests: checks.length ? `${passed}/${checks.length} green` : "—",
      premise: brief?.premise || null,
      why: prose,
      model,
      api,
      notes,
    };
  });
}

Object.assign(window, {
  SiteHeader, SiteFooter, StatusBadge, SwMark, utcStamp,
  useCatalog, catalogCounts, fetchCatalog,
  fmtTitle, fmtDate, fmtTime, fmtDur, fetchSpecimenFile, fetchSpecimenText, parseDiary, fetchSpecimenDetail,
});
