/* Brain of Reese — document viewer (phase 10) + the shared document * renderer (phase 26). * * Two jobs: * * 1. renderDocument(doc, { titleEl, metaEl, contentEl }) — the * EXPORTED rendering core. The /document.html page and the document * modal (assets/document-modal.js, phase 26) both render through it, * so the two surfaces can never drift: * * • md / markdown → the shared escape-first renderer (markdown.js) * in a ≤46rem centered column; * • any other → the raw content as a text node inside *
 (mono, horizontal
 *                         scroll).
 *
 *  2. The /document.html page itself: reads `source`/`path` query
 *     params, fetches the stateless content endpoint
 *     (GET /api/documents/content — database only, no filesystem), and
 *     renders via renderDocument into #doc-title / #doc-meta /
 *     #doc-content. A missing document (unknown pair, missing params,
 *     network error) shows the designed not-found card with a link back
 *     to the Sources page.
 *
 * XSS-safe by construction: markdown is escaped before transform, raw
 * formats are set via textContent, and every document-derived string
 * (title, badges, path) is written with textContent — never innerHTML
 * (innerHTML goes through renderMarkdown, which escapes first).
 *
 * Phase 19: the viewer joins the shared header (assets/header.js) — the
 * whoami fetch is the module's cached promise (one request per page,
 * shared with initSharedHeader's toggling). Phase 34 task 02: the New
 * chat binding is module-owned (assets/header.js, the SINGLE one) — on
 * this non-chat page it clears the phase-14 conversation key and
 * navigates to the chat's empty state.
 *
 * Phase 26 (import safety): the modal module imports renderDocument
 * from THIS file on the chat/sources pages, so everything
 * /document.html-specific runs only when #doc-title exists (the guard
 * around the page block below). Importing renderDocument elsewhere has
 * no side effects: no back-link resolution, no whoami, no content
 * fetch, no New Chat binding.
 */

import { fetchIsAdmin, initSharedHeader } from "./header.js";

function fmtDate(iso) {
  try {
    return new Date(iso).toLocaleString();
  } catch {
    return iso;
  }
}

function contentUrl(s, p) {
  return "/api/documents/content?source=" + encodeURIComponent(s) + "&path=" + encodeURIComponent(p);
}

function metaBadge(cls, text) {
  const el = document.createElement("span");
  el.className = cls;
  el.textContent = text;
  return el;
}

/* ---------- shared renderer (phase 26, task 02) ----------
 * Populates the three elements every render surface provides: a title,
 * a .doc-meta badge row (source · format · mono path · indexed ·
 * chunks), and a content container — .doc-md for md/markdown (the
 * shared escape-first renderer), 
 otherwise. The
 * XSS contract: innerHTML only through renderMarkdown; every
 * document-derived string is a text node. */
export function renderDocument(doc, { titleEl, metaEl, contentEl }) {
  titleEl.textContent = doc.title;
  // Phase 34 task 04: the titlebar title ellipsizes — the full title
  // stays reachable on hover via the `title` attribute.
  titleEl.setAttribute("title", doc.title);

  const pathCode = document.createElement("code");
  pathCode.className = "doc-path";
  pathCode.textContent = doc.path;
  metaEl.replaceChildren(
    metaBadge("doc-source-badge", doc.source),
    metaBadge("format-badge", doc.format),
    pathCode,
    metaBadge("doc-indexed", `Indexed ${fmtDate(doc.indexed_at)}`),
    metaBadge("doc-chunks", `${doc.chunks} chunk${doc.chunks === 1 ? "" : "s"}`),
  );

  contentEl.replaceChildren();
  if (doc.format === "md" || doc.format === "markdown") {
    const wrap = document.createElement("div");
    wrap.className = "doc-md";
    wrap.innerHTML = renderMarkdown(doc.content); // escape-first: XSS-safe
    contentEl.appendChild(wrap);
  } else {
    const pre = document.createElement("pre");
    pre.className = "doc-raw";
    pre.textContent = doc.content; // text node: never parsed as HTML
    contentEl.appendChild(pre);
  }
}

/* ---------- /document.html page (phases 10/13/19) ----------
 * Phase 26: viewer-page-specific — see the import-safety note in the
 * header. The guard is #doc-title: it exists only on this page, so the
 * modal module's `import { renderDocument } from "./document.js"` on
 * the chat/sources pages evaluates none of the code below. */
if (document.querySelector("#doc-title")) {
  const params = new URLSearchParams(window.location.search);
  const source = params.get("source") || "";
  const path = params.get("path") || "";

  const titleEl = document.querySelector("#doc-title");
  const metaEl = document.querySelector("#doc-meta");
  const contentEl = document.querySelector("#doc-content");
  const notFoundEl = document.querySelector("#doc-not-found");
  const mainEl = document.querySelector("#main");
  const backLink = document.querySelector("#doc-back");

  /* Back button (phase 13): the return target comes from the `back`
   * query param, not the browser history — the dedicated page is
   * reachable directly (no history to go back to). The param is honored
   * only for same-origin relative URLs (starts with "/" but not "//"),
   * so absolute (https://…), protocol-relative (//…), and
   * pseudo-protocol (javascript:…) values are rejected; anything else
   * falls back to the Sources page. The static href="/sources.html" in
   * document.html remains the no-JS fallback, and with the href set the
   * anchor's default click behavior IS the deterministic navigation (no
   * browser-history heuristics). */
  const backParam = params.get("back") || "";
  const backTarget =
    backParam.startsWith("/") && !backParam.startsWith("//")
      ? backParam
      : "/sources.html";
  backLink.href = backTarget;
  const backLabel = backLink.querySelector("span");
  if (backLabel) {
    backLabel.textContent =
      backTarget === "/"
        ? "Chat"
        : backTarget === "/sources.html"
          ? "Sources"
          : "Back";
  }

  /* renderDocument fills the page elements; the page additionally owns
   * the document.title (the modal keeps the page title untouched). */
  function render(doc) {
    renderDocument(doc, { titleEl, metaEl, contentEl });
    document.title = `${doc.title} · Brain of Reese`;
  }

  function showNotFound() {
    titleEl.textContent = "Document not found";
    titleEl.removeAttribute("title"); // no stale full-title tooltip
    document.title = "Document not found · Brain of Reese";
    metaEl.replaceChildren();
    contentEl.replaceChildren();
    notFoundEl.hidden = false;
  }

  /* Phase 19 (phase 34 task 03, owner confirmation 2026-08-26): the
   * viewer now carries the SAME standard bar as every other page —
   * nav incl. the admin-only links, Tuning toggle, Sync, New chat, the
   * auth pair — all toggled here on the module's cached whoami.
   * Independent of the doc fetch (its own IIFE — load() below never
   * waits on it).
   * (fetchIsAdmin is imported for parity with the other header
   * consumers — the module's cached promise is the single whoami per
   * page either way.) */
  (async () => {
    await initSharedHeader();
  })();

  /* The New chat binding is module-owned (assets/header.js, phase 34
   * task 02 — the SINGLE binding): on this non-chat page it clears the
   * phase-14 conversation key and navigates to the chat's empty state. */

  async function load() {
    try {
      if (!source || !path) {
        showNotFound();
        return;
      }
      const r = await fetch(contentUrl(source, path));
      if (!r.ok) {
        showNotFound();
        return;
      }
      render(await r.json());
    } catch {
      showNotFound();
    } finally {
      mainEl.focus(); // move focus to main on load (a11y)
    }
  }

  load();
}