#!/usr/bin/env node
/**
 * Mermaid Self-Healing Pipeline (user-provided "95%+ reliability" edition)
 *
 * Usage:
 *   node tools/mermaid/mermaid-self-heal.js <file-or-dir> [...]
 *
 * Notes:
 * - Edits Markdown files in-place, rewriting ```mermaid fences.
 * - If `mmdc` (mermaid-cli) is available in PATH, it is used for validation.
 * - If `mmdc` is missing, the script still applies repairs but skips validation.
 */

const fs = require("fs");
const path = require("path");
const os = require("os");
const { execSync } = require("child_process");

const SHAPES = [
  "\\[\\[([^\\]]+)\\]\\]", // stadium
  "\\[\\(\\([^\\)]+\\)\\)\\]", // cylindrical
  "\\[\\(/([^\\)]+)\\)\\]\\]", // rounded rect?
  "\\[([^\\]]+)\\]", // rectangle (default)
  "\\(\\(([^\\)]+)\\)\\)", // circle
  "\\(\\{([^\\}]+)\\}\\)", // diamond
  "\\(\\[([^\\]]+)\\]\\)", // hex
  "\\[\\/([^\\]]+)\\/\\]", // parallelogram
  "\\[\\\\([^\\]]+)\\\\\\]", // alt parallelogram
  "\\{\\{([^\\}]+)\\}\\}", // stadium alt
  "\\(\\{([^\\}]+)\\}\\)", // subroutine
  "\\(\\(([^\\)]+)\\)\\)", // circle double
];

const SHAPE_REGEX = new RegExp(SHAPES.map((s) => `(${s})`).join("|"));

function sanitizeAndNormalize(raw) {
  let code =
    String(raw || "")
      .replace(/[\u00A0\u200B\u200E\uFEFF\u2060]/g, "") // invisible
      .replace(/\r\n?/g, "\n")
      .replace(/\t/g, "  ")
      .trim() + "\n";

  // Force header to very first line
  const lines = code.split("\n");
  const firstContent = lines.findIndex((l) => l.trim());
  if (firstContent > 0) {
    const header = lines.splice(firstContent, 1)[0];
    lines.unshift(header.trim());
    code = lines.join("\n");
  }
  return code;
}

function forceValidId(id) {
  if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(id)) return id;
  let clean = String(id || "")
    .replace(/[^A-Za-z0-9_]/g, "_")
    .replace(/^_+/, "")
    .replace(/_+$/, "");
  if (!clean) clean = "node";
  if (/^\d/.test(clean)) clean = "_" + clean;
  return clean;
}

function quoteLabel(label) {
  const s = String(label || "");
  if (!s.includes("\n") && /^[\w\s.,\-–—]+$/.test(s) && !/[":|]/.test(s)) return s;
  return `"${s.replace(/"/g, "#34;").replace(/\n/g, "\\n")}"`;
}

function repairNodesAndLabels(code) {
  // First pass – fix IDs
  code = code.replace(/^(\s*)([^\s\[\](){}]+)(\s*[[\](){}])/gm, (_m, indent, id, shape) => {
    return `${indent}${forceValidId(id)}${shape}`;
  });

  // Second pass – quote shape labels (correctly) for common node syntaxes.
  const esc = (s) => String(s || "").replace(/"/g, "#34;").replace(/\n/g, "\\n");
  const alreadyQuoted = (s) => {
    const t = String(s || "").trim();
    return t.length >= 2 && t.startsWith('"') && t.endsWith('"');
  };

  // [label]
  code = code.replace(/(\b[^\s\[\](){}]+)\[([^\]\n]*)\]/g, (_m, id, label) => {
    if (alreadyQuoted(label)) return `${id}[${label}]`;
    return `${id}["${esc(label)}"]`;
  });

  return code;
}

function detectType(code) {
  const first = String(code || "").split("\n", 1)[0].toLowerCase();
  if (first.includes("sequencediagram")) return "sequence";
  if (first.includes("classdiagram")) return "class";
  if (first.includes("statediagram")) return "state";
  if (first.includes("gantt")) return "gantt";
  if (first.includes("erdiagram")) return "er";
  if (first.includes("pie")) return "pie";
  if (first.includes("gitgraph")) return "gitgraph";
  if (first.includes("mindmap")) return "mindmap";
  if (first.includes("timeline")) return "timeline";
  if (first.includes("quadrantchart")) return "quadrantchart";
  if (first.includes("xychart")) return "xychart";
  return "flowchart";
}

function sequenceSpecificFixes(code) {
  const participants = new Set();
  const participantLines = [];

  const lines = String(code || "").split("\n");
  const cleaned = [];

  for (let line of lines) {
    const pl = line.match(/^\s*participant\s+(.+)/i);
    if (pl) {
      const id = forceValidId(pl[1].split(" as ")[0].trim());
      participants.add(id);
      participantLines.push(`participant ${id}`);
    } else {
      cleaned.push(line);
    }
  }

  // Re-inject participants at top
  let result = [...participantLines, ...cleaned].join("\n");

  // Balance alt/loop/par/opt/critical/rect
  const blocks = ["alt", "else", "loop", "par", "opt", "critical", "rect rgb(0,0,0)"];
  let stack = [];
  for (let line of result.split("\n")) {
    const trimmed = line.trim();
    if (blocks.some((b) => trimmed.startsWith(b))) stack.push(trimmed.split(" ")[0]);
    if (trimmed === "end") {
      if (stack.length) stack.pop();
    }
  }
  while (stack.length) {
    result += "\nend";
    stack.pop();
  }

  return result;
}

function balanceSubgraphs(code) {
  let depth = 0;
  const lines = String(code || "").split("\n");
  const result = [];

  for (let line of lines) {
    if (/\bsubgraph\b/i.test(line)) depth++;
    if (/\bend\b/i.test(line)) depth = Math.max(0, depth - 1);
    result.push(line);
  }
  while (depth-- > 0) result.push("end");
  return result.join("\n");
}

function ensureHeaderAtTop(code) {
  const lines = String(code || "").replace(/\r\n?/g, "\n").split("\n");
  const headerRe =
    /^(flowchart|graph|sequenceDiagram|classDiagram|stateDiagram(?:-v2)?|gantt|ganttChart|erDiagram|pie|gitgraph|mindmap|timeline|quadrantChart|xychart-beta|xychart)\b/i;
  const isInit = (l) => String(l || "").trim().startsWith("%%{");

  const initLine = lines.length > 0 && isInit(lines[0]) ? String(lines[0] || "").trim() : null;

  let headerIdx = -1;
  for (let i = initLine ? 1 : 0; i < lines.length; i++) {
    const t = String(lines[i] || "").trim();
    if (headerRe.test(t)) {
      headerIdx = i;
      break;
    }
  }

  let headerLine = headerIdx >= 0 ? String(lines[headerIdx] || "").trim() : "flowchart TD";
  headerLine = headerLine.replace(/^graph\b/i, "flowchart");
  if (/^flowchart\b/i.test(headerLine) && !/\b(LR|RL|TD|TB|BT)\b/i.test(headerLine)) {
    headerLine = "flowchart TD";
  }

  const out = [];
  if (initLine) out.push(initLine);
  out.push(headerLine);
  for (let i = 0; i < lines.length; i++) {
    if (initLine && i === 0) continue;
    if (headerIdx === i) continue;
    const l = String(lines[i] || "");
    if (!l.trim()) continue;
    out.push(l);
  }
  return out.join("\n").trim() + "\n";
}

function selfHealMermaid(block) {
  let code = ensureHeaderAtTop(sanitizeAndNormalize(block));

  const t = detectType(code);
  if (t === "flowchart") {
    code = repairNodesAndLabels(code);
    code = balanceSubgraphs(code);
  }

  // Final normalisation
  code = code.replace(/-\s+->/g, "-->").replace(/==+/g, "==>").replace(/-\./g, "-.");

  return code;
}

function hasCmd(cmd) {
  try {
    execSync(`command -v ${cmd}`, { stdio: "ignore" });
    return true;
  } catch {
    return false;
  }
}

function validateWithMmdc(inputMmdText) {
  if (!hasCmd("mmdc")) return { ok: null, stderr: "mmdc_not_found" };
  const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "mmdc-heal-"));
  const inFile = path.join(tmpDir, "temp.mmd");
  fs.writeFileSync(inFile, inputMmdText, "utf8");
  try {
    execSync(`mmdc -i ${JSON.stringify(inFile)} -o /dev/null --quiet`, { stdio: "pipe" });
    return { ok: true, stderr: "" };
  } catch (e) {
    const stderr =
      e && typeof e === "object" && e.stderr && Buffer.isBuffer(e.stderr)
        ? e.stderr.toString("utf8")
        : e && typeof e === "object" && typeof e.message === "string"
          ? e.message
          : "mmdc_failed";
    return { ok: false, stderr };
  } finally {
    try {
      fs.rmSync(tmpDir, { recursive: true, force: true });
    } catch {}
  }
}

function healMarkdownFile(filePath) {
  let content = fs.readFileSync(filePath, "utf8");

  content = content.replace(/```mermaid\s*([\s\S]*?)```/g, (_match, rawBlock) => {
    let attempt = selfHealMermaid(rawBlock);
    let healed = false;

    for (let i = 0; i < 5; i++) {
      const v = validateWithMmdc(attempt);
      if (v.ok === null) {
        healed = true; // no validator available; still apply healing output
        break;
      }
      if (v.ok === true) {
        healed = true;
        break;
      }

      const err = v.stderr || "";
      const lineMatch = err.match(/line (\d+)/i);
      const line = lineMatch ? parseInt(lineMatch[1], 10) - 2 : null; // mmdc counts header as line 1 or 2

      if (err.includes("Parse error") && line !== null) {
        let lines = attempt.split("\n");
        let bad = lines[line] || "";
        // Last-ditch quote everything on that line
        bad = bad.replace(/\[([^\]"][^\]]*)\]/g, '["$1"]').replace(/\(([^)"]+)\)/g, '("$1")');
        lines[line] = bad;
        attempt = lines.join("\n");
      }
    }

    const final = healed ? attempt : `%% SELF-HEAL FAILED AFTER 5 ATTEMPTS\n${attempt}`;
    return "```mermaid\n" + final + "\n```";
  });

  fs.writeFileSync(filePath, content);
}

function walkMarkdownFiles(startPath) {
  const st = fs.statSync(startPath);
  if (st.isFile()) {
    if (startPath.toLowerCase().endsWith(".md") || startPath.toLowerCase().endsWith(".markdown")) return [startPath];
    return [];
  }
  if (!st.isDirectory()) return [];
  const out = [];
  const entries = fs.readdirSync(startPath, { withFileTypes: true });
  for (const e of entries) {
    const p = path.join(startPath, e.name);
    if (e.isDirectory()) out.push(...walkMarkdownFiles(p));
    else if (e.isFile() && (p.toLowerCase().endsWith(".md") || p.toLowerCase().endsWith(".markdown"))) out.push(p);
  }
  return out;
}

function main(argv) {
  const targets = argv.slice(2);
  if (!targets.length) {
    console.error("Usage: node tools/mermaid/mermaid-self-heal.js <file-or-dir> [...]");
    process.exit(2);
  }
  for (const t of targets) {
    const abs = path.resolve(t);
    const files = walkMarkdownFiles(abs);
    for (const f of files) healMarkdownFile(f);
  }
}

if (require.main === module) main(process.argv);
