// Fluentra Editor — visual canvas components
// Loads AFTER editor-utils.jsx

const {
  NODE_COLORS, getNodePorts, getNodeH, EC_NW, EC_NH,
  COND_W, COND_H, mapNodeTypeToKind, kindToType
} = window.FE;

/* ══════════════════════════════════════════════════════════════════
   NODE GLYPH
   ═════════════════════════════════════════════════════════════ */
function NodeGlyph({ kind, size = 12 }) {
  switch (kind) {
    case "start":   return <span style={{fontSize:size}}>▶</span>;
    case "end":     return <span style={{fontSize:size+1}}>■</span>;
    case "cond":    return <span style={{fontSize:size+2,fontFamily:"Geist Mono, monospace"}}>?</span>;
    case "action":
    case "api":     return <span style={{fontSize:size+1}}>⚡</span>;
    case "webhook": return <span style={{fontSize:size+1}}>⚡</span>;
    case "wait":    return <span style={{fontSize:size+1}}>⏱</span>;
    case "varup":   return <span style={{fontSize:size+1,fontFamily:"Geist Mono, monospace"}}>x</span>;
    case "sleep":   return <span style={{fontSize:size+1}}>z</span>;
    case "fork":    return <span style={{fontSize:size+2}}>⑂</span>;
    case "join":    return <span style={{fontSize:size+2}}>⑃</span>;
    case "sub":     return <span style={{fontSize:size+2}}>≡</span>;
  }
  return null;
}

/* ══════════════════════════════════════════════════════════════════
   FLOW NODE (SVG card — read-only)
   ═════════════════════════════════════════════════════════════ */
function FlowNode({ x, y, w=124, h=50, kind, name, sub, active=false, dim=false, selected=false }) {
  const c = NODE_COLORS[kind] || NODE_COLORS.action;
  const r = 10;
  return (
    <g transform={`translate(${x} ${y})`} style={{opacity:dim?0.45:1,transition:"opacity .3s"}}>
      {(active||selected) && (
        <rect x="-4" y="-4" width={w+8} height={h+8} rx={r+3}
          fill="none" stroke={c.ring} strokeOpacity={active?0.55:0.35} strokeWidth="1"
          style={{filter:`drop-shadow(0 0 12px ${c.glow})`}}/>
      )}
      <rect width={w} height={h} rx={r} fill={c.bg} stroke={active?c.ring:"rgba(255,255,255,.10)"} strokeWidth={active?1.4:1}/>
      <rect x="0" y="0" width="3" height={h} rx={2}
        fill={c.ring} style={{filter:active?`drop-shadow(0 0 6px ${c.ring})`:"none"}}/>
      <g transform={`translate(${14} ${h/2})`}>
        <circle r="11" fill={c.bg} stroke={c.ring} strokeOpacity=".55"/>
        <foreignObject x="-9" y="-9" width="18" height="18" style={{textAlign:"center",color:c.ring,lineHeight:"18px"}}>
          <div xmlns="http://www.w3.org/1999/xhtml" style={{color:c.ring,fontWeight:600,fontSize:11,textAlign:"center",lineHeight:"18px"}}>
            <NodeGlyph kind={kind}/>
          </div>
        </foreignObject>
      </g>
      <text x={34} y={h/2-5} fill="#EDEFF7" fontSize="11.5" fontWeight="600" fontFamily="Geist, sans-serif" letterSpacing="-.01em">
        {name && name.length > 18 ? name.slice(0,18)+'…' : (name||kind)}
      </text>
      {sub && <text x={34} y={h/2+9} fill="#5B6378" fontSize="10" fontFamily="Geist Mono, monospace">
        {sub.length > 20 ? sub.slice(0,20)+'…' : sub}
      </text>}
    </g>
  );
}

/* ══════════════════════════════════════════════════════════════════
   FLOW EDGE (bezier connector)
   ═════════════════════════════════════════════════════════════ */
function FlowEdge({ a, b, sx, sy, ex, ey, color="#6366F1", animated=true, dotted=false, glow=false }) {
  const x1 = sx !== undefined ? sx : (a ? a.x+a.w : 0);
  const y1 = sy !== undefined ? sy : (a ? a.y+a.h/2 : 0);
  const x2 = ex !== undefined ? ex : (b ? b.x : 0);
  const y2 = ey !== undefined ? ey : (b ? b.y+b.h/2 : 0);

  let d;
  if (x2 < x1 - 40) {
    const dx = x1 - x2;
    const dy = y2 - y1;
    if (dx > 300) {
      const arc   = Math.max(40, dx * 0.08) * (dy < 0 ? 1 : -1);
      const midX  = (x1 + x2) / 2;
      d = `M ${x1} ${y1} Q ${x1+50} ${y1+arc} ${midX} ${y1+arc} Q ${x2-50} ${y1+arc} ${x2} ${y2}`;
    } else {
      const hExit  = Math.min(54, dy * 0.18);
      const vEntry = Math.max(32, dy * 0.28);
      d = `M ${x1} ${y1} C ${x1+hExit} ${y1}, ${x2} ${y2-vEntry}, ${x2} ${y2}`;
    }
  } else {
    const dx = Math.abs(x2-x1);
    const c1x=x1+Math.max(20,dx*.45), c1y=y1;
    const c2x=x2-Math.max(20,dx*.45), c2y=y2;
    d = `M ${x1} ${y1} C ${c1x} ${c1y}, ${c2x} ${c2y}, ${x2} ${y2}`;
  }

  return (
    <g>
      {glow && <path d={d} stroke={color} strokeOpacity=".25" strokeWidth="4" fill="none"
        style={{filter:`drop-shadow(0 0 6px ${color})`}}/>}
      <path d={d} stroke={color} strokeWidth="1.4" fill="none"
        strokeDasharray={animated?"5 5":dotted?"2 4":"0"}
        strokeLinecap="round">
        {animated && <animate attributeName="stroke-dashoffset" from="20" to="0" dur="1.4s" repeatCount="indefinite"/>}
      </path>
    </g>
  );
}

/* ══════════════════════════════════════════════════════════════════
   FLOW DIAGRAM (read-only visualization card)
   ═════════════════════════════════════════════════════════════ */
function FlowDiagram({ title="workflow", instanceId="", meta="", diagramRef="", status="running", nodes, edges, height=460 }) {
  const statusDot = {running:"#22D3EE",waiting:"#FBBF24",completed:"#34D399",failed:"#F87171"}[status]||"#22D3EE";

  const svgRef  = React.useRef(null);
  const dragRef = React.useRef(null);
  const vtRef   = React.useRef({x:60,y:60,s:1});
  const [vt, setVt] = React.useState({x:60,y:60,s:1});

  const fitView = React.useCallback(() => {
    if (!nodes.length || !svgRef.current) return;
    const pad = 56;
    const xs = nodes.flatMap(n => { const p = getNodePorts(n,edges); return [n.x, n.x+p.w]; });
    const ys = nodes.flatMap(n => { const p = getNodePorts(n,edges); return [n.y, n.y+p.h]; });
    const bx = Math.min(...xs)-pad, by = Math.min(...ys)-pad;
    const bw = Math.max(...xs)-bx+pad, bh = Math.max(...ys)-by+pad;
    const cw = svgRef.current.clientWidth  || 640;
    const ch = svgRef.current.clientHeight || 400;
    const s  = Math.min(cw/bw, ch/bh, 1.4);
    const nv = { x:(cw-bw*s)/2 - bx*s, y:(ch-bh*s)/2 - by*s, s };
    vtRef.current = nv; setVt(nv);
  }, [nodes, edges]);

  React.useEffect(() => { fitView(); }, [nodes.length]);

  React.useEffect(() => {
    const el = svgRef.current; if (!el) return;
    const fn = e => {
      e.preventDefault();
      const rect = el.getBoundingClientRect();
      const mx = e.clientX-rect.left, my = e.clientY-rect.top;
      const f = e.deltaY < 0 ? 1.12 : 1/1.12;
      setVt(v => {
        const ns = Math.max(0.08, Math.min(8, v.s*f)), r = ns/v.s;
        const nv = { x:mx-(mx-v.x)*r, y:my-(my-v.y)*r, s:ns };
        vtRef.current = nv; return nv;
      });
    };
    el.addEventListener('wheel', fn, {passive:false});
    return () => el.removeEventListener('wheel', fn);
  }, []);

  const onPointerDown = e => {
    if (e.button!==0) return;
    e.currentTarget.setPointerCapture(e.pointerId);
    dragRef.current = {px:e.clientX, py:e.clientY, vx:vtRef.current.x, vy:vtRef.current.y};
  };
  const onPointerMove = e => {
    if (!dragRef.current) return;
    const nv = {...vtRef.current, x:dragRef.current.vx+(e.clientX-dragRef.current.px), y:dragRef.current.vy+(e.clientY-dragRef.current.py)};
    vtRef.current = nv; setVt(nv);
  };
  const onPointerUp = () => { dragRef.current = null; };

  const doZoom = d => {
    const el = svgRef.current; if (!el) return;
    const cx = el.clientWidth/2, cy = el.clientHeight/2;
    setVt(v => {
      const ns = Math.max(0.08, Math.min(8, v.s*d)), r = ns/v.s;
      const nv = {x:cx-(cx-v.x)*r, y:cy-(cy-v.y)*r, s:ns};
      vtRef.current = nv; return nv;
    });
  };

  const ps  = Math.max(4, 18*vt.s);
  const dpx = ((vt.x%ps)+ps)%ps;
  const dpy = ((vt.y%ps)+ps)%ps;
  const dpr = Math.max(0.5, Math.min(1.5, 0.85*vt.s));

  const byId = Object.fromEntries(nodes.map(n=>[n.id,n]));

  return (
    <div style={{position:'relative',width:'100%',height,overflow:'hidden',
      background:'#070912',borderRadius:14,border:'1px solid rgba(255,255,255,.08)'}}>

      <svg ref={svgRef} width="100%" height="100%"
        style={{display:'block',userSelect:'none',touchAction:'none',cursor:'grab'}}
        onPointerDown={onPointerDown} onPointerMove={onPointerMove}
        onPointerUp={onPointerUp} onPointerLeave={onPointerUp}>

        <defs>
          <pattern id="fd-dot" x={dpx} y={dpy} width={ps} height={ps} patternUnits="userSpaceOnUse">
            <circle cx={dpr} cy={dpr} r={dpr} fill="rgba(255,255,255,0.055)"/>
          </pattern>
        </defs>
        <rect width="100%" height="100%" fill="url(#fd-dot)"/>

        {nodes.length===0 && (
          <text x="50%" y="50%" textAnchor="middle"
            fill="rgba(255,255,255,.18)" fontFamily="Geist Mono,monospace" fontSize="13">
            No diagram data
          </text>
        )}

        <g transform={`translate(${vt.x},${vt.y}) scale(${vt.s})`}>

          {/* ── Edges — port-aware, same as editor ─────────────── */}
          {edges.map((e,i) => {
            const a=byId[e.from], b=byId[e.to];
            if (!a||!b) return null;
            const ap = getNodePorts(a, edges), bp = getNodePorts(b, edges);
            let fid = e.fromPort;
            if (!fid && a.kind==='cond') fid = e.color==='#F87171' ? 'false' : 'true';
            const fp = ap.outputs.find(p=>p.id===(fid||'out')) || ap.outputs[0];
            const tp = bp.inputs.find(p=>p.id===(e.toPort||'in'))  || bp.inputs[0];
            if (!fp||!tp) return null;
            const color = e.color || NODE_COLORS[a.kind]?.ring || '#6366F1';
            return <FlowEdge key={i} sx={fp.x} sy={fp.y} ex={tp.x} ey={tp.y}
              color={color} animated={false} glow={false}/>;
          })}

          {/* ── Nodes ──────────────────────────────────────────── */}
          {nodes.map(n => {
            const ports = getNodePorts(n, edges);
            const nc    = NODE_COLORS[n.kind] || NODE_COLORS.action;
            const isActive = !!(n.active || n.selected);
            return (
              <g key={n.id}>
                {isActive && (
                  <rect x={n.x-7} y={n.y-7} width={ports.w+14} height={ports.h+14} rx="16"
                    fill="none" stroke={nc.ring} strokeWidth="1.5" strokeOpacity=".55"
                    style={{filter:`drop-shadow(0 0 14px ${nc.glow})`}}
                    pointerEvents="none"/>
                )}
                <EditorNodeShape node={{...n, name:n.name||n.id}} ports={ports} selected={isActive}/>
                {ports.outputs.map(p => (
                  <circle key={p.id} cx={p.x} cy={p.y} r="4"
                    fill="#060810" stroke={p.color||nc.ring} strokeWidth="1.5" strokeOpacity=".65"
                    pointerEvents="none"/>
                ))}
                {ports.inputs.map(p => (
                  <circle key={p.id} cx={p.x} cy={p.y} r="4"
                    fill="#060810" stroke={nc.ring} strokeWidth="1.5" strokeOpacity=".3"
                    pointerEvents="none"/>
                ))}
                {n.kind==='cond' && ports.outputs.map(p => p.label && (
                  <text key={p.id+'lbl'} x={p.x+8} y={p.y+4}
                    fill={p.color||nc.ring} fillOpacity=".9" fontSize="9" fontWeight="700"
                    fontFamily="Geist Mono,monospace" pointerEvents="none">{p.label}</text>
                ))}
              </g>
            );
          })}
        </g>
      </svg>

      <div style={{position:'absolute',top:12,left:14,display:'flex',alignItems:'center',gap:8,
        padding:'4px 10px', background:'rgba(11,13,24,.88)',
        border:'1px solid rgba(255,255,255,.09)',borderRadius:7,backdropFilter:'blur(8px)',
        fontFamily:'Geist Mono,monospace',fontSize:11,pointerEvents:'none',maxWidth:'60%'}}>
        <span style={{width:7,height:7,borderRadius:'50%',flexShrink:0,
          background:statusDot, boxShadow:`0 0 8px ${statusDot}`,
          animation:status==='running'||status==='waiting'?'fd-pulse 1.6s ease-in-out infinite':'none'}}/>
        <b style={{color:'#EDEFF7',fontWeight:600,overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}}>{title}</b>
        {meta && <span style={{color:'rgba(255,255,255,.32)',marginLeft:2}}>{meta}</span>}
      </div>

      <div style={{position:'absolute',left:14,bottom:14,display:'flex',gap:5,padding:'4px 6px',
        background:'rgba(11,13,24,.92)',border:'1px solid var(--line)',borderRadius:8,backdropFilter:'blur(8px)'}}>
        {[
          ['+', ()=>doZoom(1.2), 'Zoom in'],
          ['−', ()=>doZoom(1/1.2), 'Zoom out'],
          ['⤢', fitView, 'Fit view'],
        ].map(([s,fn,label]) => (
          <button key={s} className="icon-btn" onClick={fn} aria-label={label}
            style={{width:26,height:26,borderRadius:6,fontSize:s==='⤢'?11:16,
              display:'flex',alignItems:'center',justifyContent:'center'}}>{s}</button>
        ))}
        <span className="mono" style={{fontSize:11,color:'var(--ink-faint)',alignSelf:'center',padding:'0 4px'}}>
          {Math.round(vt.s*100)}%
        </span>
      </div>

      <style>{`@keyframes fd-pulse{0%,100%{transform:scale(1);opacity:1}50%{transform:scale(1.4);opacity:.6}}`}</style>
    </div>
  );
}
window.FlowDiagram = FlowDiagram;

/* ══════════════════════════════════════════════════════════════════
   EDITOR NODE SHAPE
   ═════════════════════════════════════════════════════════════ */
function EditorNodeShape({ node, ports, selected }) {
  const { kind, x, y, name } = node;
  const nc = NODE_COLORS[kind] || NODE_COLORS.action;
  const { w, h } = ports;

  if (kind === 'cond') {
    const cx = w/2, cy = h/2;
    const d    = `M ${cx} 0 L ${w} ${cy} L ${cx} ${h} L 0 ${cy} Z`;
    const dSel = `M ${cx} -6 L ${w+6} ${cy} L ${cx} ${h+6} L -6 ${cy} Z`;
    const label = (name||'cond').length > 13 ? (name||'cond').slice(0,13)+'…' : (name||'cond');
    return (
      <g transform={`translate(${x} ${y})`}>
        {selected && <path d={dSel} fill="none" stroke={nc.ring} strokeWidth="1"
          strokeOpacity=".45" style={{filter:`drop-shadow(0 0 12px ${nc.glow})`}}/>}
        <path d={d} fill={nc.bg} stroke={nc.ring} strokeWidth="1.2" strokeOpacity=".85"/>
        <g transform={`translate(${cx} ${cy})`}>
          <circle r="12" fill={nc.bg} stroke={nc.ring} strokeOpacity=".5"/>
          <foreignObject x="-9" y="-9" width="18" height="18">
            <div xmlns="http://www.w3.org/1999/xhtml"
              style={{color:nc.ring,fontWeight:600,fontSize:13,textAlign:"center",lineHeight:"18px"}}>
              <NodeGlyph kind="cond"/>
            </div>
          </foreignObject>
        </g>
        <text x={cx} y={cy-11} fill="#EDEFF7" fontSize="10.5" fontWeight="600"
          fontFamily="Geist, sans-serif" textAnchor="middle" pointerEvents="none">{label}</text>
        <text x={cx} y={cy+16} fill="#5B6378" fontSize="9"
          fontFamily="Geist Mono, monospace" textAnchor="middle" pointerEvents="none">Condition</text>
      </g>
    );
  }

  if (kind === 'fork' || kind === 'join') {
    const r = 10;
    const portCount = kind === 'fork' ? ports.outputs.length : ports.inputs.length;
    const label = (name||kind).length > 18 ? (name||kind).slice(0,18)+'…' : (name||kind);
    return (
      <g transform={`translate(${x} ${y})`}>
        {selected && <rect x="-4" y="-4" width={w+8} height={h+8} rx={r+3}
          fill="none" stroke={nc.ring} strokeWidth="1" strokeOpacity=".35"
          style={{filter:`drop-shadow(0 0 12px ${nc.glow})`}}/>}
        <rect width={w} height={h} rx={r} fill={nc.bg} stroke={nc.ring} strokeWidth="1.2" strokeOpacity=".85"/>
        <rect x="0" y="0" width="3" height={h} rx={2} fill={nc.ring}/>
        <g transform={`translate(14 ${h/2})`}>
          <circle r="11" fill={nc.bg} stroke={nc.ring} strokeOpacity=".5"/>
          <foreignObject x="-9" y="-9" width="18" height="18">
            <div xmlns="http://www.w3.org/1999/xhtml"
              style={{color:nc.ring,fontWeight:600,fontSize:11,textAlign:"center",lineHeight:"18px"}}>
              <NodeGlyph kind={kind}/>
            </div>
          </foreignObject>
        </g>
        <text x={34} y={h/2-5} fill="#EDEFF7" fontSize="11.5" fontWeight="600"
          fontFamily="Geist, sans-serif" letterSpacing="-.01em" pointerEvents="none">{label}</text>
        <text x={34} y={h/2+9} fill="#5B6378" fontSize="10"
          fontFamily="Geist Mono, monospace" pointerEvents="none">{kind} · {portCount}×</text>
      </g>
    );
  }

  return <FlowNode x={x} y={y} w={w} h={h}
    kind={kind} name={name||node.id} sub={node.sub||kindToType(kind)}
    active={false} selected={selected}/>;
}

/* ══════════════════════════════════════════════════════════════════
   EDITOR CANVAS — pan / zoom / drag nodes / drag-to-connect
   ═════════════════════════════════════════════════════════════ */
function snap10(v) { return Math.round(v/10)*10; }

function EditorCanvas({ nodes, edges, selected, selectedEdge, onSelect, onSelectEdge, onNodeMove, onNodeMoveEnd, onDropNode, onAddEdge, onRemoveEdge, height }) {
  const svgRef  = React.useRef(null);
  const [camera, setCamera] = React.useState({x:60, y:60, z:1});
  const camRef  = React.useRef({x:60,y:60,z:1});
  const iRef    = React.useRef(null);
  const [connecting, setConnecting] = React.useState(null);
  const connRef = React.useRef(null);
  const [edgeMid, setEdgeMid] = React.useState(null); // {x,y} in screen coords for delete btn

  React.useEffect(() => { camRef.current = camera; }, [camera]);
  React.useEffect(() => { connRef.current = connecting; }, [connecting]);

  // Recalculate edge midpoint for delete button when edges/selection/camera changes
  React.useEffect(() => {
    if (selectedEdge == null) { setEdgeMid(null); return; }
    const e = edges[selectedEdge];
    if (!e) { setEdgeMid(null); return; }
    const a = nodes.find(n=>n.id===e.from);
    const b = nodes.find(n=>n.id===e.to);
    if (!a||!b) { setEdgeMid(null); return; }
    const ap = getNodePorts(a, edges);
    const bp = getNodePorts(b, edges);
    let fromPortId = e.fromPort;
    if (!fromPortId && a.kind==='cond')
      fromPortId = e.color==='#F87171' ? 'false' : 'true';
    const fp = ap.outputs.find(p=>p.id===(fromPortId||'out')) || ap.outputs[0];
    const tp = bp.inputs.find(p=>p.id===(e.toPort||'in'))    || bp.inputs[0];
    if (!fp||!tp) { setEdgeMid(null); return; }
    const rect = svgRef.current?.getBoundingClientRect();
    if (!rect) { setEdgeMid(null); return; }
    const mx = (fp.x + tp.x) / 2;
    const my = (fp.y + tp.y) / 2;
    const sx = rect.left + camRef.current.x + mx * camRef.current.z;
    const sy = rect.top  + camRef.current.y + my * camRef.current.z;
    setEdgeMid({x: sx, y: sy});
  }, [selectedEdge, edges, nodes, camera]);

  React.useEffect(() => {
    const svg = svgRef.current; if (!svg) return;
    const handler = e => {
      e.preventDefault();
      const rect = svg.getBoundingClientRect();
      const mx = e.clientX-rect.left, my = e.clientY-rect.top;
      const f = e.deltaY < 0 ? 1.12 : 1/1.12;
      setCamera(c => {
        const nz = Math.max(0.12, Math.min(4, c.z*f));
        return { x: mx-(mx-c.x)*(nz/c.z), y: my-(my-c.y)*(nz/c.z), z: nz };
      });
    };
    svg.addEventListener('wheel', handler, {passive:false});
    return () => svg.removeEventListener('wheel', handler);
  }, []);

  const toWorld = (cx, cy) => {
    const rect = svgRef.current.getBoundingClientRect();
    const c = camRef.current;
    return { x:(cx-rect.left-c.x)/c.z, y:(cy-rect.top-c.y)/c.z };
  };

  const getAttr = (t, attr) => {
    let el = t;
    while (el && el !== svgRef.current) {
      if (el.dataset?.[attr]) return el.dataset[attr];
      el = el.parentElement;
    }
    return null;
  };
  const getNid = t => {
    let el = t;
    while (el && el !== svgRef.current) {
      if (el.dataset?.handleOut || el.dataset?.handleIn) return null;
      if (el.dataset?.nid) return el.dataset.nid;
      el = el.parentElement;
    }
    return null;
  };

  const onSvgPointerDown = e => {
    const handleOut  = getAttr(e.target, 'handleOut');
    const handlePort = getAttr(e.target, 'handlePort');
    const nid        = !handleOut && getNid(e.target);
    const c          = camRef.current;

    if (handleOut) {
      e.preventDefault();
      const node = nodes.find(n => n.id === handleOut);
      if (!node) return;
      const ports   = getNodePorts(node, edges);
      const portObj = ports.outputs.find(p => p.id === handlePort) || ports.outputs[0];
      if (!portObj) return;
      const w2 = toWorld(e.clientX, e.clientY);
      const c2 = {fromId:handleOut, fromPort:portObj.id, x1:portObj.x, y1:portObj.y, x2:w2.x, y2:w2.y};
      setConnecting(c2); connRef.current = c2;
      iRef.current = {mode:'connect'};
      onSelectEdge(null);
    } else if (nid) {
      e.preventDefault();
      onSelect(nid);
      onSelectEdge(null);
      const node = nodes.find(n => n.id === nid);
      if (!node) return;
      iRef.current = {mode:'node', id:nid, startX:e.clientX, startY:e.clientY, origX:node.x, origY:node.y};
    } else {
      // Clicked on background — check if an edge was clicked
      const edgeIndex = getAttr(e.target, 'edgeIndex');
      if (edgeIndex != null) {
        onSelectEdge(+edgeIndex);
        onSelect(null);
        iRef.current = null;
        return;
      }
      onSelect(null);
      onSelectEdge(null);
      iRef.current = {mode:'pan', startX:e.clientX, startY:e.clientY, origX:c.x, origY:c.y};
    }
    svgRef.current.setPointerCapture(e.pointerId);
  };

  const onSvgPointerMove = e => {
    const d = iRef.current;
    if (!d) return;
    if (d.mode === 'connect') {
      const w = toWorld(e.clientX, e.clientY);
      setConnecting(prev => prev ? {...prev, x2:w.x, y2:w.y} : null);
    } else if (d.mode === 'pan') {
      setCamera(c => ({...c, x:d.origX+(e.clientX-d.startX), y:d.origY+(e.clientY-d.startY)}));
    } else if (d.mode === 'node') {
      const c = camRef.current;
      onNodeMove(d.id, d.origX+(e.clientX-d.startX)/c.z, d.origY+(e.clientY-d.startY)/c.z);
    }
  };

  const onSvgPointerUp = e => {
    const d = iRef.current;
    if (d?.mode === 'connect') {
      const conn = connRef.current;
      if (conn) {
        const w = toWorld(e.clientX, e.clientY);
        let best = null, bestDist = window.FE.SNAP_R;
        nodes.forEach(n => {
          if (n.id === conn.fromId) return;
          getNodePorts(n, edges).inputs.forEach(p => {
            const dist = Math.hypot(w.x-p.x, w.y-p.y);
            if (dist < bestDist) { bestDist=dist; best={nodeId:n.id, portId:p.id}; }
          });
        });
        if (best) onAddEdge(conn.fromId, conn.fromPort, best.nodeId, best.portId);
      }
      setConnecting(null); connRef.current = null;
    } else if (d?.mode === 'node') {
      const c = camRef.current;
      const finalX = snap10(d.origX+(e.clientX-d.startX)/c.z);
      const finalY = snap10(d.origY+(e.clientY-d.startY)/c.z);
      if (onNodeMoveEnd) onNodeMoveEnd(d.id, finalX, finalY);
    }
    iRef.current = null;
  };

  const onDragOver   = e => { e.preventDefault(); e.dataTransfer.dropEffect='copy'; };
  const onDropCanvas = e => {
    e.preventDefault();
    const kind = e.dataTransfer.getData('node-kind');
    if (!kind || !svgRef.current) return;
    const w = toWorld(e.clientX, e.clientY);
    onDropNode(kind, snap10(w.x), snap10(w.y));
  };

  const byId = Object.fromEntries(nodes.map(n=>[n.id,n]));
  const ps = Math.max(4, 18*camera.z);
  const px = ((camera.x%ps)+ps)%ps;
  const py = ((camera.y%ps)+ps)%ps;
  const pr = Math.max(0.5, Math.min(1.5, 0.85*camera.z));

  return (
    <div style={{position:'relative',width:'100%',height:height||'100%',overflow:'hidden'}}
      onDragOver={onDragOver} onDrop={onDropCanvas}>
      <svg ref={svgRef} width="100%" height="100%"
        style={{display:'block',background:'#070912',userSelect:'none',touchAction:'none',
          cursor: connecting ? 'crosshair' : 'default'}}
        onPointerDown={onSvgPointerDown}
        onPointerMove={onSvgPointerMove}
        onPointerUp={onSvgPointerUp}
      >
        <defs>
          <pattern id="ec-dot" x={px} y={py} width={ps} height={ps} patternUnits="userSpaceOnUse">
            <circle cx={pr} cy={pr} r={pr} fill="rgba(255,255,255,0.055)"/>
          </pattern>
        </defs>
        <rect width="100%" height="100%" fill="url(#ec-dot)"/>

        <g transform={`translate(${camera.x},${camera.y}) scale(${camera.z})`}>

          {/* ── Edges (port-aware) ─────────────────────────── */}
          {edges.map((e,i) => {
            const a=byId[e.from], b=byId[e.to];
            if(!a||!b) return null;
            const aPorts = getNodePorts(a, edges);
            const bPorts = getNodePorts(b, edges);
            let fromPortId = e.fromPort;
            if (!fromPortId && a.kind==='cond')
              fromPortId = e.color==='#F87171' ? 'false' : 'true';
            const fromPort = aPorts.outputs.find(p=>p.id===(fromPortId||'out')) || aPorts.outputs[0];
            const toPort   = bPorts.inputs.find(p=>p.id===(e.toPort||'in'))    || bPorts.inputs[0];
            if (!fromPort||!toPort) return null;
            const color = e.color
              || (e.fromPort==='true'?'#34D399':e.fromPort==='false'?'#F87171':null)
              || NODE_COLORS[a.kind]?.ring || '#6366F1';
            const isSel = selectedEdge === i;
            // Compute bezier midpoint for invisible hit path (same math as FlowEdge)
            const x1=fromPort.x, y1=fromPort.y, x2=toPort.x, y2=toPort.y;
            let d;
            if (x2 < x1 - 40) {
              const dx = x1 - x2, dy = y2 - y1;
              if (dx > 300) {
                const arc = Math.max(40, dx * 0.08) * (dy < 0 ? 1 : -1);
                const midX = (x1 + x2) / 2;
                d = `M ${x1} ${y1} Q ${x1+50} ${y1+arc} ${midX} ${y1+arc} Q ${x2-50} ${y1+arc} ${x2} ${y2}`;
              } else {
                const hExit = Math.min(54, dy * 0.18);
                const vEntry = Math.max(32, dy * 0.28);
                d = `M ${x1} ${y1} C ${x1+hExit} ${y1}, ${x2} ${y2-vEntry}, ${x2} ${y2}`;
              }
            } else {
              const dx = Math.abs(x2-x1);
              const c1x=x1+Math.max(20,dx*.45), c1y=y1;
              const c2x=x2-Math.max(20,dx*.45), c2y=y2;
              d = `M ${x1} ${y1} C ${c1x} ${c1y}, ${c2x} ${c2y}, ${x2} ${y2}`;
            }
            return (
              <g key={i} data-edge-index={i}>
                {/* invisible wide hit path */}
                <path d={d} stroke="transparent" strokeWidth={isSel?12:8} fill="none"
                  style={{cursor:'pointer'}} data-edge-index={i}
                  onClick={ev=>{ ev.stopPropagation(); onSelectEdge(i); }}/>
                {/* visible path */}
                <path d={d} stroke={color} strokeWidth={isSel?2.6:1.4} fill="none"
                  strokeDasharray="0"
                  strokeLinecap="round"
                  style={{pointerEvents:'none'}}/>
                {isSel && (
                  <path d={d} stroke={color} strokeWidth="1" fill="none"
                    strokeDasharray="4 4" strokeLinecap="round"
                    style={{pointerEvents:'none',opacity:0.6}}>
                    <animate attributeName="stroke-dashoffset" from="8" to="0" dur=".6s" repeatCount="indefinite"/>
                  </path>
                )}
              </g>
            );
          })}

          {/* ── In-progress connection bezier ─────────────── */}
          {connecting && (() => {
            const bend = Math.max(40, Math.abs(connecting.x2-connecting.x1)*0.5);
            const d = `M${connecting.x1} ${connecting.y1} C${connecting.x1+bend} ${connecting.y1},${connecting.x2-bend} ${connecting.y2},${connecting.x2} ${connecting.y2}`;
            return (
              <g pointerEvents="none">
                <path d={d} stroke="#6366F1" strokeOpacity=".25" strokeWidth="5" fill="none"
                  style={{filter:'drop-shadow(0 0 8px #6366F1)'}}/>
                <path d={d} stroke="#818CF8" strokeWidth="1.8" fill="none"
                  strokeDasharray="6 4" strokeLinecap="round">
                  <animate attributeName="stroke-dashoffset" from="20" to="0" dur=".8s" repeatCount="indefinite"/>
                </path>
                <circle cx={connecting.x2} cy={connecting.y2} r="4"
                  fill="#818CF8" fillOpacity=".9" style={{filter:'drop-shadow(0 0 5px #6366F1)'}}/>
              </g>
            );
          })()}

          {/* ── Nodes + port handles ───────────────────────── */}
          {nodes.map(n => {
            const nc   = NODE_COLORS[n.kind] || NODE_COLORS.action;
            const ports = getNodePorts(n, edges);
            const isSource = connecting?.fromId === n.id;

            return (
              <g key={n.id}>
                {/* draggable body */}
                <g data-nid={n.id} style={{cursor: connecting ? 'crosshair' : 'grab'}}>
                  <EditorNodeShape node={n} ports={ports} selected={n.id===selected}/>
                </g>

                {/* ── Input ports ── */}
                {ports.inputs.map(p => {
                  const near = connecting && !isSource
                    && Math.hypot(connecting.x2-p.x, connecting.y2-p.y) < window.FE.SNAP_R;
                  return (
                    <g key={p.id}>
                      <circle cx={p.x} cy={p.y}
                        r={near ? 9 : 5}
                        fill={near ? nc.ring : '#060810'}
                        stroke={nc.ring} strokeWidth={near?2.5:1.5}
                        strokeOpacity={connecting&&!isSource ? 0.9 : 0.35}
                        data-handle-in={n.id} data-handle-port={p.id}
                        style={{cursor:'crosshair',transition:'r .1s'}}
                        pointerEvents={connecting ? 'all' : 'none'}/>
                      {!connecting && <text x={p.x-8} y={p.y+4}
                        fill={nc.ring} fillOpacity=".28" fontSize="9"
                        fontFamily="Geist Mono,monospace" textAnchor="middle"
                        pointerEvents="none">◀</text>}
                    </g>
                  );
                })}

                {/* ── Output ports ── */}
                {ports.outputs.map(p => {
                  const active = isSource && connecting?.fromPort===p.id;
                  const col = p.color || nc.ring;
                  return (
                    <g key={p.id}>
                      {/* invisible larger hit area */}
                      <circle cx={p.x} cy={p.y}
                        r={22}
                        fill="transparent"
                        data-handle-out={n.id} data-handle-port={p.id}
                        style={{cursor:'crosshair'}}/>
                      {/* visible handle */}
                      <circle cx={p.x} cy={p.y}
                        r={active ? 7 : 5}
                        fill={active ? col : '#060810'}
                        stroke={col} strokeWidth={active?2.5:1.5}
                        strokeOpacity={active ? 1 : 0.7}
                        style={{pointerEvents:'none'}}/>
                      {!connecting && p.label && (
                        <text x={p.x+10} y={p.y+4} fill={col} fillOpacity=".8"
                          fontSize="9" fontWeight="700" fontFamily="Geist Mono,monospace"
                          pointerEvents="none">{p.label}</text>
                      )}
                      {!connecting && !p.label && (
                        <text x={p.x+8} y={p.y+4} fill={col} fillOpacity=".3"
                          fontSize="9" fontFamily="Geist Mono,monospace"
                          pointerEvents="none">▶</text>
                      )}
                    </g>
                  );
                })}
              </g>
            );
          })}
        </g>
      </svg>

      {/* Edge delete floating button */}
      {edgeMid && (
        <button className="icon-btn"
          onClick={()=>{ if (onRemoveEdge && selectedEdge!=null) onRemoveEdge(selectedEdge); }}
          style={{
            position:'absolute', left:edgeMid.x-12, top:edgeMid.y-12,
            width:24, height:24, borderRadius:12,
            background:'rgba(248,113,113,.15)', border:'1px solid rgba(248,113,113,.4)',
            color:'#F87171', fontSize:14, zIndex:20,
            display:'flex', alignItems:'center', justifyContent:'center',
          }}
          title="Delete edge"
          aria-label="Delete edge">
          ×
        </button>
      )}

      {/* Zoom controls */}
      <div style={{position:'absolute',left:14,bottom:14,display:'flex',gap:5,padding:'4px 6px',
        background:'rgba(11,13,24,.92)',border:'1px solid var(--line)',borderRadius:8,backdropFilter:'blur(8px)'}}>
        {[
          ['+', () => setCamera(c=>({...c,z:Math.min(4,c.z*1.2)})), 'Zoom in'],
          ['−', () => setCamera(c=>({...c,z:Math.max(0.12,c.z/1.2)})), 'Zoom out'],
          ['⤢', () => setCamera({x:60,y:60,z:1}), 'Fit view'],
        ].map(([s,fn,label]) => (
          <button key={s} className="icon-btn" onClick={fn} aria-label={label}
            style={{width:26,height:26,borderRadius:6,fontSize:s==='⤢'?11:16,
              display:'flex',alignItems:'center',justifyContent:'center'}}>{s}</button>
        ))}
        <span className="mono" style={{fontSize:11,color:'var(--ink-faint)',alignSelf:'center',padding:'0 4px'}}>
          {Math.round(camera.z*100)}%
        </span>
      </div>

      <div style={{position:'absolute',right:14,bottom:14,padding:'5px 10px',
        background:'rgba(11,13,24,.92)',border:'1px solid var(--line)',borderRadius:7,
        fontFamily:'Geist Mono, monospace',fontSize:10.5,color:'var(--ink-faint)',
        backdropFilter:'blur(8px)'}}>
        {connecting ? '● release on ◀ port to connect · Esc cancel' : 'scroll=zoom · drag bg=pan · drag ▶ to connect'}
      </div>
    </div>
  );
}

window.FE.EditorNodeShape = EditorNodeShape;
window.FE.EditorCanvas = EditorCanvas;
window.FE.NodeGlyph = NodeGlyph;
