// Fluentra Editor — property panels & no-code components
// Loads AFTER editor-utils.jsx and editor-shapes.jsx

const FE = window.FE;
const {
  DARK_SEL, CfgHead, AdvancedToggle,
  OP_GROUPS, ALL_OPERATORS, VALUE_TYPE_OPTS,
  discoverDiagramVariables,
  exprToValue, valueToExpr,
  buildComparisonExpr, parseCompoundExpr, parseComparisonExpr,
  parsePayloadExpr, buildPayloadExpr,
  exprToTemplate, templateToExpr,
  renderSegmentsToDom, htmlToSegments, templateToSegments, segmentsToTemplate,
  NODE_COLORS, kindToType
} = FE;

/* ── VariablePicker v2 (searchable dropdown + free text) ───── */
function VariablePicker({ value, onChange, style, extraVars, sourceFilter }) {
  let [src, key] = (value || '').split('.');
  if (!src) src = 'event';
  const [open, setOpen] = React.useState(false);
  const [filter, setFilter] = React.useState('');
  const wrapRef = React.useRef(null);

  React.useEffect(() => {
    const fn = e => { if (wrapRef.current && !wrapRef.current.contains(e.target)) setOpen(false); };
    document.addEventListener('mousedown', fn);
    return () => document.removeEventListener('mousedown', fn);
  }, []);

  const sources = sourceFilter ? [sourceFilter] : ['event','variables'];
  const baseFields = React.useMemo(() => {
    const raw = src === 'variables'
      ? [...(extraVars || []), ...FE.COMMON_VARIABLE_FIELDS]
      : FE.COMMON_EVENT_FIELDS;
    return Array.from(new Set(raw.map(f => f.trim().toLowerCase()))).sort();
  }, [src, extraVars]);
  const allFields = React.useMemo(() =>
    baseFields.filter(f => f.includes(filter.toLowerCase())),
  [baseFields, filter]);

  return (
    <div ref={wrapRef} style={{position:'relative',flex:1,...style}}>
      <div onClick={()=>setOpen(!open)}
        style={{display:'flex',alignItems:'center',gap:4,cursor:'pointer',
          padding:'5px 8px',background:'#0d0f1c',border:'1px solid rgba(255,255,255,.12)',
          borderRadius:6,fontSize:11,fontFamily:'Geist Mono,monospace',color:'#EDEFF7',
          minHeight:28}}>
        {value && key ? (
          <span style={{color:'#818CF8'}}>{src}<span style={{color:'rgba(255,255,255,.4)'}}>.</span>{key}</span>
        ) : (
          <span style={{color:'var(--ink-faint)'}}>Select variable…</span>
        )}
        <span style={{marginLeft:'auto',fontSize:9,color:'var(--ink-faint)'}}>▼</span>
      </div>
      {open && (
        <div style={{position:'absolute',top:'100%',left:0,right:0,zIndex:60,marginTop:4,
          background:'#13152B',border:'1px solid rgba(255,255,255,.15)',borderRadius:8,
          boxShadow:'0 8px 24px rgba(0,0,0,.4)',overflow:'hidden',minWidth:200}}>
          <div style={{display:'flex',borderBottom:'1px solid rgba(255,255,255,.08)'}}>
            {sources.map(s => (
              <button key={s} onClick={()=>{onChange(s + '.'); setFilter('');}}
                style={{flex:1,padding:'6px 0',fontSize:11,fontFamily:'Geist Mono,monospace',
                  background:src===s?'rgba(99,102,241,.12)':'transparent',
                  color:src===s?'#818CF8':'var(--ink-faint)',
                  border:'none',borderBottom:src===s?'2px solid #818CF8':'2px solid transparent',
                  cursor:'pointer'}}>
                {s}
              </button>
            ))}
          </div>
          <input autoFocus value={filter} onChange={e=>setFilter(e.target.value)}
            placeholder="Search or type field…"
            style={{width:'100%',padding:'6px 10px',fontSize:11,background:'transparent',
              border:'none',borderBottom:'1px solid rgba(255,255,255,.08)',color:'#EDEFF7',
              fontFamily:'Geist Mono,monospace',outline:'none'}}/>
          <div style={{maxHeight:160,overflow:'auto'}}>
            {allFields.map(f => (
              <div key={f} onClick={()=>{onChange(src + '.' + f); setOpen(false); setFilter('');}}
                style={{padding:'5px 10px',fontSize:11,fontFamily:'Geist Mono,monospace',
                  color:'#EDEFF7',cursor:'pointer',whiteSpace:'nowrap',
                  borderBottom:'1px solid rgba(255,255,255,.04)'}}
                onMouseEnter={e=>e.currentTarget.style.background='rgba(99,102,241,.10)'}
                onMouseLeave={e=>e.currentTarget.style.background='transparent'}>
                {f}
              </div>
            ))}
            {filter && !allFields.includes(filter) && (
              <div onClick={()=>{onChange(src + '.' + filter); setOpen(false); setFilter('');}}
                style={{padding:'5px 10px',fontSize:11,fontFamily:'Geist Mono,monospace',
                  color:'#34D399',cursor:'pointer',borderTop:'1px dashed rgba(255,255,255,.08)'}}>
                Use &ldquo;{filter}&rdquo;
              </div>
            )}
          </div>
        </div>
      )}
    </div>
  );
}

/* ── ValueInput v4 (text / number / bool / variable / math / counter / expression) ───── */
function ValueInput({ type, value, op, amount, onChange, placeholder, style, extraVars }) {
  const t = type || 'text';
  return (
    <div style={{display:'flex',gap:5,flex:1,alignItems:'center',...style}}>
      <select value={t} onChange={e=>onChange({type:e.target.value,value:'',op:'+',amount:'0'})}
        style={{...DARK_SEL,flex:'0 0 80px',fontSize:10.5,padding:'4px 6px'}}>
        {VALUE_TYPE_OPTS.map(o=>(<option key={o.k} value={o.k} style={{background:'#0d0f1c'}}>{o.l}</option>))}
      </select>
      {t === 'bool' ? (
        <select value={value || 'True'} onChange={e=>onChange({type,value:e.target.value})}
          style={{...DARK_SEL,flex:1,fontSize:10.5,padding:'4px 6px'}}>
          <option value="True"  style={{background:'#0d0f1c'}}>True</option>
          <option value="False" style={{background:'#0d0f1c'}}>False</option>
        </select>
      ) : t === 'variable' ? (
        <VariablePicker value={value} onChange={v=>onChange({type,value:v})} extraVars={extraVars} />
      ) : t === 'math' ? (
        <>
          <VariablePicker value={value} onChange={v=>onChange({type,value:v,op,amount})} extraVars={extraVars} />
          <select value={op||'+'} onChange={e=>onChange({type,value,op:e.target.value,amount})}
            style={{...DARK_SEL,flex:'0 0 50px',fontSize:10.5,padding:'4px 6px'}}>
            {['+','-','*','/'].map(o=>(<option key={o} value={o} style={{background:'#0d0f1c'}}>{o}</option>))}
          </select>
          <input className="input mono" type="number" value={amount||'0'}
            onChange={e=>onChange({type,value,op,amount:e.target.value})}
            placeholder="5" style={{flex:'0 0 60px',fontSize:10.5,padding:'4px 7px'}}/>
        </>
      ) : t === 'counter' ? (
        <>
          <VariablePicker value={value} onChange={v=>onChange({type,value:v,op,amount})} extraVars={extraVars} />
          <select value={op||'+'} onChange={e=>onChange({type,value,op:e.target.value,amount})}
            style={{...DARK_SEL,flex:'0 0 80px',fontSize:10.5,padding:'4px 6px'}}>
            <option value="+" style={{background:'#0d0f1c'}}>Increment</option>
            <option value="-" style={{background:'#0d0f1c'}}>Decrement</option>
          </select>
          <span style={{color:'var(--ink-faint)',fontSize:11,flexShrink:0}}>by</span>
          <input className="input mono" type="number" value={amount||'1'}
            onChange={e=>onChange({type,value,op,amount:e.target.value})}
            placeholder="1" style={{flex:'0 0 50px',fontSize:10.5,padding:'4px 7px'}}/>
        </>
      ) : t === 'expression' ? (
        <input className="input mono" value={value || ''}
          onChange={e=>onChange({type,value:e.target.value})}
          placeholder="Python expression" style={{flex:1,fontSize:10.5,padding:'4px 7px'}}/>
      ) : t === 'number' ? (
        <input className="input mono" type="number" value={value || ''}
          onChange={e=>onChange({type,value:e.target.value})}
          placeholder="42" style={{flex:1,fontSize:10.5,padding:'4px 7px'}}/>
      ) : (
        <input className="input mono" value={value || ''}
          onChange={e=>onChange({type,value:e.target.value})}
          placeholder={placeholder || 'value'} style={{flex:1,fontSize:10.5,padding:'4px 7px'}}/>
      )}
    </div>
  );
}

/* ── ComparisonRule v3 (all operators shown, disabled with tooltip) ─ */
function ComparisonRule({ rule, onChange, extraVars }) {
  const { variable, operator, valueType, value } = rule;
  const needsValue = operator !== 'is_empty' && operator !== 'is_not_empty';

  return (
    <div style={{display:'flex',flexDirection:'column',gap:6,padding:'8px 10px',
      background:'rgba(255,255,255,.03)',border:'1px solid rgba(255,255,255,.07)',borderRadius:8}}>
      <div style={{display:'flex',gap:5,alignItems:'center'}}>
        <VariablePicker value={variable} onChange={v=>onChange({variable:v})} style={{flex:1}} extraVars={extraVars} />
        <select value={operator||'=='} onChange={e=>onChange({operator:e.target.value})}
          style={{...DARK_SEL,flex:'0 0 140px',fontSize:10.5,padding:'4px 6px'}}>
          {ALL_OPERATORS.map(op=>{
            const disabled = !op.types.has(valueType||'text');
            return (
              <option key={op.k} value={op.k} disabled={disabled}
                title={disabled ? `Switch value type to ${Array.from(op.types).join('/')} to use '${op.l}'` : ''}
                style={{background:'#0d0f1c', color: disabled ? '#5B6378' : '#EDEFF7'}}>
                {op.l}
              </option>
            );
          })}
        </select>
      </div>
      {needsValue && (
        <ValueInput type={valueType} value={value} op={rule.op} amount={rule.amount}
          onChange={({type,value,op,amount})=>onChange({valueType:type,value,op,amount})} extraVars={extraVars} />
      )}
    </div>
  );
}

/* ═════════════════════════════════════════════════════════════════
   CONDITION NODE CONFIG  —  visual rule builder v2
   ═════════════════════════════════════════════════════════════ */
function ConditionEditor({ raw, selEdges, nodes, onChangeRaw }) {
  const expr = raw?.expression || 'True';
  const parsed = parseCompoundExpr(expr);
  const canBuilder = parsed !== null;
  const extraVars = React.useMemo(() => discoverDiagramVariables(nodes), [nodes]);

  const [showAdvanced, setShowAdvanced] = React.useState(false);
  const [rules, setRules] = React.useState(canBuilder ? parsed.rules : [{variable:'event.type', operator:'==', valueType:'text', value:''}]);
  const [joiners, setJoiners] = React.useState(canBuilder ? parsed.joiners : []);
  const [exprVal, setExprVal] = React.useState(expr);

  React.useEffect(() => {
    const p = parseCompoundExpr(expr);
    setRules(p ? p.rules : [{variable:'event.type', operator:'==', valueType:'text', value:''}]);
    setJoiners(p ? p.joiners : []);
    setExprVal(expr);
    setShowAdvanced(false);
  }, [expr]);

  const updateRule = (i, patch) => setRules(rs => rs.map((r, j) => j === i ? { ...r, ...patch } : r));
  const addRule = () => {
    setRules(rs => [...rs, {variable:'event.type', operator:'==', valueType:'text', value:''}]);
    setJoiners(js => [...js, 'and']);
  };
  const removeRule = i => {
    setRules(rs => rs.filter((_, j) => j !== i));
    setJoiners(js => js.filter((_, j) => j !== i - 1));
  };

  const buildExpr = () => {
    if (rules.length === 0) return 'True';
    return rules.map(buildComparisonExpr).reduce((expr, r, i) => {
      if (i === 0) return r;
      return expr + ' ' + (joiners[i-1] || 'and') + ' ' + r;
    }, '');
  };

  const apply = () => {
    const built = buildExpr();
    onChangeRaw({...(raw||{}), expression: showAdvanced ? exprVal : built});
  };

  const trueEdge  = selEdges.find(e=>e.fromPort==='true'  || e.color==='#34D399');
  const falseEdge = selEdges.find(e=>e.fromPort==='false' || e.color==='#F87171');

  return (
    <div style={{display:'flex',flexDirection:'column',gap:10}}>
      <div style={{display:'flex',alignItems:'center',justifyContent:'space-between'}}>
        <span style={{fontSize:10,fontFamily:'Geist Mono,monospace',letterSpacing:'.06em',
          textTransform:'uppercase',color:'var(--ink-faint)'}}>
          {showAdvanced ? 'Advanced Python' : 'If…'}
        </span>
        <AdvancedToggle active={showAdvanced} onClick={()=>setShowAdvanced(!showAdvanced)} color="#FBBF24"/>
      </div>

      {!canBuilder && !showAdvanced && (
        <div style={{padding:'7px 10px',background:'rgba(251,191,36,.06)',
          border:'1px solid rgba(251,191,36,.2)',borderRadius:7,
          fontSize:11,color:'#FBBF24',lineHeight:1.5}}>
          This condition uses advanced Python. Open <b>Advanced</b> to edit it, or rebuild it here with simple rules.
        </div>
      )}

      {showAdvanced ? (
        <>
          <textarea className="textarea mono" rows={4} value={exprVal}
            onChange={e=>setExprVal(e.target.value)} style={{fontSize:11.5,lineHeight:1.6}}
            placeholder={"event.get('status') == 'active'\nint(variables.get('count', 0)) > 10"}/>
          <div className="hint">
            Available: <code>event</code>, <code>variables</code> (dict-like)
          </div>
        </>
      ) : (
        <>
          {rules.map((rule, i) => (
            <div key={i} style={{display:'flex',flexDirection:'column',gap:6}}>
              {i > 0 && (
                <div style={{display:'flex',alignItems:'center',gap:10}}>
                  <div style={{flex:1,height:1,background:'rgba(255,255,255,.08)'}}/>
                  <select value={joiners[i-1]||'and'} onChange={e=>{
                    const newJoiners = [...joiners];
                    newJoiners[i-1] = e.target.value;
                    setJoiners(newJoiners);
                  }}
                    style={{...DARK_SEL,flex:'0 0 70px',fontSize:10.5,padding:'3px 6px'}}>
                    <option value="and" style={{background:'#0d0f1c'}}>AND</option>
                    <option value="or"  style={{background:'#0d0f1c'}}>OR</option>
                  </select>
                  <div style={{flex:1,height:1,background:'rgba(255,255,255,.08)'}}/>
                </div>
              )}
              <div style={{display:'flex',gap:5,alignItems:'flex-start'}}>
                <div style={{flex:1}}>
                  <ComparisonRule rule={rule} onChange={patch=>updateRule(i, patch)} extraVars={extraVars} />
                </div>
                {rules.length > 1 && (
                  <button className="icon-btn" style={{width:22,height:22,fontSize:12,marginTop:8}} aria-label="Remove rule"
                    onClick={()=>removeRule(i)}>×</button>
                )}
              </div>
            </div>
          ))}
          <button className="btn btn-ghost btn-sm" onClick={addRule}
            style={{fontSize:11,padding:'4px 0'}}>+ Add condition</button>
        </>
      )}

      <button className="btn btn-sm" onClick={apply}
        style={{width:'100%',background:'rgba(251,191,36,.1)',
          border:'1px solid rgba(251,191,36,.3)',color:'#FBBF24'}}>
        ✓ Apply
      </button>

      <CfgHead label="Routing"/>
      {[
        [trueEdge,  '#34D399','rgba(52,211,153,.05)','rgba(52,211,153,.2)','T'],
        [falseEdge, '#F87171','rgba(248,113,113,.05)','rgba(248,113,113,.2)','F'],
      ].map(([edge,col,bg,border,lbl])=> (
        <div key={lbl} style={{display:'flex',alignItems:'center',gap:8,padding:'6px 10px',
          background:bg,border:`1px solid ${border}`,borderRadius:7}}>
          <span style={{color:col,fontSize:11,fontWeight:700,flexShrink:0}}>{lbl}</span>
          <span style={{fontFamily:'Geist Mono,monospace',fontSize:11,flex:1,color:'var(--ink)'}}>
            {edge?.to || <span style={{color:'var(--ink-faint)'}}>not connected</span>}
          </span>
        </div>
      ))}
    </div>
  );
}

/* ═════════════════════════════════════════════════════════════════
   PILL COMPOSER
   ═════════════════════════════════════════════════════════════ */
function PillComposer({ template, onChange, style, insertRef }) {
  const ref = React.useRef(null);
  const savedRangeRef = React.useRef(null);
  const skipSyncRef = React.useRef(false);

  React.useEffect(() => {
    const saveSel = () => {
      const sel = window.getSelection();
      if (sel && sel.rangeCount > 0 && ref.current && ref.current.contains(sel.getRangeAt(0).commonAncestorContainer)) {
        savedRangeRef.current = sel.getRangeAt(0).cloneRange();
      }
    };
    document.addEventListener('mousedown', saveSel);
    return () => document.removeEventListener('mousedown', saveSel);
  }, []);

  React.useEffect(() => {
    if (!ref.current) return;
    if (skipSyncRef.current) {
      skipSyncRef.current = false;
      return;
    }
    const segs = templateToSegments(template || '');
    renderSegmentsToDom(ref.current, segs);
  }, [template]);

  const readAndSync = () => {
    if (!ref.current) return;
    const segs = htmlToSegments(ref.current);
    const tpl = segmentsToTemplate(segs);
    onChange(tpl);
  };

  const insertPill = React.useCallback((src, key) => {
    if (!ref.current || !key) return;
    ref.current.focus();
    const sel = window.getSelection();
    let range = null;

    if (savedRangeRef.current && ref.current.contains(savedRangeRef.current.commonAncestorContainer)) {
      range = savedRangeRef.current.cloneRange();
      sel.removeAllRanges();
      sel.addRange(range);
    } else if (sel && sel.rangeCount > 0 && ref.current.contains(sel.getRangeAt(0).commonAncestorContainer)) {
      range = sel.getRangeAt(0);
    }

    if (!range) {
      range = document.createRange();
      range.selectNodeContents(ref.current);
      range.collapse(false);
      sel.removeAllRanges();
      sel.addRange(range);
    }

    range.deleteContents();
    const span = document.createElement('span');
    span.setAttribute('data-pill', '1');
    span.setAttribute('data-src', src);
    span.setAttribute('data-key', 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 = src + '.' + key;
    span.contentEditable = 'false';
    range.insertNode(span);

    let nextNode = span.nextSibling;
    if (!nextNode || nextNode.nodeType !== Node.TEXT_NODE) {
      nextNode = document.createTextNode('\u200B');
      span.parentNode.insertBefore(nextNode, span.nextSibling);
    }
    range.setStart(nextNode, 0);
    range.setEnd(nextNode, 0);
    sel.removeAllRanges();
    sel.addRange(range);
    savedRangeRef.current = range.cloneRange();

    skipSyncRef.current = true;
    readAndSync();
  }, []);

  React.useEffect(() => {
    if (insertRef) insertRef.current = insertPill;
  }, [insertRef, insertPill]);

  const handleKeyDown = e => {
    if (e.key === 'Backspace' || e.key === 'Delete') {
      const sel = window.getSelection();
      if (sel && sel.rangeCount > 0) {
        const range = sel.getRangeAt(0);
        let node = range.startContainer;
        if (node.nodeType === Node.TEXT_NODE) node = node.parentElement;
        if (node && node.getAttribute && node.getAttribute('data-pill')) {
          e.preventDefault();
          node.remove();
          readAndSync();
          return;
        }
        if (!range.collapsed) {
          const frag = range.cloneContents();
          const pills = frag.querySelectorAll ? frag.querySelectorAll('[data-pill]') : [];
          if (pills.length > 0) {
            setTimeout(readAndSync, 0);
          }
        }
      }
    }
  };

  return (
    <div style={{position:'relative',...style}}>
      <div ref={ref} contentEditable suppressContentEditableWarning
        onBlur={readAndSync}
        onKeyDown={handleKeyDown}
        style={{minHeight:80,padding:'8px 10px',background:'#0d0f1c',border:'1px solid rgba(255,255,255,.12)',
          borderRadius:6,color:'#EDEFF7',fontSize:12.5,lineHeight:1.5,outline:'none',
          fontFamily:'Geist, sans-serif',whiteSpace:'pre-wrap',wordBreak:'break-word'}}/>
    </div>
  );
}

/* ═════════════════════════════════════════════════════════════════
   ACTION NODE CONFIG
   ═════════════════════════════════════════════════════════════ */
function ActionEditor({ raw, nodes, onChangeRaw }) {
  const extraVars = React.useMemo(() => discoverDiagramVariables(nodes), [nodes]);
  const r = raw || {};
  const [actionType, setActionType] = React.useState(r.action_type || 'log');
  const [msgExpr, setMsgExpr] = React.useState(r.message_expression || "''");

  const tpl = exprToTemplate(msgExpr);
  const canTemplate = tpl !== null;
  const [showAdvanced, setShowAdvanced] = React.useState(false);
  const [template, setTemplate] = React.useState(tpl || '');
  const [vpValue, setVpValue] = React.useState('');
  const pillInsertRef = React.useRef(null);

  React.useEffect(() => {
    const t = exprToTemplate(r.message_expression || "''");
    setActionType(r.action_type || 'log');
    setMsgExpr(r.message_expression || "''");
    setTemplate(t || '');
    setShowAdvanced(false);
  }, [r.action_type, r.message_expression]);

  const apply = () => onChangeRaw({...r, action_type: actionType,
    message_expression: showAdvanced ? msgExpr : templateToExpr(template)});

  return (
    <div style={{display:'flex',flexDirection:'column',gap:10}}>
      <CfgHead label="Action type"/>
      <select value={actionType} onChange={e=>setActionType(e.target.value)} style={{...DARK_SEL,width:'100%'}}>
        {['log','notify','custom'].map(t=>(
          <option key={t} value={t} style={{background:'#0d0f1c'}}>{t}</option>
        ))}
      </select>

      <div style={{display:'flex',alignItems:'center',justifyContent:'space-between'}}>
        <span style={{fontSize:10,fontFamily:'Geist Mono,monospace',letterSpacing:'.06em',
          textTransform:'uppercase',color:'var(--ink-faint)'}}>
          {showAdvanced ? 'Advanced Python' : 'Message'}
        </span>
        <AdvancedToggle active={showAdvanced} onClick={()=>setShowAdvanced(!showAdvanced)} color="#22D3EE"/>
      </div>

      {showAdvanced ? (
        <>
          {!canTemplate && (
            <div style={{padding:'7px 10px',background:'rgba(248,113,113,.06)',
              border:'1px solid rgba(248,113,113,.2)',borderRadius:7,
              fontSize:11,color:'#F87171',lineHeight:1.5}}>
              This message uses advanced Python. You can edit it here or switch back to the simple composer.
            </div>
          )}
          <textarea className="textarea mono" rows={3} value={msgExpr}
            onChange={e=>setMsgExpr(e.target.value)} style={{fontSize:11.5}}
            placeholder={'f"Order {variables.get(\'order_id\')} updated"'}/>
          <div className="hint">Python f-string or expression &rarr; str.</div>
        </>
      ) : (
        <>
          {!canTemplate && (
            <div style={{padding:'7px 10px',background:'rgba(248,113,113,.06)',
              border:'1px solid rgba(248,113,113,.2)',borderRadius:7,
              fontSize:11,color:'#F87171',lineHeight:1.5}}>
              This message uses advanced Python that can&rsquo;t be shown in the visual builder. Open <b>Advanced</b> to edit it, or write a new simple message here.
            </div>
          )}

          <PillComposer template={template} onChange={setTemplate} insertRef={pillInsertRef} />

          <VariablePicker
            value={vpValue}
            onChange={v => {
              setVpValue(v);
              if (!v) return;
              const dotIdx = v.indexOf('.');
              const src = dotIdx > 0 ? v.slice(0, dotIdx) : '';
              const key = dotIdx >= 0 ? v.slice(dotIdx + 1) : '';
              if (src && key && pillInsertRef.current) {
                pillInsertRef.current(src, key);
                setVpValue('');
              }
            }}
            extraVars={extraVars}
          />

          <div className="hint">Type your message and insert variables. Use {'{{'} for a literal {'{'}</div>
        </>
      )}

      <button className="btn btn-sm" onClick={apply}
        style={{width:'100%',background:'rgba(34,211,238,.08)',
          border:'1px solid rgba(34,211,238,.25)',color:'#22D3EE'}}>
        ✓ Apply
      </button>
    </div>
  );
}

/* ═════════════════════════════════════════════════════════════════
   API REQUEST NODE CONFIG
   ═════════════════════════════════════════════════════════════ */
function ApiRequestEditor({ raw, nodes, onChangeRaw }) {
  const extraVars = React.useMemo(() => discoverDiagramVariables(nodes), [nodes]);
  const r = raw || {};
  const [method,   setMethod]   = React.useState(r.method||'GET');
  const [url,      setUrl]      = React.useState(r.url_expression||'');
  const [payload,  setPayload]  = React.useState(r.payload_expression||'');
  const [respVar,  setRespVar]  = React.useState(r.response_variable||'api_response');
  const [timeout,  setTimeout_] = React.useState(r.timeout_seconds||30);
  const [headers,  setHeaders]  = React.useState(
    Object.entries(r.headers||{}).map(([k,v])=>({k,v}))
  );

  const parsed = parsePayloadExpr(payload);
  const canBuilder = parsed !== null;
  const [showAdvanced, setShowAdvanced] = React.useState(false);
  const [fields, setFields] = React.useState(parsed || []);

  React.useEffect(() => {
    const p = parsePayloadExpr(r.payload_expression || '');
    setMethod(r.method || 'GET');
    setUrl(r.url_expression || '');
    setPayload(r.payload_expression || '');
    setRespVar(r.response_variable || 'api_response');
    setTimeout_(r.timeout_seconds || 30);
    setHeaders(Object.entries(r.headers||{}).map(([k,v])=>({k,v})));
    setFields(p || []);
    setShowAdvanced(false);
  }, [r.method, r.url_expression, r.payload_expression, r.response_variable, r.timeout_seconds]);

  const addHeader = () => setHeaders(h=>[...h,{k:'',v:''}]);
  const removeHeader = i => setHeaders(h=>h.filter((_,j)=>j!==i));
  const updateHeader = (i,field,val) => setHeaders(h=>h.map((x,j)=>j===i?{...x,[field]:val}:x));

  const addField = () => setFields(f=>[...f,{key:'',type:'string',value:''}]);
  const removeField = i => setFields(f=>f.filter((_,j)=>j!==i));
  const updateField = (i, patch) => setFields(f=>f.map((x,j)=>j===i?{...x,...patch}:x));

  const apply = () => {
    const hdrs = {};
    headers.forEach(({k,v})=>{ if(k) hdrs[k]=v; });
    const finalPayload = showAdvanced ? (payload||undefined)
                        : (fields.length ? buildPayloadExpr(fields) : undefined);
    onChangeRaw({...r, method, url_expression:url, payload_expression:finalPayload,
      response_variable:respVar, timeout_seconds:+timeout, headers:hdrs});
  };

  const methodColor = {GET:'#34D399',POST:'#FBBF24',PUT:'#A78BFA',PATCH:'#67E8F9',DELETE:'#F87171'}[method]||'#22D3EE';

  return (
    <div style={{display:'flex',flexDirection:'column',gap:10}}>
      <div style={{display:'flex',gap:6}}>
        <select value={method} onChange={e=>setMethod(e.target.value)}
          style={{...DARK_SEL,flex:'0 0 80px',color:methodColor,fontWeight:700}}>
          {['GET','POST','PUT','PATCH','DELETE'].map(m=>(
            <option key={m} value={m} style={{background:'#0d0f1c',color:methodColor}}>{m}</option>
          ))}
        </select>
        <input className="input mono" value={url} onChange={e=>setUrl(e.target.value)}
          placeholder="https://api.example.com/items"
          style={{flex:1,fontSize:11,padding:'5px 8px'}}/>
      </div>

      <CfgHead label="Headers"/>
      {headers.map((h,i)=> (
        <div key={i} style={{display:'flex',gap:5,alignItems:'center'}}>
          <input className="input mono" value={h.k} onChange={e=>updateHeader(i,'k',e.target.value)}
            placeholder="Header-Name" style={{flex:1,fontSize:10.5,padding:'4px 7px'}}/>
          <span style={{color:'var(--ink-faint)',fontSize:11}}>:</span>
          <input className="input mono" value={h.v} onChange={e=>updateHeader(i,'v',e.target.value)}
            placeholder="value" style={{flex:1,fontSize:10.5,padding:'4px 7px'}}/>
          <button className="icon-btn" style={{width:18,height:18,fontSize:11,flexShrink:0}} aria-label="Remove header"
            onClick={()=>removeHeader(i)}>×</button>
        </div>
      ))}
      <button className="btn btn-ghost btn-sm" onClick={addHeader}
        style={{fontSize:11,padding:'3px 0'}}>+ Add header</button>

      <div style={{display:'flex',alignItems:'center',justifyContent:'space-between'}}>
        <span style={{fontSize:10,fontFamily:'Geist Mono,monospace',letterSpacing:'.06em',
          textTransform:'uppercase',color:'var(--ink-faint)'}}>
          {showAdvanced ? 'Advanced Python' : 'Payload (JSON)'}
        </span>
        <AdvancedToggle active={showAdvanced} onClick={()=>setShowAdvanced(!showAdvanced)} color="#34D399"/>
      </div>

      {showAdvanced ? (
        <>
          <textarea className="textarea mono" rows={3} value={payload}
            onChange={e=>setPayload(e.target.value)} style={{fontSize:11.5}}
            placeholder={"{'order_id': variables.get('order_id')}"}/>
          <div className="hint">Python expression &rarr; dict/str sent as request body.</div>
        </>
      ) : (
        <>
          {!canBuilder && (
            <div style={{padding:'7px 10px',background:'rgba(248,113,113,.06)',
              border:'1px solid rgba(248,113,113,.2)',borderRadius:7,
              fontSize:11,color:'#F87171',lineHeight:1.5}}>
              This payload uses advanced Python. Open <b>Advanced</b> to edit it, or build a new simple payload here.
            </div>
          )}
          {fields.map((f,i)=> (
            <div key={i} style={{display:'flex',gap:5,alignItems:'center'}}>
              <input className="input mono" value={f.key}
                onChange={e=>updateField(i,{key:e.target.value})}
                placeholder="key" style={{flex:'0 0 90px',fontSize:10.5,padding:'4px 7px'}}/>
              <select value={f.type} onChange={e=>updateField(i,{type:e.target.value})}
                style={{...DARK_SEL,flex:'0 0 72px',fontSize:10.5,padding:'4px 6px'}}>
                {[['string','Text'],['number','Number'],['bool','True / False'],['variable','Variable'],['expr','Expression']].map(([v,l])=>(
                  <option key={v} value={v} style={{background:'#0d0f1c'}}>{l}</option>
                ))}
              </select>
              {f.type==='bool'
                ? <select value={f.value||'True'} onChange={e=>updateField(i,{value:e.target.value})}
                    style={{...DARK_SEL,flex:1,fontSize:10.5,padding:'4px 6px'}}>
                    <option value="True"  style={{background:'#0d0f1c'}}>True</option>
                    <option value="False" style={{background:'#0d0f1c'}}>False</option>
                  </select>
                : f.type==='variable'
                  ? <VariablePicker value={f.value} onChange={v=>updateField(i,{value:v})} extraVars={extraVars} />
                  : <input className="input mono" value={f.value}
                      onChange={e=>updateField(i,{value:e.target.value})}
                      placeholder={f.type==='number'?'42':'value'}
                      style={{flex:1,fontSize:10.5,padding:'4px 7px'}}/>
              }
              <button className="icon-btn" style={{width:18,height:18,fontSize:11,flexShrink:0}} aria-label="Remove field"
                onClick={()=>removeField(i)}>×</button>
            </div>
          ))}
          <button className="btn btn-ghost btn-sm" onClick={addField}
            style={{fontSize:11,padding:'3px 0'}}>+ Add field</button>
        </>
      )}

      <div style={{display:'flex',gap:8}}>
        <div className="field" style={{flex:1,gap:4}}>
          <label>Save response as</label>
          <input className="input mono" value={respVar} onChange={e=>setRespVar(e.target.value)}
            style={{fontSize:11,padding:'5px 8px'}}/>
        </div>
        <div className="field" style={{flex:'0 0 70px',gap:4}}>
          <label>Timeout (s)</label>
          <input className="input mono" type="number" value={timeout}
            onChange={e=>setTimeout_(e.target.value)} style={{fontSize:11,padding:'5px 8px'}}/>
        </div>
      </div>

      <button className="btn btn-sm" onClick={apply}
        style={{width:'100%',background:'rgba(52,211,153,.08)',
          border:'1px solid rgba(52,211,153,.25)',color:'#34D399'}}>
        ✓ Apply
      </button>
    </div>
  );
}

/* ═════════════════════════════════════════════════════════════════
   WAIT UNTIL NODE CONFIG
   ═════════════════════════════════════════════════════════════ */
function WaitUntilEditor({ raw, nodes, onChangeRaw }) {
  const r = raw || {};
  const expr = r.match_expr || '';

  const parsed = parseCompoundExpr(expr);
  const canBuilder = parsed !== null;
  const extraVars = React.useMemo(() => discoverDiagramVariables(nodes), [nodes]);

  const [showAdvanced, setShowAdvanced] = React.useState(false);
  const [rules, setRules] = React.useState(canBuilder ? parsed.rules : [{variable:'event.type', operator:'==', valueType:'text', value:''}]);
  const [joiners, setJoiners] = React.useState(canBuilder ? parsed.joiners : []);
  const [exprVal, setExprVal] = React.useState(expr);

  React.useEffect(() => {
    const p = parseCompoundExpr(expr);
    setRules(p ? p.rules : [{variable:'event.type', operator:'==', valueType:'text', value:''}]);
    setJoiners(p ? p.joiners : []);
    setExprVal(expr);
    setShowAdvanced(false);
  }, [expr]);

  const updateRule = (i, patch) => setRules(rs => rs.map((r, j) => j === i ? { ...r, ...patch } : r));
  const addRule = () => {
    setRules(rs => [...rs, {variable:'event.type', operator:'==', valueType:'text', value:''}]);
    setJoiners(js => [...js, 'and']);
  };
  const removeRule = i => {
    setRules(rs => rs.filter((_, j) => j !== i));
    setJoiners(js => js.filter((_, j) => j !== i - 1));
  };

  const buildExpr = () => {
    if (rules.length === 0) return 'True';
    return rules.map(buildComparisonExpr).reduce((expr, r, i) => {
      if (i === 0) return r;
      return expr + ' ' + (joiners[i-1] || 'and') + ' ' + r;
    }, '');
  };

  const apply = () => onChangeRaw({...r, match_expr: showAdvanced ? exprVal : buildExpr()});

  return (
    <div style={{display:'flex',flexDirection:'column',gap:10}}>
      <div className="hint">The instance waits until an incoming event matches these rules.</div>

      <div style={{display:'flex',alignItems:'center',justifyContent:'space-between'}}>
        <span style={{fontSize:10,fontFamily:'Geist Mono,monospace',letterSpacing:'.06em',
          textTransform:'uppercase',color:'var(--ink-faint)'}}>
          {showAdvanced ? 'Advanced Python' : 'Match rules'}
        </span>
        <AdvancedToggle active={showAdvanced} onClick={()=>setShowAdvanced(!showAdvanced)} color="#A78BFA"/>
      </div>

      {!canBuilder && !showAdvanced && (
        <div style={{padding:'7px 10px',background:'rgba(167,139,250,.06)',
          border:'1px solid rgba(167,139,250,.2)',borderRadius:7,
          fontSize:11,color:'#A78BFA',lineHeight:1.5}}>
          This matcher uses advanced Python. Open <b>Advanced</b> to edit it, or rebuild it here with simple rules.
        </div>
      )}

      {showAdvanced ? (
        <>
          <textarea className="textarea mono" rows={4} value={exprVal}
            onChange={e=>setExprVal(e.target.value)} style={{fontSize:11.5,lineHeight:1.6}}
            placeholder={"event.get('type') == 'payment_received'\nint(variables.get('amount', 0)) > 0"}/>
          <div className="hint">Evaluated against each incoming event. Instance resumes when this returns True.</div>
        </>
      ) : (
        <>
          {rules.map((rule, i) => (
            <div key={i} style={{display:'flex',flexDirection:'column',gap:6}}>
              {i > 0 && (
                <div style={{display:'flex',alignItems:'center',gap:10}}>
                  <div style={{flex:1,height:1,background:'rgba(255,255,255,.08)'}}/>
                  <select value={joiners[i-1]||'and'} onChange={e=>{
                    const newJoiners = [...joiners];
                    newJoiners[i-1] = e.target.value;
                    setJoiners(newJoiners);
                  }}
                    style={{...DARK_SEL,flex:'0 0 70px',fontSize:10.5,padding:'3px 6px'}}>
                    <option value="and" style={{background:'#0d0f1c'}}>AND</option>
                    <option value="or"  style={{background:'#0d0f1c'}}>OR</option>
                  </select>
                  <div style={{flex:1,height:1,background:'rgba(255,255,255,.08)'}}/>
                </div>
              )}
              <div style={{display:'flex',gap:5,alignItems:'flex-start'}}>
                <div style={{flex:1}}>
                  <ComparisonRule rule={rule} onChange={patch=>updateRule(i, patch)} extraVars={extraVars} />
                </div>
                {rules.length > 1 && (
                  <button className="icon-btn" style={{width:22,height:22,fontSize:12,marginTop:8}} aria-label="Remove rule"
                    onClick={()=>removeRule(i)}>×</button>
                )}
              </div>
            </div>
          ))}
          <button className="btn btn-ghost btn-sm" onClick={addRule}
            style={{fontSize:11,padding:'4px 0'}}>+ Add rule</button>
        </>
      )}

      <button className="btn btn-sm" onClick={apply}
        style={{width:'100%',background:'rgba(167,139,250,.08)',
          border:'1px solid rgba(167,139,250,.3)',color:'#A78BFA'}}>
        ✓ Apply
      </button>
    </div>
  );
}

/* ═════════════════════════════════════════════════════════════════
   VARIABLE UPDATE NODE CONFIG
   ═════════════════════════════════════════════════════════════ */
function VariableUpdateEditor({ raw, nodes, onChangeRaw }) {
  const r = raw || {};
  const extraVars = React.useMemo(() => discoverDiagramVariables(nodes), [nodes]);

  const existing = Object.entries(r.updates||{}).map(([k,v])=>{
    const { type, value } = exprToValue(v);
    return { k, type, value };
  });
  const [rows, setRows] = React.useState(existing.length ? existing : [{k:'',type:'text',value:''}]);

  React.useEffect(() => {
    const ex = Object.entries(r.updates||{}).map(([k,v])=>{
      const { type, value } = exprToValue(v);
      return { k, type, value };
    });
    setRows(ex.length ? ex : [{k:'',type:'text',value:''}]);
  }, [r.updates]);

  const addRow = () => setRows(rs=>[...rs,{k:'',type:'text',value:''}]);
  const removeRow = i => setRows(rs=>rs.filter((_,j)=>j!==i));
  const updateRow = (i,patch) => setRows(rs=>rs.map((x,j)=>j===i?{...x,...patch}:x));

  const apply = () => {
    const updates = {};
    rows.forEach(({k,type,value})=>{
      if(k.trim()) updates[k.trim()] = valueToExpr({type,value});
    });
    onChangeRaw({...r, updates});
  };

  return (
    <div style={{display:'flex',flexDirection:'column',gap:8}}>
      <CfgHead label="Set Variables"/>
      <div className="hint">Set or update variables for use later in the workflow.</div>
      {rows.map((row,i)=> (
        <div key={i} style={{display:'flex',flexDirection:'column',gap:5,
          padding:'8px 10px',background:'rgba(255,255,255,.03)',
          border:'1px solid rgba(255,255,255,.07)',borderRadius:8}}>
          <div style={{display:'flex',gap:5,alignItems:'center'}}>
            <input className="input mono" value={row.k}
              onChange={e=>updateRow(i,{k:e.target.value})}
              placeholder="variable_name" style={{flex:'0 0 120px',fontSize:10.5,padding:'4px 7px'}}/>
            <span style={{color:'var(--ink-faint)',fontSize:12,fontFamily:'Geist Mono,monospace'}}>=</span>
            <div style={{flex:1}}>
              <ValueInput type={row.type} value={row.value} op={row.op} amount={row.amount}
                onChange={({type,value,op,amount})=>updateRow(i,{type,value,op,amount})} extraVars={extraVars} />
            </div>
            <button className="icon-btn" style={{width:18,height:18,fontSize:11}} aria-label="Remove row"
              onClick={()=>removeRow(i)}>×</button>
          </div>
        </div>
      ))}
      <button className="btn btn-ghost btn-sm" onClick={addRow}
        style={{fontSize:11,padding:'3px 0'}}>+ Add variable</button>
      <button className="btn btn-sm" onClick={apply}
        style={{width:'100%',marginTop:4,background:'rgba(167,139,250,.08)',
          border:'1px solid rgba(167,139,250,.3)',color:'#A78BFA'}}>
        ✓ Apply
      </button>
    </div>
  );
}

/* ═════════════════════════════════════════════════════════════════
   SLEEP NODE CONFIG
   ═════════════════════════════════════════════════════════════ */
function SleepEditor({ raw, onChangeRaw }) {
  const r = raw || {};
  const [dur, setDur] = React.useState(String(r.duration_seconds||'5'));
  const [showAdvanced, setShowAdvanced] = React.useState(false);
  const [exprVal, setExprVal] = React.useState(String(r.duration_seconds||'5'));

  React.useEffect(() => {
    const d = String(r.duration_seconds || '5');
    setDur(d);
    setExprVal(d);
    setShowAdvanced(false);
  }, [r.duration_seconds]);

  const apply = () => onChangeRaw({...r, duration_seconds: showAdvanced ? exprVal : dur});
  const seconds = parseFloat(dur);
  const pretty = isNaN(seconds) ? '' : seconds<60 ? seconds+'s'
    : seconds<3600 ? (seconds/60).toFixed(1)+'m' : (seconds/3600).toFixed(2)+'h';

  return (
    <div style={{display:'flex',flexDirection:'column',gap:10}}>
      <div style={{display:'flex',alignItems:'center',justifyContent:'space-between'}}>
        <span style={{fontSize:10,fontFamily:'Geist Mono,monospace',letterSpacing:'.06em',
          textTransform:'uppercase',color:'var(--ink-faint)'}}>
          {showAdvanced ? 'Advanced Python' : 'Duration'}
        </span>
        <AdvancedToggle active={showAdvanced} onClick={()=>setShowAdvanced(!showAdvanced)} color="#94A3B8"/>
      </div>

      {showAdvanced ? (
        <>
          <input className="input mono" value={exprVal} onChange={e=>setExprVal(e.target.value)}
            placeholder="seconds or expression" style={{flex:1,fontSize:12,padding:'6px 10px'}}/>
          <div className="hint">Python expression, e.g. <code>variables.get('delay', 30)</code></div>
        </>
      ) : (
        <>
          <div style={{display:'flex',gap:8,alignItems:'center'}}>
            <input className="input mono" type="number" value={dur} onChange={e=>setDur(e.target.value)}
              placeholder="seconds" style={{flex:1,fontSize:12,padding:'6px 10px'}}/>
            {pretty && <span style={{fontFamily:'Geist Mono,monospace',fontSize:11,color:'var(--ink-faint)',flexShrink:0}}>{pretty}</span>}
          </div>
          <div className="hint">Seconds to pause execution.</div>
        </>
      )}

      <button className="btn btn-sm" onClick={apply}
        style={{width:'100%',background:'rgba(148,163,184,.08)',
          border:'1px solid rgba(148,163,184,.25)',color:'#94A3B8'}}>
        ✓ Apply
      </button>
    </div>
  );
}

/* ─────────────────────────────────────────────────────────────────
   FORK NODE CONFIG
   ───────────────────────────────────────────────────────────── */
function ForkEditor({ raw, nodes, onChangeRaw }) {
  const r = raw || {};
  const [joinId, setJoinId] = React.useState(r.join_node_id || '');
  const joinNodes = nodes.filter(n => n.kind === 'join');
  const apply = () => onChangeRaw({...r, join_node_id: joinId});
  return (
    <div style={{display:'flex',flexDirection:'column',gap:10}}>
      <div style={{padding:'8px 10px',background:'rgba(253,230,138,.05)',
        border:'1px solid rgba(253,230,138,.15)',borderRadius:8,
        fontSize:11,fontFamily:'Geist Mono,monospace',color:'var(--ink-faint)',lineHeight:1.6}}>
        Fork splits execution into parallel branches — one per outgoing connection.
        Each branch runs independently until it reaches the paired <b style={{color:'#FDE68A'}}>Join</b> node.
      </div>
      <CfgHead label="Paired Join node"/>
      {joinNodes.length > 0
        ? <select value={joinId} onChange={e=>setJoinId(e.target.value)} style={{...DARK_SEL,width:'100%'}}>
            <option value="" style={{background:'#0d0f1c'}}>— select —</option>
            {joinNodes.map(n=>(
              <option key={n.id} value={n.id} style={{background:'#0d0f1c'}}>{n.name||n.id}</option>
            ))}
          </select>
        : <div style={{fontSize:11.5,color:'var(--ink-faint)',fontFamily:'Geist Mono,monospace'}}>
            No Join nodes in diagram yet — add one first.
          </div>
      }
      <div className="hint">The Join node where all branches must arrive before execution continues.</div>
      <button className="btn btn-sm" onClick={apply}
        style={{width:'100%',background:'rgba(253,230,138,.08)',
          border:'1px solid rgba(253,230,138,.3)',color:'#FDE68A'}}>
        ✓ Apply
      </button>
    </div>
  );
}

/* ─────────────────────────────────────────────────────────────────
   JOIN NODE CONFIG
   ───────────────────────────────────────────────────────────── */
function JoinEditor({ raw, nodes, onChangeRaw }) {
  const r = raw || {};
  const [forkId, setForkId] = React.useState(r.fork_node_id || '');
  const forkNodes = nodes.filter(n => n.kind === 'fork');
  const apply = () => onChangeRaw({...r, fork_node_id: forkId});
  return (
    <div style={{display:'flex',flexDirection:'column',gap:10}}>
      <div style={{padding:'8px 10px',background:'rgba(103,232,249,.05)',
        border:'1px solid rgba(103,232,249,.15)',borderRadius:8,
        fontSize:11,fontFamily:'Geist Mono,monospace',color:'var(--ink-faint)',lineHeight:1.6}}>
        Join waits for <em>all</em> branches of the paired Fork to complete before continuing.
      </div>
      <CfgHead label="Paired Fork node"/>
      {forkNodes.length > 0
        ? <select value={forkId} onChange={e=>setForkId(e.target.value)} style={{...DARK_SEL,width:'100%'}}>
            <option value="" style={{background:'#0d0f1c'}}>— select —</option>
            {forkNodes.map(n=>(
              <option key={n.id} value={n.id} style={{background:'#0d0f1c'}}>{n.name||n.id}</option>
            ))}
          </select>
        : <div style={{fontSize:11.5,color:'var(--ink-faint)',fontFamily:'Geist Mono,monospace'}}>
            No Fork nodes in diagram yet — add one first.
          </div>
      }
      <div className="hint">Must match the Fork node whose branches lead to this Join.</div>
      <button className="btn btn-sm" onClick={apply}
        style={{width:'100%',background:'rgba(103,232,249,.08)',
          border:'1px solid rgba(103,232,249,.3)',color:'#67E8F9'}}>
        ✓ Apply
      </button>
    </div>
  );
}

/* ─────────────────────────────────────────────────────────────────
   MAPPING ROWS
   ───────────────────────────────────────────────────────────── */
function MappingRows({ rows, onUpd, onRm, onAdd, leftLabel, rightLabel }) {
  return (
    <div style={{display:'flex',flexDirection:'column',gap:5}}>
      <div style={{display:'flex',gap:5,fontSize:9.5,fontFamily:'Geist Mono,monospace',
        color:'var(--ink-faint)',padding:'0 2px'}}>
        <span style={{flex:1}}>{leftLabel}</span>
        <span style={{flex:1}}>{rightLabel}</span>
        <span style={{width:18}}/>
      </div>
      {rows.map((row,i)=> (
        <div key={i} style={{display:'flex',gap:5,alignItems:'center'}}>
          <input className="input mono" value={row.k} onChange={e=>onUpd(i,'k',e.target.value)}
            placeholder="variable" style={{flex:1,fontSize:10.5,padding:'4px 7px'}}/>
          <span style={{color:'var(--ink-faint)',fontSize:10,flexShrink:0}}>→</span>
          <input className="input mono" value={row.v} onChange={e=>onUpd(i,'v',e.target.value)}
            placeholder="expression" style={{flex:1,fontSize:10.5,padding:'4px 7px'}}/>
          <button className="icon-btn" style={{width:18,height:18,fontSize:11}} aria-label="Remove row"
            onClick={()=>onRm(i)}>×</button>
        </div>
      ))}
      <button className="btn btn-ghost btn-sm" onClick={onAdd}
        style={{fontSize:11,padding:'3px 0'}}>+ Add</button>
    </div>
  );
}

/* ─────────────────────────────────────────────────────────────────
   SUBFLOW NODE CONFIG
   ───────────────────────────────────────────────────────────── */
function SubFlowEditor({ raw, diagramList, onChangeRaw }) {
  const r = raw || {};
  const [diagId,    setDiagId]    = React.useState(r.diagram_id || '');
  const [inputMap,  setInputMap]  = React.useState(
    Object.entries(r.input_variables_mapping  ||{}).map(([k,v])=>({k,v}))
  );
  const [outputMap, setOutputMap] = React.useState(
    Object.entries(r.output_variables_mapping ||{}).map(([k,v])=>({k,v}))
  );

  const addIn  = () => setInputMap(m=>[...m,{k:'',v:''}]);
  const addOut = () => setOutputMap(m=>[...m,{k:'',v:''}]);
  const rmIn   = i  => setInputMap(m=>m.filter((_,j)=>j!==i));
  const rmOut  = i  => setOutputMap(m=>m.filter((_,j)=>j!==i));
  const updIn  = (i,f,v) => setInputMap(m=>m.map((x,j)=>j===i?{...x,[f]:v}:x));
  const updOut = (i,f,v) => setOutputMap(m=>m.map((x,j)=>j===i?{...x,[f]:v}:x));

  const apply = () => {
    const inMap={}, outMap={};
    inputMap.forEach(({k,v})=>{if(k.trim())inMap[k.trim()]=v;});
    outputMap.forEach(({k,v})=>{if(k.trim())outMap[k.trim()]=v;});
    onChangeRaw({...r, diagram_id:diagId,
      input_variables_mapping:inMap, output_variables_mapping:outMap});
  };

  return (
    <div style={{display:'flex',flexDirection:'column',gap:10}}>
      <CfgHead label="Sub-diagram"/>
      {diagramList && diagramList.length > 0
        ? <select value={diagId} onChange={e=>setDiagId(e.target.value)} style={{...DARK_SEL,width:'100%'}}>
            <option value="" style={{background:'#0d0f1c'}}>— select diagram —</option>
            {diagramList.map(d=>(
              <option key={d} value={d} style={{background:'#0d0f1c'}}>{d}</option>
            ))}
          </select>
        : <input className="input mono" value={diagId} onChange={e=>setDiagId(e.target.value)}
            placeholder="diagram_name" style={{fontSize:11,padding:'5px 8px'}}/>
      }
      <div className="hint">The diagram that will be called as a sub-flow.</div>

      <CfgHead label="Input mapping (parent → child)"/>
      <MappingRows rows={inputMap} onUpd={updIn} onRm={rmIn} onAdd={addIn}
        leftLabel="parent variable" rightLabel="child variable / expr"/>

      <CfgHead label="Output mapping (child → parent)"/>
      <MappingRows rows={outputMap} onUpd={updOut} onRm={rmOut} onAdd={addOut}
        leftLabel="child variable" rightLabel="parent variable / expr"/>

      <button className="btn btn-sm" onClick={apply}
        style={{width:'100%',marginTop:2,background:'rgba(52,211,153,.08)',
          border:'1px solid rgba(52,211,153,.25)',color:'#34D399'}}>
        ✓ Apply
      </button>
    </div>
  );
}

/* ─────────────────────────────────────────────────────────────────
   START / END / WEBHOOK — informational panels
   ───────────────────────────────────────────────────────────── */
function StartEndInfo({ kind }) {
  const info = {
    start: { color:'#34D399', text:"The Start node is the entry point of the diagram. No configuration needed — it triggers when a new event matches the diagram's acceptance criteria." },
    end:   { color:'#F87171', text:'The End node terminates the instance. No configuration needed — reaching it marks the instance as completed.' },
    webhook: { color:'#22D3EE', text:'Webhook node receives external HTTP events. Configure the path segment in the Tenant Event Endpoints page, then route events here.' },
  }[kind] || { color:'#6366F1', text:'No additional configuration for this node type.' };
  return (
    <div style={{padding:'10px 12px',borderRadius:8,background:'rgba(255,255,255,.03)',
      border:`1px solid ${info.color}22`,fontSize:11.5,
      fontFamily:'Geist Mono,monospace',color:'var(--ink-faint)',lineHeight:1.7}}>
      <span style={{color:info.color,fontWeight:600}}>ℹ </span>{info.text}
    </div>
  );
}

/* ── Dispatcher ── */
function NodeConfigPanel({ node, selEdges, nodes, diagramList, onChangeRaw }) {
  switch (node.kind) {
    case 'cond':    return <ConditionEditor     raw={node._raw} selEdges={selEdges} nodes={nodes} onChangeRaw={onChangeRaw}/>;
    case 'action':  return <ActionEditor         raw={node._raw} nodes={nodes} onChangeRaw={onChangeRaw}/>;
    case 'api':     return <ApiRequestEditor     raw={node._raw} nodes={nodes} onChangeRaw={onChangeRaw}/>;
    case 'wait':    return <WaitUntilEditor      raw={node._raw} nodes={nodes} onChangeRaw={onChangeRaw}/>;
    case 'varup':   return <VariableUpdateEditor  raw={node._raw} nodes={nodes} onChangeRaw={onChangeRaw}/>;
    case 'sleep':   return <SleepEditor           raw={node._raw} onChangeRaw={onChangeRaw}/>;
    case 'fork':    return <ForkEditor            raw={node._raw} nodes={nodes} onChangeRaw={onChangeRaw}/>;
    case 'join':    return <JoinEditor            raw={node._raw} nodes={nodes} onChangeRaw={onChangeRaw}/>;
    case 'sub':     return <SubFlowEditor         raw={node._raw} diagramList={diagramList} onChangeRaw={onChangeRaw}/>;
    case 'start':   return <StartEndInfo kind="start"/>;
    case 'end':     return <StartEndInfo kind="end"/>;
    case 'webhook': return <StartEndInfo kind="webhook"/>;
    default:        return null;
  }
}

// Export panel components to window.FE so app-flow.jsx can reference them if needed
window.FE.VariablePicker = VariablePicker;
window.FE.ValueInput = ValueInput;
window.FE.ComparisonRule = ComparisonRule;
window.FE.PillComposer = PillComposer;
window.FE.ConditionEditor = ConditionEditor;
window.FE.ActionEditor = ActionEditor;
window.FE.ApiRequestEditor = ApiRequestEditor;
window.FE.WaitUntilEditor = WaitUntilEditor;
window.FE.VariableUpdateEditor = VariableUpdateEditor;
window.FE.SleepEditor = SleepEditor;
window.FE.ForkEditor = ForkEditor;
window.FE.JoinEditor = JoinEditor;
window.FE.MappingRows = MappingRows;
window.FE.SubFlowEditor = SubFlowEditor;
window.FE.StartEndInfo = StartEndInfo;
window.FE.NodeConfigPanel = NodeConfigPanel;
