// Browser-friendly mirror of nexus-bot/src/workflow/narrative.js.
// Exposes window.WorkflowNarrative for workflow-viewer.js. Keep in sync with
// the server module — same fns, same output shape.
(function () {
  "use strict";

  const OP_HUMAN = {
    eq: "=", neq: "≠",
    gt: ">", gte: "≥",
    lt: "<", lte: "≤",
    contains: "contains",
    exists: "is set",
    not_exists: "is missing",
  };

  function humanCron(expr) {
    if (!expr) return "";
    const e = String(expr).trim();
    const sixField = e.split(/\s+/);
    if (sixField.length === 6) {
      const minute = sixField[1];
      const m = minute.match(/^\*\/(\d+)$/);
      if (m) return `every ${m[1]} min`;
    }
    const parts = e.split(/\s+/);
    if (parts.length !== 5) return e;
    const [min, hour, dom, mon, dow] = parts;
    const everyMin = min.match(/^\*\/(\d+)$/);
    if (everyMin && hour === "*" && dom === "*" && mon === "*" && dow === "*") return `every ${everyMin[1]} min`;
    const everyHour = hour.match(/^\*\/(\d+)$/);
    if (everyHour && dom === "*" && mon === "*" && dow === "*" && /^\d+$/.test(min)) return `every ${everyHour[1]}h (at :${min.padStart(2, "0")})`;
    if (/^\d+$/.test(min) && /^\d+$/.test(hour) && dom === "*" && mon === "*" && dow === "*") return `daily at ${hour.padStart(2, "0")}:${min.padStart(2, "0")} UTC`;
    const DOW = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
    if (/^\d+$/.test(min) && /^\d+$/.test(hour) && dom === "*" && mon === "*" && /^\d+$/.test(dow)) return `weekly on ${DOW[Number(dow)]} at ${hour.padStart(2, "0")}:${min.padStart(2, "0")} UTC`;
    if (/^\d+$/.test(min) && /,/.test(hour) && dom === "*" && mon === "*" && dow === "*") return `at ${hour.split(",").map(h => h.padStart(2, "0") + ":" + min.padStart(2, "0")).join(", ")} UTC`;
    if (/,/.test(min) && hour === "*" && dom === "*" && mon === "*" && dow === "*") return `at minute ${min} every hour`;
    if (min === "*" && hour === "*" && dom === "*" && mon === "*" && dow === "*") return "every minute";
    return e;
  }
  function humanOp(op) { return OP_HUMAN[op] || op || ""; }
  function humanVar(expr) {
    if (!expr) return "";
    const s = String(expr);
    const m = s.match(/^\{\{\s*([\w.]+)\s*\}\}$/);
    if (m) {
      const path = m[1];
      const last = path.split(".").pop();
      return last.replace(/_/g, " ");
    }
    return s;
  }
  const FORMATTERS = {
    human: humanOp,
    var: humanVar,
    "cron-human": humanCron,
    lower: (s) => String(s == null ? "" : s).toLowerCase(),
    number: (s) => String(s == null ? "" : s),
    capitalize: (s) => {
      const v = String(s == null ? "" : s);
      return v.length === 0 ? v : v.charAt(0).toUpperCase() + v.slice(1);
    },
  };
  function bindTemplate(template, params) {
    if (!template) return null;
    const tokenRegex = /\{([^{}|]+?)(?:\|([^{}]+))?\}/g;
    let allBound = true;
    const out = template.replace(tokenRegex, (_match, key, fmt) => {
      const k = key.trim();
      const raw = params == null ? undefined : params[k];
      if (raw === undefined || raw === null || raw === "") { allBound = false; return ""; }
      if (!fmt) return String(raw);
      let val = raw;
      for (const name of fmt.split("|").map(s => s.trim()).filter(Boolean)) {
        const f = FORMATTERS[name];
        val = f ? f(val) : val;
      }
      return String(val);
    });
    return allBound ? out.replace(/\s+/g, " ").trim() : null;
  }
  function resolveNarrative(node, def) {
    if (!node || !def) return (node && node.type) || "";
    const params = node.parameters || {};
    const presetBound = bindTemplate(node.narrative_template, params);
    if (presetBound) return presetBound;
    const regBound = bindTemplate(def.narrative_template, params);
    if (regBound) return regBound;
    if (node.name) return node.name;
    if (def.narrative_label) return def.narrative_label;
    if (def.label) return def.label;
    return node.type || "";
  }
  function humanCondition(params) {
    const left = humanVar(params && params.left);
    const op = humanOp(params && params.op);
    const right = params && params.right;
    if (params && params.op === "exists") return `${left} is set`;
    if (params && params.op === "not_exists") return `${left} is missing`;
    if (right === "" || right === undefined || right === null) return `${left} ${op}`.trim();
    return `${left} ${op} ${right}`.trim();
  }
  function branchLabels(node, def) {
    if (!node || !def) return [];
    if (node.type === "logic.if") {
      const cond = humanCondition(node.parameters || {});
      return [
        { label: `Yes — ${cond}`, color: "#10b981" },
        { label: "No",            color: "#ef4444" },
      ];
    }
    if (node.type === "logic.switch") {
      const cases = String((node.parameters || {}).cases || "").split(",").map(s => s.trim()).filter(Boolean);
      if (cases.length === 0) return [];
      return cases.map((c) => ({ label: c, color: "#94a3b8" }));
    }
    return ((def.outputs || [])).map((p) => ({ label: p.name === "main" ? "" : p.name, color: "#888" }));
  }
  function walkStory(workflow, registry) {
    const out = [];
    if (!workflow || !registry) return out;
    const nodesById = {};
    for (const n of (workflow.nodes || [])) nodesById[n.id] = n;
    const defByType = {};
    for (const d of (registry.nodes || [])) defByType[d.type] = d;
    const conns = workflow.connections || {};
    const inDegree = {};
    for (const id of Object.keys(nodesById)) inDegree[id] = 0;
    for (const src of Object.keys(conns)) {
      const byConn = conns[src];
      for (const ct of Object.keys(byConn)) {
        if (ct !== "main") continue;
        for (const slot of (byConn[ct] || [])) {
          for (const t of (slot || [])) if (inDegree[t.node] !== undefined) inDegree[t.node]++;
        }
      }
    }
    const triggers = Object.keys(nodesById).filter((id) => inDegree[id] === 0);
    function nextOf(id) { const byConn = conns[id] || {}; return byConn.main || []; }
    const visited = new Set();
    function visit(id, indent) {
      if (visited.has(id)) { out.push({ kind: "join", text: "(continues as above)", nodeId: id, indent }); return; }
      visited.add(id);
      const node = nodesById[id];
      const def = defByType[node && node.type];
      if (!node || !def) return;
      const text = resolveNarrative(node, def);
      const slots = nextOf(id);
      if (node.type === "logic.if") {
        const cond = humanCondition(node.parameters || {});
        out.push({ kind: "if", text: `If ${cond}`, nodeId: id, indent });
        const yes = (slots[0] || [])[0];
        const no  = (slots[1] || [])[0];
        if (yes) visit(yes.node, indent + 1);
        if (no)  { out.push({ kind: "step", text: "Otherwise", nodeId: id, indent, marker: "else" }); visit(no.node, indent + 1); }
        return;
      }
      if (node.type === "logic.switch") {
        out.push({ kind: "switch", text, nodeId: id, indent });
        for (let i = 0; i < slots.length; i++) for (const t of (slots[i] || [])) visit(t.node, indent + 1);
        return;
      }
      if (node.type === "logic.loop") {
        out.push({ kind: "loop", text, nodeId: id, indent });
        for (const t of (slots[0] || [])) visit(t.node, indent + 1);
        return;
      }
      out.push({ kind: node.type.indexOf("trigger.") === 0 ? "trigger" : "step", text, nodeId: id, indent });
      for (const slot of slots) for (const t of (slot || [])) visit(t.node, indent);
    }
    for (const t of triggers) visit(t, 0);
    return out;
  }

  window.WorkflowNarrative = {
    resolveNarrative, humanCondition, humanCron, humanOp, humanVar, branchLabels, walkStory,
  };
})();
