#!/usr/bin/env node
/**
 * Validate Mermaid blocks in a Markdown file by actually calling `mermaid.render()` in headless Chromium.
 *
 * Designed to run inside the Forgejo PDF worker image.
 *
 * Usage (inside worker):
 *   NODE_PATH=/opt/forgejo-pdf/node_modules node /script/mermaid-validate-worker.js /work/file.md
 */

const fs = require("node:fs");
const path = require("node:path");
const os = require("node:os");
const crypto = require("node:crypto");
const puppeteer = require("puppeteer");

function sha256Hex(text) {
  return crypto.createHash("sha256").update(String(text)).digest("hex");
}

function parseMermaidBlocks(markdown) {
  const blocks = [];
  const re = /```mermaid\\s*([\\s\\S]*?)```/g;
  let m;
  while ((m = re.exec(markdown)) !== null) {
    blocks.push({
      start: m.index,
      end: m.index + m[0].length,
      rawBlock: m[1],
    });
  }
  return blocks;
}

async function withBrowser(fn) {
  const userDataDir = fs.mkdtempSync(path.join(os.tmpdir(), "chrome-profile-"));
  const browser = await puppeteer.launch({
    headless: "new",
    args: ["--no-sandbox", "--disable-dev-shm-usage", "--allow-file-access-from-files", `--user-data-dir=${userDataDir}`],
  });
  try {
    return await fn(browser);
  } finally {
    try {
      await browser.close();
    } catch {}
    try {
      fs.rmSync(userDataDir, { recursive: true, force: true });
    } catch {}
  }
}

async function createMermaidPage(browser) {
  const page = await browser.newPage();
  await page.setRequestInterception(true);
  page.on("request", (req) => {
    const u = req.url();
    if (u.startsWith("file:") || u.startsWith("about:") || u.startsWith("data:")) return req.continue();
    return req.abort();
  });
  await page.setContent("<!doctype html><html><head></head><body></body></html>", { waitUntil: "load" });
  await page.addScriptTag({ path: "/opt/forgejo-pdf/assets/js/mermaid.min.js" });
  await page.evaluate(() => {
    if (!globalThis.mermaid) throw new Error("mermaid_missing");
    globalThis.mermaid.initialize({
      startOnLoad: false,
      securityLevel: "strict",
      htmlLabels: false,
      flowchart: { htmlLabels: false, useMaxWidth: false },
      sequence: { htmlLabels: false },
      state: { htmlLabels: false },
      class: { htmlLabels: false },
      fontFamily: "IBM Plex Sans",
      theme: "base",
    });
  });
  return page;
}

async function tryRender(page, id, code) {
  return await page.evaluate(
    async ({ id, code }) => {
      try {
        const r = await globalThis.mermaid.render(id, code);
        return { ok: true, svgLen: r && r.svg ? r.svg.length : 0 };
      } catch (e) {
        const msg = e && typeof e === "object" && (e.str || e.message) ? String(e.str || e.message) : String(e);
        return { ok: false, error: msg };
      }
    },
    { id, code }
  );
}

function firstNonEmptyLine(block) {
  const lines = String(block || "").replace(/\\r\\n?/g, "\\n").split("\\n");
  for (const l of lines) {
    const t = l.trim();
    if (t) return t;
  }
  return "";
}

async function main() {
  const filePath = process.argv[2];
  if (!filePath) {
    console.error("Usage: node mermaid-validate-worker.js /path/to/file.md");
    process.exit(2);
  }
  const markdown = fs.readFileSync(filePath, "utf8");
  const blocks = parseMermaidBlocks(markdown);

  const failures = [];
  await withBrowser(async (browser) => {
    const page = await createMermaidPage(browser);
    for (let i = 0; i < blocks.length; i++) {
      const b = blocks[i];
      const id = "m-" + sha256Hex(`${path.basename(filePath)}|${i}|${b.rawBlock}`).slice(0, 12);
      const r = await tryRender(page, id, b.rawBlock);
      if (!r.ok) {
        failures.push({ index: i, header: firstNonEmptyLine(b.rawBlock), error: r.error });
        if (failures.length >= 25) break;
      }
    }
    await page.close();
  });

  const out = { file: filePath, total: blocks.length, failures };
  console.log(JSON.stringify(out));
  process.exit(failures.length ? 1 : 0);
}

main().catch((e) => {
  console.error(JSON.stringify({ error: String(e && e.message ? e.message : e) }));
  process.exit(1);
});

