// Fluentra Editor — pure utilities & constants (no React state)
// Loaded BEFORE shapes, panels, and RuleBuilder.

window.FE = window.FE || {};

/* ══════════════════════════════════════════════════════════════════
   NODE PALETTE COLORS
   ═════════════════════════════════════════════════════════════ */
window.FE.NODE_COLORS = {
  start:   { ring:"#34D399", glow:"rgba(52,211,153,.35)",  bg:"#0E1A18" },
  end:     { ring:"#F87171", glow:"rgba(248,113,113,.35)", bg:"#1A1014" },
  cond:    { ring:"#FBBF24", glow:"rgba(251,191,36,.35)",  bg:"#1A1610" },
  action:  { ring:"#22D3EE", glow:"rgba(34,211,238,.35)",  bg:"#0F1B22" },
  api:     { ring:"#34D399", glow:"rgba(52,211,153,.30)",  bg:"#0F1B16" },
  wait:    { ring:"#A78BFA", glow:"rgba(167,139,250,.35)", bg:"#161226" },
  varup:   { ring:"#A78BFA", glow:"rgba(167,139,250,.30)", bg:"#161226" },
  sleep:   { ring:"#94A3B8", glow:"rgba(148,163,184,.30)", bg:"#13161E" },
  webhook: { ring:"#22D3EE", glow:"rgba(34,211,238,.55)",  bg:"#0F1B22" },
  fork:    { ring:"#FDE68A", glow:"rgba(253,230,138,.30)", bg:"#1A1810" },
  join:    { ring:"#67E8F9", glow:"rgba(103,232,249,.30)", bg:"#0F1B22" },
  sub:     { ring:"#34D399", glow:"rgba(52,211,153,.30)",  bg:"#0F1B16" },
};

/* ══════════════════════════════════════════════════════════════════
   TYPE MAPPING
   ═════════════════════════════════════════════════════════════ */
window.FE.mapNodeTypeToKind = function(type) {
  return ({
    Start:'start', End:'end', Condition:'cond',
    Action:'action', ApiRequest:'api', WaitUntil:'wait',
    VariableUpdate:'varup', Sleep:'sleep', Fork:'fork',
    Join:'join', SubFlow:'sub', Webhook:'webhook',
    webhook:'webhook',
  })[type] || 'action';
};

window.FE.kindToType = function(kind) {
  return ({
    start:'Start', end:'End', cond:'Condition',
    action:'Action', api:'ApiRequest', wait:'WaitUntil',
    varup:'VariableUpdate', sleep:'Sleep', fork:'Fork',
    join:'Join', sub:'SubFlow', webhook:'Webhook',
  })[kind] || 'Action';
};

/* ══════════════════════════════════════════════════════════════════
   AUTO LAYOUT
   ═════════════════════════════════════════════════════════════ */
window.FE.autoLayout = function(nodeIds, edges, opts) {
  const { NW=160, NH=54, HG=80, VG=56, padX=48, padY=48, maxCols=10, branchOff=28 } = opts || {};

  const adj = {}, edgeIdx = {};
  nodeIds.forEach(id => { adj[id] = []; });
  edges.forEach(({ from, to }, i) => {
    if (adj[from]) adj[from].push(to);
    edgeIdx[`${from}|${to}`] = i;
  });

  const WHITE = 0, GRAY = 1, BLACK = 2;
  const state = {};
  nodeIds.forEach(id => state[id] = WHITE);

  const backEdges = new Set();
  function dfs(u) {
    state[u] = GRAY;
    for (const v of adj[u] || []) {
      if (state[v] === GRAY) {
        const idx = edgeIdx[`${u}|${v}`];
        if (idx !== undefined) backEdges.add(idx);
      } else if (state[v] === WHITE) {
        dfs(v);
      }
    }
    state[u] = BLACK;
  }
  nodeIds.forEach(id => { if (state[id] === WHITE) dfs(id); });

  const out = {}, indeg = {}, parents = {};
  nodeIds.forEach(id => { out[id]=[]; indeg[id]=0; parents[id]=[]; });
  edges.forEach(({ from, to, color }, i) => {
    if (backEdges.has(i)) return;
    if (out[from]) out[from].push({ to, color });
    if (indeg[to] !== undefined) indeg[to]++;
    if (parents[to]) parents[to].push({ from, color });
  });

  const layers = [], visited = new Set();
  let queue = nodeIds.filter(id => indeg[id]===0);
  while (queue.length || nodeIds.some(id => !visited.has(id))) {
    if (!queue.length) {
      const forced = nodeIds
        .filter(id => !visited.has(id))
        .reduce((a, b) => indeg[a] <= indeg[b] ? a : b);
      queue.push(forced);
    }
    const layer = [...queue]; queue = [];
    layers.push(layer);
    layer.forEach(id => {
      visited.add(id);
      (out[id]||[]).forEach(({ to }) => {
        if (!visited.has(to)) { indeg[to]--; if (indeg[to]<=0) queue.push(to); }
      });
    });
  }

  const desiredY = {};
  const backboneY = padY + 200;

  layers.forEach(layer => {
    layer.forEach(id => {
      const pars = parents[id];
      let y;
      if (pars.length === 0) {
        y = backboneY;
      } else {
        let sum = 0, cnt = 0;
        pars.forEach(({ from, color }) => {
          const py = desiredY[from] !== undefined ? desiredY[from] : backboneY;
          sum += py;
          cnt++;
        });
        y = sum / cnt;
        pars.forEach(({ from, color }) => {
          if (color === '#34D399') y -= branchOff;
          if (color === '#F87171') y += branchOff;
        });
        if (pars.length >= 2) {
          const pys = pars.map(({ from }) => desiredY[from] ?? backboneY);
          const spread = Math.max(...pys) - Math.min(...pys);
          if (spread > branchOff * 1.5) {
            const nearBackbone = pys.filter(py => Math.abs(py - backboneY) < branchOff).length;
            if (nearBackbone >= pars.length / 2) y = backboneY;
          }
        }
      }
      desiredY[id] = y;
    });
  });

  const pos = {};
  let baseY = padY;

  for (let g = 0; g < layers.length; g += maxCols) {
    const group = layers.slice(g, g + maxCols);
    const groupNodes = [];

    group.forEach((layer, colIdx) => {
      const sorted = [...layer].sort((a, b) => desiredY[a] - desiredY[b]);
      sorted.forEach((id, idx) => {
        let y = Math.max(desiredY[id], baseY);
        if (idx > 0) {
          const prevId = sorted[idx - 1];
          const prevY = groupNodes.find(n => n.id === prevId)?.y || baseY;
          y = Math.max(y, prevY + NH + VG);
        }
        groupNodes.push({ id, y });
        pos[id] = { x: padX + (g + colIdx) * (NW + HG), y };
      });
    });

    const maxBottom = groupNodes.length
      ? Math.max(...groupNodes.map(n => n.y + NH)) + padY
      : baseY + padY;
    baseY = maxBottom;
  }

  return pos;
};

/* ══════════════════════════════════════════════════════════════════
   DIAGRAM SERIALIZATION
   ═════════════════════════════════════════════════════════════ */
window.FE.parseDiagram = function(diagramJson) {
  const rawNodes = diagramJson.nodes || {};
  const edges = [];

  const nodeMap = {};
  if (Array.isArray(rawNodes)) {
    rawNodes.forEach(n => {
      const nodeId = n.id || n.node_id;
      if (nodeId) nodeMap[nodeId] = { type: n.type, ...(n.config || {}), _config: n.config || {} };
    });
  } else {
    Object.keys(rawNodes).forEach(id => { nodeMap[id] = rawNodes[id]; });
  }

  const ids = Object.keys(nodeMap);

  if (Array.isArray(diagramJson.edges)) {
    diagramJson.edges.forEach(e => {
      if (!e.from || !e.to) return;
      const color = e.condition === 'True'  ? '#34D399'
                  : e.condition === 'False' ? '#F87171'
                  : undefined;
      const edge = { from: e.from, to: e.to };
      if (color) edge.color = color;
      edges.push(edge);
    });
  }

  ids.forEach(id => {
    const n = nodeMap[id];
    (n.transitions || []).forEach(to => { if (to) edges.push({ from: id, to }); });
    if (n.true_node)  edges.push({ from: id, to: n.true_node,  color: '#34D399' });
    if (n.false_node) edges.push({ from: id, to: n.false_node, color: '#F87171' });
  });

  const positions = window.FE.autoLayout(ids, edges);
  const nodes = ids.map(id => {
    const n = nodeMap[id];
    const kind = window.FE.mapNodeTypeToKind(n.type);
    const pos = positions[id] || { x: 40, y: 40 };
    return { id, ...pos, kind, name: id, sub: n.type, _raw: { ...n } };
  });
  return { nodes, edges };
};

window.FE.serializeDiagram = function(name, nodes, edges, meta) {
  const nodesObj = {};
  nodes.forEach(n => {
    const base = n._raw ? {...n._raw} : {type:window.FE.kindToType(n.kind)};
    base.type = base.type || window.FE.kindToType(n.kind);
    const trueOut  = edges.find(e => e.from===n.id && (e.fromPort==='true'  || e.color==='#34D399'));
    const falseOut = edges.find(e => e.from===n.id && (e.fromPort==='false' || e.color==='#F87171'));
    const outs     = edges.filter(e => e.from===n.id && e.fromPort!=='true' && e.fromPort!=='false' && !e.color).map(e=>e.to);
    if (trueOut || falseOut) {
      if (trueOut)  base.true_node  = trueOut.to;
      if (falseOut) base.false_node = falseOut.to;
      delete base.transitions;
    } else {
      base.transitions = outs;
    }
    nodesObj[n.id] = base;
  });
  return {
    name,
    nodes: nodesObj,
    instantiation_key_expression: meta?.instantiation_key_expression||'',
    acceptance_criteria: meta?.acceptance_criteria||'',
    ttl: meta?.ttl||86400,
  };
};

/* ══════════════════════════════════════════════════════════════════
   CANVAS CONSTANTS & PORT HELPERS
   ═════════════════════════════════════════════════════════════ */
window.FE.EC_NW = 160;
window.FE.EC_NH = 54;
window.FE.SNAP_R = 30;
window.FE.COND_W = 136;
window.FE.COND_H = 66;

window.FE.getNodeH = function(kind, portCount) {
  if (kind === 'fork' || kind === 'join')
    return Math.max(window.FE.EC_NH, (portCount + 1) * 21 + 12);
  return kind === 'cond' ? window.FE.COND_H : window.FE.EC_NH;
};

window.FE.getNodePorts = function(node, edges) {
  const { kind, x, y } = node;

  if (kind === 'cond') {
    return {
      w: window.FE.COND_W, h: window.FE.COND_H,
      inputs:  [{ id:'in',    x,          y: y + window.FE.COND_H/2 }],
      outputs: [
        { id:'true',  x: x+window.FE.COND_W, y: y + window.FE.COND_H*0.27, label:'T', color:'#34D399' },
        { id:'false', x: x+window.FE.COND_W, y: y + window.FE.COND_H*0.73, label:'F', color:'#F87171' },
      ],
    };
  }

  if (kind === 'fork') {
    const numOut = Math.max(2, edges.filter(e => e.from===node.id).length + 1);
    const h = window.FE.getNodeH('fork', numOut);
    return {
      w: window.FE.EC_NW, h,
      inputs:  [{ id:'in', x, y: y + h/2 }],
      outputs: Array.from({length: numOut}, (_, i) => ({
        id: `out_${i}`, x: x+window.FE.EC_NW, y: y + h*(i+1)/(numOut+1),
      })),
    };
  }

  if (kind === 'join') {
    const numIn = Math.max(2, edges.filter(e => e.to===node.id).length + 1);
    const h = window.FE.getNodeH('join', numIn);
    return {
      w: window.FE.EC_NW, h,
      inputs: Array.from({length: numIn}, (_, i) => ({
        id: `in_${i}`, x, y: y + h*(i+1)/(numIn+1),
      })),
      outputs: [{ id:'out', x: x+window.FE.EC_NW, y: y + h/2 }],
    };
  }

  if (kind === 'start') return {
    w: window.FE.EC_NW, h: window.FE.EC_NH,
    inputs:  [],
    outputs: [{ id:'out', x: x+window.FE.EC_NW, y: y + window.FE.EC_NH/2 }],
  };

  if (kind === 'end') return {
    w: window.FE.EC_NW, h: window.FE.EC_NH,
    inputs:  [{ id:'in', x, y: y + window.FE.EC_NH/2 }],
    outputs: [],
  };

  return {
    w: window.FE.EC_NW, h: window.FE.EC_NH,
    inputs:  [{ id:'in',  x,        y: y + window.FE.EC_NH/2 }],
    outputs: [{ id:'out', x:x+window.FE.EC_NW, y: y + window.FE.EC_NH/2 }],
  };
};

/* ══════════════════════════════════════════════════════════════════
   UI CONSTANTS & SIMPLE COMPONENTS
   ═════════════════════════════════════════════════════════════ */
window.FE.DARK_SEL = {
  background:'#0d0f1c', color:'#EDEFF7',
  border:'1px solid rgba(255,255,255,.12)',
  borderRadius:6, fontFamily:'Geist Mono, monospace',
  fontSize:11, padding:'5px 8px',
};

window.FE.CfgHead = function CfgHead({ label }) {
  return (
    <div style={{fontSize:10,fontFamily:'Geist Mono,monospace',letterSpacing:'.06em',
      textTransform:'uppercase',color:'var(--ink-faint)',marginTop:4}}>{label}</div>
  );
};

window.FE.AdvancedToggle = function AdvancedToggle({ active, onClick, color }) {
  const c = color || '#6366F1';
  return (
    <button onClick={onClick} title={active ? 'Close advanced' : 'Open advanced code editor'}
      style={{
        width:28, height:28, borderRadius:6, cursor:'pointer', flexShrink:0,
        display:'flex', alignItems:'center', justifyContent:'center',
        background: active ? 'rgba(99,102,241,.15)' : 'transparent',
        border: active ? '1px solid rgba(99,102,241,.35)' : '1px solid rgba(255,255,255,.10)',
        color: active ? c : 'var(--ink-faint)',
        fontFamily:'Geist Mono,monospace', fontSize:12, transition:'all .15s',
      }}>
      {'{ }'}
    </button>
  );
};

/* ══════════════════════════════════════════════════════════════════
   NO-CODE VALUE SYSTEM
   ═════════════════════════════════════════════════════════════ */
window.FE.COMMON_EVENT_FIELDS = ['type','id','timestamp','source','payload','truck_id','speed','status','amount','email','name'];
window.FE.COMMON_VARIABLE_FIELDS = ['truck_id','count','last_speed','ticket_response','api_response','delay','message','order_id','customer_id','status_code'];

window.FE.discoverDiagramVariables = function(nodes) {
  const vars = new Set(window.FE.COMMON_VARIABLE_FIELDS);
  (nodes || []).forEach(n => {
    if (n.kind === 'varup' && n._raw?.updates) {
      Object.keys(n._raw.updates).forEach(k => vars.add(k));
    }
    if (n.kind === 'api' && n._raw?.save_response_as) {
      vars.add(n._raw.save_response_as);
    }
  });
  return Array.from(vars).sort();
};

window.FE.VALUE_TYPE_OPTS = [
  {k:'text',       l:'Text'},
  {k:'number',     l:'Number'},
  {k:'bool',       l:'True/False'},
  {k:'variable',   l:'Variable'},
  {k:'math',       l:'Calculate'},
  {k:'counter',    l:'Counter'},
  {k:'expression', l:'Expression'},
];

window.FE.OP_GROUPS = {
  text: [
    {k:'==', l:'is equal to'}, {k:'!=', l:'is not equal to'},
    {k:'contains', l:'contains'}, {k:'is_empty', l:'is empty'}, {k:'is_not_empty', l:'is not empty'},
  ],
  number: [
    {k:'==', l:'is equal to'}, {k:'!=', l:'is not equal to'},
    {k:'>', l:'is greater than'}, {k:'<', l:'is less than'},
    {k:'>=', l:'is greater than or equal'}, {k:'<=', l:'is less than or equal'},
  ],
  bool: [
    {k:'==', l:'is true'}, {k:'!=', l:'is false'},
  ],
};

window.FE.ALL_OPERATORS = [
  {k:'==', l:'is equal to', types: new Set(['text','number','bool'])},
  {k:'!=', l:'is not equal to', types: new Set(['text','number','bool'])},
  {k:'contains', l:'contains', types: new Set(['text'])},
  {k:'is_empty', l:'is empty', types: new Set(['text'])},
  {k:'is_not_empty', l:'is not empty', types: new Set(['text'])},
  {k:'>', l:'is greater than', types: new Set(['number'])},
  {k:'<', l:'is less than', types: new Set(['number'])},
  {k:'>=', l:'is greater than or equal', types: new Set(['number'])},
  {k:'<=', l:'is less than or equal', types: new Set(['number'])},
];

window.FE.valueToExpr = function({ type, value, op, amount }) {
  if (type === 'text')    return "'" + value + "'";
  if (type === 'number')  return value;
  if (type === 'bool')    return value;
  if (type === 'variable') {
    const parts = (value || '').split('.');
    const src = parts[0], key = parts.slice(1).join('.');
    if (src && key) return src + ".get('" + key + "')";
    return value || 'None';
  }
  if (type === 'math') {
    const parts = (value || '').split('.');
    const src = parts[0], key = parts.slice(1).join('.');
    if (src && key) {
      return "int(" + src + ".get('" + key + "', 0)) " + (op || '+') + " " + (amount || '0');
    }
    return value || 'None';
  }
  if (type === 'counter') {
    const parts = (value || '').split('.');
    const src = parts[0], key = parts.slice(1).join('.');
    if (src && key) {
      return "int(" + src + ".get('" + key + "', 0)) " + (op || '+') + " " + (amount || '1');
    }
    return value || 'None';
  }
  return value || 'None';
};

window.FE.exprToValue = function(expr) {
  expr = expr.trim();
  if (expr === 'True' || expr === 'False') return { type: 'bool', value: expr };
  if (/^-?\d+(\.\d+)?$/.test(expr)) return { type: 'number', value: expr };
  // Simple variable ref
  const vm = expr.match(/^(event|variables)\.get\(['"]([^'"]+)['"]\)$/);
  if (vm) return { type: 'variable', value: vm[1] + '.' + vm[2] };
  // int()-wrapped ref
  const intVm = expr.match(/^int\((event|variables)\.get\(['"]([^'"]+)['"],\s*\d+\)\)$/);
  if (intVm) return { type: 'variable', value: intVm[1] + '.' + intVm[2] };
  // Counter: int(...)+1  or int(...)-1  (default by 1)
  const counterM = expr.match(/^int\((event|variables)\.get\(['"]([^'"]+)['"],\s*\d+\)\)\s*([+\-])\s*(\d+)$/);
  if (counterM) {
    return { type: 'counter', value: counterM[1] + '.' + counterM[2], op: counterM[3], amount: counterM[4] };
  }
  // Math (any operator, any amount)
  const mathM = expr.match(/^int\((event|variables)\.get\(['"]([^'"]+)['"],\s*\d+\)\)\s*([+\-*/])\s*(-?\d+(\.\d+)?)$/);
  if (mathM) return { type: 'math', value: mathM[1] + '.' + mathM[2], op: mathM[3], amount: mathM[4] };
  // String literal
  const sm = expr.match(/^['"](.*)['"]$/);
  if (sm) return { type: 'text', value: sm[1] };
  return { type: 'expression', value: expr };
};

window.FE.buildComparisonExpr = function({ variable, operator, valueType, value, op, amount, leftOp, leftAmount }) {
  const parts = (variable || '').split('.');
  const src = parts[0], key = parts.slice(1).join('.');
  if (!src || !key) return 'True';
  const isNumComp = ['>','<','>=','<='].includes(operator);
  const isNum = valueType === 'number' || valueType === 'math' || valueType === 'counter' || isNumComp;
  let left = isNum ? "int(" + src + ".get('" + key + "', 0))" : src + ".get('" + key + "')";
  if (leftOp && leftAmount) {
    left = left + ' ' + leftOp + ' ' + leftAmount;
  }
  let right = window.FE.valueToExpr({type:valueType,value,op,amount});
  if (isNum && (valueType === 'variable' || valueType === 'math' || valueType === 'counter')) {
    const vp = (value || '').split('.');
    const vsrc = vp[0], vkey = vp.slice(1).join('.');
    if (vsrc && vkey) right = "int(" + vsrc + ".get('" + vkey + "', 0))";
  }
  switch(operator) {
    case '==': return left + ' == ' + right;
    case '!=': return left + ' != ' + right;
    case '>':  return left + ' > '  + right;
    case '<':  return left + ' < '  + right;
    case '>=': return left + ' >= ' + right;
    case '<=': return left + ' <= ' + right;
    case 'contains': return right + ' in ' + left;
    case 'is_empty': return 'not ' + src + ".get('" + key + "')";
    case 'is_not_empty': return 'bool(' + src + ".get('" + key + "'))";
    default: return 'True';
  }
};

window.FE.parseComparisonExpr = function(expr) {
  expr = expr.trim();
  const emptyM = expr.match(/^not\s+(event|variables)\.get\(['"]([^'"]+)['"]\)$/);
  if (emptyM) return { variable: emptyM[1] + '.' + emptyM[2], operator: 'is_empty', valueType: 'text', value: '' };
  const notEmptyM = expr.match(/^bool\((event|variables)\.get\(['"]([^'"]+)['"]\)\)$/);
  if (notEmptyM) return { variable: notEmptyM[1] + '.' + notEmptyM[2], operator: 'is_not_empty', valueType: 'text', value: '' };
  const leftMathM = expr.match(/^int\((event|variables)\.get\(['"]([^'"]+)['"],\s*\d+\)\)\s*([+\-*/])\s*(-?\d+(?:\.\d+)?)\s*(==|!=|>=|<=|>|<)\s*(.+)$/s);
  if (leftMathM) {
    const ev = window.FE.exprToValue(leftMathM[6]);
    return { variable: leftMathM[1] + '.' + leftMathM[2], operator: leftMathM[5], valueType: ev.type === 'number' ? 'number' : ev.type, value: ev.value, op: ev.op, amount: ev.amount, leftOp: leftMathM[3], leftAmount: leftMathM[4] };
  }
  const numM = expr.match(/^int\((event|variables)\.get\(['"]([^'"]+)['"],\s*\d+\)\)\s*(==|!=|>=|<=|>|<)\s*(.+)$/s);
  if (numM) {
    const ev = window.FE.exprToValue(numM[4]);
    return { variable: numM[1] + '.' + numM[2], operator: numM[3], valueType: ev.type === 'number' ? 'number' : ev.type, value: ev.value, op: ev.op, amount: ev.amount };
  }
  const containsM = expr.match(/^(.+)\s+in\s+(event|variables)\.get\(['"]([^'"]+)['"]\)$/);
  if (containsM) {
    const ev = window.FE.exprToValue(containsM[1]);
    return { variable: containsM[2] + '.' + containsM[3], operator: 'contains', valueType: ev.type, value: ev.value, op: ev.op, amount: ev.amount };
  }
  const RE = /^(event|variables)\.get\(['"]([^'"]+)['"]\)\s*(==|!=|>=|<=|>|<)\s*(.+)$/s;
  const m = expr.match(RE);
  if (!m) return null;
  const ev = window.FE.exprToValue(m[4]);
  return { variable: m[1] + '.' + m[2], operator: m[3], valueType: ev.type, value: ev.value, op: ev.op, amount: ev.amount };
};

window.FE.parseCompoundExpr = function(expr) {
  expr = expr.trim();
  if (!expr) return { rules: [], joiners: [] };

  const andParts = window.FE.splitByLogicalOp(expr, 'and');
  const orParts  = window.FE.splitByLogicalOp(expr, 'or');

  let parts, joiner;
  if (andParts.length > 1) { parts = andParts; joiner = 'and'; }
  else if (orParts.length > 1) { parts = orParts; joiner = 'or'; }
  else { parts = [expr]; joiner = 'and'; }

  const rules = parts.map(window.FE.parseComparisonExpr);
  if (rules.some(r => r === null)) return null;
  const joiners = [];
  for (let i = 1; i < rules.length; i++) joiners.push(joiner);
  return { rules, joiners };
};

window.FE.splitByLogicalOp = function(expr, op) {
  const parts = [];
  let depth = 0, inStr = false, strCh = null, cur = '';
  const opLen = op.length;
  for (let i = 0; i < expr.length; i++) {
    const ch = expr[i], prev = expr[i-1];
    if (inStr) {
      cur += ch;
      if (ch === strCh && prev !== '\\') inStr = false;
    } else if (ch === '"' || ch === "'") {
      cur += ch; inStr = true; strCh = ch;
    } else if ('([{'.includes(ch)) { depth++; cur += ch; }
    else if (')]}'.includes(ch)) { depth--; cur += ch; }
    else if (depth === 0 && expr.slice(i, i+opLen) === op &&
             (i===0 || !/[A-Za-z0-9_]/.test(expr[i-1])) &&
             (i+opLen>=expr.length || !/[A-Za-z0-9_]/.test(expr[i+opLen]))) {
      parts.push(cur.trim());
      cur = '';
      i += opLen - 1;
    } else {
      cur += ch;
    }
  }
  if (cur.trim()) parts.push(cur.trim());
  return parts;
};

/* ══════════════════════════════════════════════════════════════════
   PAYLOAD / TEMPLATE HELPERS
   ═════════════════════════════════════════════════════════════ */
window.FE.parsePayloadExpr = function(expr) {
  expr = expr.trim();
  let body = null;
  if (expr.startsWith('{') && expr.endsWith('}')) {
    body = expr.slice(1, -1).trim();
  } else {
    const fm = expr.match(/^f(['"])(.*)\1$/s);
    if (fm) {
      let b = fm[2];
      b = b.replace(/\\"/g, '"').replace(/\{\{/g, '{').replace(/\}\}/g, '}');
      if (b.startsWith('{') && b.endsWith('}')) body = b.slice(1, -1).trim();
    }
  }
  if (body === null) return null;
  if (!body) return [];

  const parts = [];
  let depth = 0, inStr = false, strCh = null, cur = '';
  for (let i = 0; i < body.length; i++) {
    const ch = body[i], prev = body[i-1];
    if (inStr) {
      cur += ch;
      if (ch === strCh && prev !== '\\') inStr = false;
    } else if (ch === '"' || ch === "'") {
      cur += ch; inStr = true; strCh = ch;
    } else if ('([{'.includes(ch)) { depth++; cur += ch; }
    else if (')]}'.includes(ch)) { depth--; cur += ch; }
    else if (ch === ',' && depth === 0) { parts.push(cur.trim()); cur = ''; }
    else { cur += ch; }
  }
  if (cur.trim()) parts.push(cur.trim());

  function classify(val) {
    let v = val.trim();
    if (v.startsWith('{') && v.endsWith('}')) v = v.slice(1, -1).trim();
    if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) {
      const inner = v.slice(1, -1);
      const vm = inner.match(/^(event|variables)\.get\(['"]([^'"]+)['"]\)$/);
      if (vm) return { type: 'variable', value: vm[1] + '.' + vm[2] };
      return { type: 'string', value: inner };
    }
    if (/^-?\d+(\.\d+)?$/.test(v)) return { type: 'number', value: v };
    if (v === 'True' || v === 'False') return { type: 'bool', value: v };
    const m = v.match(/^(event|variables)\.get\(['"]([^'"]+)['"]\)$/);
    if (m) return { type: 'variable', value: m[1] + '.' + m[2] };
    return { type: 'expr', value: val };
  }

  return parts.map(part => {
    const ci = part.indexOf(':');
    if (ci === -1) return null;
    const key = part.slice(0, ci).trim().replace(/^["']|["']$/g, '');
    const val = part.slice(ci + 1).trim();
    const { type, value } = classify(val);
    return { key, type, value };
  }).filter(Boolean);
};

window.FE.buildPayloadExpr = function(fields) {
  const pairs = fields.map(({key, type, value}) => {
    let expr;
    if (type === 'string') expr = "'" + value + "'";
    else if (type === 'number') expr = value;
    else if (type === 'bool') expr = value;
    else if (type === 'variable') {
      const parts = value.split('.');
      const src = parts[0], k = parts.slice(1).join('.');
      expr = src + ".get('" + k + "')";
    } else expr = value;
    return '"' + key + '": ' + expr;
  });
  return '{' + pairs.join(', ') + '}';
};

window.FE.exprToTemplate = function(expr) {
  const m = expr.match(/^f(['"])(.*)\1$/s);
  if (!m) return null;
  let body = m[2];
  body = body.replace(/\{variables\.get\(['"]([A-Za-z_][A-Za-z0-9_]*)['"]\)\}/g, '{variables.$1}');
  body = body.replace(/\{event\.get\(['"]([A-Za-z_][A-Za-z0-9_]*)['"]\)\}/g, '{event.$1}');
  const simplified = body
    .replace(/\{variables\.[A-Za-z_][A-Za-z0-9_]*\}/g, '')
    .replace(/\{event\.[A-Za-z_][A-Za-z0-9_]*\}/g, '');
  if (/\{[^{}]+\}/.test(simplified)) return null;
  body = body.replace(/\{\{/g, '\x01').replace(/\}\}/g, '\x02');
  body = body.replace(/\x01/g, '{').replace(/\x02/g, '}');
  return body;
};

window.FE.templateToExpr = function(tpl) {
  let body = tpl
    .replace(/\{variables\.([A-Za-z_][A-Za-z0-9_]*)\}/g, '\x01VAR:$1\x01')
    .replace(/\{event\.([A-Za-z_][A-Za-z0-9_]*)\}/g, '\x01EVT:$1\x01');
  body = body.replace(/\{/g, '{{').replace(/\}/g, '}}');
  body = body.replace(/\x01VAR:([^\x01]+)\x01/g, "{variables.get('$1')}");
  body = body.replace(/\x01EVT:([^\x01]+)\x01/g, "{event.get('$1')}");
  return 'f"' + body + '"';
};

/* ══════════════════════════════════════════════════════════════════
   PILL COMPOSER DOM HELPERS
   ═════════════════════════════════════════════════════════════ */
window.FE.renderSegmentsToDom = function(container, segments) {
  container.textContent = '';
  segments.forEach(seg => {
    if (seg.type === 'pill') {
      const span = document.createElement('span');
      span.setAttribute('data-pill', '1');
      span.setAttribute('data-src', seg.src);
      span.setAttribute('data-key', seg.key);
      span.style.cssText = 'display:inline-block;background:rgba(99,102,241,.18);border:1px solid rgba(99,102,241,.4);color:#818CF8;border-radius:5px;padding:1px 7px;font-size:11px;font-family:Geist Mono,monospace;margin:0 2px;vertical-align:middle;white-space:nowrap;cursor:default;';
      span.textContent = seg.src + '.' + seg.key;
      span.contentEditable = 'false';
      container.appendChild(span);
      container.appendChild(document.createTextNode('\u200B'));
    } else {
      container.appendChild(document.createTextNode(seg.text));
    }
  });
};

window.FE.htmlToSegments = function(el) {
  const segments = [];
  let curText = '';
  function flushText() {
    if (curText) { segments.push({type:'text', text: curText}); curText = ''; }
  }
  function walk(node) {
    if (node.nodeType === Node.TEXT_NODE) {
      curText += node.textContent.replace(/\u200B/g, '');
    } else if (node.nodeType === Node.ELEMENT_NODE) {
      if (node.getAttribute && node.getAttribute('data-pill')) {
        flushText();
        segments.push({type:'pill', src: node.getAttribute('data-src'), key: node.getAttribute('data-key')});
      } else {
        for (const child of node.childNodes) walk(child);
      }
    }
  }
  for (const child of el.childNodes) walk(child);
  flushText();
  const merged = [];
  segments.forEach(seg => {
    if (seg.type === 'text' && merged.length > 0 && merged[merged.length-1].type === 'text') {
      merged[merged.length-1].text += seg.text;
    } else {
      merged.push(seg);
    }
  });
  return merged;
};

window.FE.templateToSegments = function(tpl) {
  const segments = [];
  const re = /\{(event|variables)\.([A-Za-z_][A-Za-z0-9_]*)\}/g;
  let lastIdx = 0, m;
  while ((m = re.exec(tpl)) !== null) {
    if (m.index > lastIdx) segments.push({type:'text', text: tpl.slice(lastIdx, m.index)});
    segments.push({type:'pill', src: m[1], key: m[2]});
    lastIdx = m.index + m[0].length;
  }
  if (lastIdx < tpl.length) segments.push({type:'text', text: tpl.slice(lastIdx)});
  return segments;
};

window.FE.segmentsToTemplate = function(segments) {
  return segments.map(seg => {
    if (seg.type === 'pill') return '{' + seg.src + '.' + seg.key + '}';
    return seg.text;
  }).join('');
};
