/* ============================================================
   FlowBuilder v3 — explicit node graph with movable connectors.
   • Drag a node to move it.
   • Drag from an OUTPUT port (right) onto an INPUT port (left) to wire.
   • Click a connector to select it, then delete.
   • Click a node to inspect/edit; add from palette; remove.
   Branching: no-reply continues; reply → In conversation → Won / Lost.
   ============================================================ */

const STEP_KINDS = {
  email:            { label: "Email",            icon: "mail",      color: "var(--accent)",      reply: true,  w: 256, h: 152 },
  schedule:         { label: "Scheduled email",  icon: "calendar",  color: "var(--accent)",      reply: true,  w: 256, h: 160 },
  linkedin_connect: { label: "LinkedIn connect", icon: "userPlus",  color: "#8FB8E8",            reply: true,  w: 240, h: 124 },
  linkedin_msg:     { label: "LinkedIn message", icon: "linkedin",  color: "#8FB8E8",            reply: true,  w: 240, h: 146 },
  ig_follow:        { label: "Follow on IG",     icon: "instagram", color: "var(--lane-agency)", reply: false, w: 208, h: 92  },
  ig_dm:            { label: "Instagram DM",     icon: "instagram", color: "var(--lane-agency)", reply: true,  w: 240, h: 146 },
  call:             { label: "Call",             icon: "phone",     color: "var(--amber)",       reply: false, w: 220, h: 104 },
  task:             { label: "Manual task",      icon: "check",     color: "var(--text-2)",      reply: false, w: 220, h: 104 },
  wait:             { label: "Wait / delay",     icon: "clock",     color: "var(--text-2)",      reply: false, w: 172, h: 76, isWait: true },
  revisit:          { label: "Reach out later",  icon: "snooze",    color: "var(--amber)",       reply: false, w: 210, h: 96, isRevisit: true },
};
const ADD_ORDER = ["wait", "revisit", "email", "schedule", "linkedin_connect", "linkedin_msg", "ig_follow", "ig_dm", "call", "task"];
const TAG_KINDS = {
  replied:    { label: "Replied",    color: "var(--accent)", icon: "reply" },
  interested: { label: "Interested", color: "var(--green)",  icon: "trend" },
  not_now:    { label: "Not now",    color: "var(--amber)",  icon: "snooze" },
  won:        { label: "Won",        color: "var(--green)",  icon: "trend" },
  lost:       { label: "Lost",       color: "var(--red)",    icon: "x" },
};
const TAG_ORDER = ["replied", "interested", "not_now", "won", "lost"];
const FEDGE_COLOR = { start: "var(--accent)", flow: "rgba(255,255,255,0.30)", reply: "var(--green)", won: "var(--green)", lost: "var(--red)" };

const NDIM = { trigger: { w: 212, h: 92 }, convo: { w: 236, h: 104 }, won: { w: 178, h: 80 }, lost: { w: 178, h: 80 }, finished: { w: 196, h: 84 } };
let _gid = 0;
const gid = (p) => p + (++_gid) + "_" + Math.random().toString(36).slice(2, 5);

function nodePorts(n) {
  const w = n.w, h = n.h;
  if (n.type === "trigger") return [{ id: "out", side: "out", x: w, y: h / 2, kind: "start", label: "start" }];
  if (n.type === "step") {
    const k = STEP_KINDS[n.kind];
    const ps = [{ id: "in", side: "in", x: 0, y: h / 2, kind: "flow" }];
    if (k.reply) { ps.push({ id: "next", side: "out", x: w, y: h * 0.4, kind: "flow", label: "no reply" }); ps.push({ id: "reply", side: "out", x: w, y: h * 0.74, kind: "reply", label: "reply" }); }
    else ps.push({ id: "next", side: "out", x: w, y: h / 2, kind: "flow" });
    return ps;
  }
  if (n.type === "convo") return [{ id: "in", side: "in", x: 0, y: h / 2, kind: "reply" }, { id: "won", side: "out", x: w, y: h * 0.38, kind: "won", label: "won" }, { id: "lost", side: "out", x: w, y: h * 0.68, kind: "lost", label: "lost" }];
  if (n.type === "split") return [{ id: "in", side: "in", x: 0, y: h / 2, kind: "flow" }, { id: "a", side: "out", x: w, y: h * 0.38, kind: "flow", label: "A" }, { id: "b", side: "out", x: w, y: h * 0.68, kind: "flow", label: "B" }];
  if (n.type === "tag") return [{ id: "in", side: "in", x: 0, y: h / 2, kind: "flow" }];
  return [{ id: "in", side: "in", x: 0, y: h / 2, kind: "flow" }];
}
function portOf(n, id) { return nodePorts(n).find((p) => p.id === id); }

function seedSteps(seq) {
  const out = [];
  seq.steps.forEach((s, i) => {
    if (i > 0 && s.delay > 0) out.push({ kind: "wait", days: s.delay });
    const kind = s.channel === "email" ? "email" : s.channel === "linkedin" ? "linkedin_msg" : "ig_dm";
    out.push({ kind, subject: s.subject || "", body: s.body || "", mode: s.mode || "manual" });
  });
  return out;
}

function buildSeed(seq) {
  const cy = 300, gapX = 60;
  const nodes = [], edges = [];
  let x = 36;
  nodes.push({ id: "trigger", type: "trigger", x, y: cy - 46, ...NDIM.trigger, seq });
  x += NDIM.trigger.w + gapX;
  const chain = [];
  seedSteps(seq).forEach((st) => {
    const k = STEP_KINDS[st.kind];
    const node = { id: gid("s"), type: "step", kind: st.kind, x, y: cy - k.h / 2, w: k.w, h: k.h, data: st };
    nodes.push(node); chain.push(node); x += k.w + gapX;
  });
  nodes.push({ id: "finished", type: "finished", x, y: cy - 42, ...NDIM.finished });
  const midX = 36 + (x - 36) / 2;
  nodes.push({ id: "convo", type: "convo", x: midX - 118, y: cy + 210, ...NDIM.convo });
  nodes.push({ id: "won", type: "won", x: midX - 118 + 366, y: cy + 178, ...NDIM.won });
  nodes.push({ id: "lost", type: "lost", x: midX - 118 + 366, y: cy + 300, ...NDIM.lost });

  const E = (fn, fp, tn, tp, kind) => edges.push({ id: gid("e"), from: { node: fn, port: fp }, to: { node: tn, port: tp }, kind });
  const first = chain[0] ? chain[0].id : "finished";
  E("trigger", "out", first, "in", "start");
  chain.forEach((n, i) => {
    const next = chain[i + 1] ? chain[i + 1].id : "finished";
    E(n.id, "next", next, "in", "flow");
    if (STEP_KINDS[n.kind].reply) E(n.id, "reply", "convo", "in", "reply");
  });
  E("convo", "won", "won", "in", "won");
  E("convo", "lost", "lost", "in", "lost");
  return { nodes, edges };
}

function fPath(p1, p2) { const dx = Math.max(46, Math.abs(p2.x - p1.x) * 0.5); return `M ${p1.x} ${p1.y} C ${p1.x + dx} ${p1.y}, ${p2.x - dx} ${p2.y}, ${p2.x} ${p2.y}`; }

function PortDot({ p, onWireStart, onWireEnd }) {
  const color = FEDGE_COLOR[p.kind] || "rgba(255,255,255,0.4)";
  return (
    <div className={"node-port node-port--" + p.side} style={{ left: p.side === "in" ? -7 : undefined, right: p.side === "out" ? -7 : undefined, top: p.y }}>
      {p.side === "out" && p.label && <span className="port-label">{p.label}</span>}
      <span className="port-dot port-dot--live" style={{ background: color, boxShadow: `0 0 0 3px ${color}22` }}
        onMouseDown={p.side === "out" ? (e) => onWireStart(e, p.id) : undefined}
        onMouseUp={p.side === "in" ? (e) => onWireEnd(e) : undefined}></span>
    </div>
  );
}

function FlowNodeView({ node, selected, count, onDown, onSelect, onWireStart, onWireEnd }) {
  const sel = selected ? " is-selected" : "";
  const ports = nodePorts(node);
  const stop = (e) => { e.stopPropagation(); onSelect(node.id); };
  const portEls = ports.map((p) => <PortDot key={p.id} p={p} onWireStart={(e, pid) => onWireStart(e, node.id, pid)} onWireEnd={onWireEnd} />);
  const badge = count > 0 ? <span className="node-count" title={count + " contacts here"}><Icon name="contacts" size={11} />{count}</span> : null;

  let body;
  if (node.type === "trigger") {
    body = (<React.Fragment>
      <div className="node-head"><Icon name="flag" size={14} /> Enrolled contacts</div>
      <div className="node-pad">
        <div style={{ display: "flex", alignItems: "center", gap: 8 }}><LaneChip lane={node.seq.lane} small /><span className="num" style={{ fontSize: 20, fontWeight: 700 }}>{count != null ? count : node.seq.enrolled}</span></div>
        <div style={{ fontSize: 11.5, color: "var(--text-3)", marginTop: 4 }}>in this flow</div>
      </div></React.Fragment>);
  } else if (node.type === "step") {
    const k = STEP_KINDS[node.kind], s = node.data;
    body = k.isWait ? (
      <div className="node-pad" style={{ padding: 14, display: "flex", alignItems: "center", gap: 9 }}>
        <Icon name="clock" size={15} style={{ color: "var(--text-2)" }} /><span style={{ fontSize: 13, fontWeight: 650 }}>Wait</span>
        <span className="num" style={{ fontSize: 15, fontWeight: 700 }}>{s.days}</span><span style={{ fontSize: 12.5, color: "var(--text-3)" }}>{s.days === 1 ? "day" : "days"}</span>
      </div>
    ) : k.isRevisit ? (
      <div className="node-pad" style={{ padding: "12px 14px" }}>
        <div style={{ display: "flex", alignItems: "center", gap: 8 }}><Icon name="snooze" size={15} style={{ color: "var(--amber)" }} /><span style={{ fontSize: 13, fontWeight: 650, color: "var(--amber)" }}>Reach out later</span></div>
        <div style={{ fontSize: 11.5, color: "var(--text-3)", marginTop: 5 }}>re-engage in <span className="num" style={{ color: "var(--text)", fontWeight: 600 }}>{s.days}</span> days · loops back in</div>
      </div>
    ) : (<React.Fragment>
      <div className="node-head" style={{ color: k.color }}><Icon name={k.icon} size={14} /> {k.label}
        {["email", "linkedin_msg", "ig_dm", "schedule"].includes(node.kind) && <span className={"node-mode " + (s.mode === "manual" ? "manual" : "auto")}>{s.mode === "manual" ? "approve" : "auto"}</span>}</div>
      <div className="node-pad">
        {["email", "schedule"].includes(node.kind) && s.subject && <div className="node-subject">{s.subject}</div>}
        {node.kind === "ig_follow" ? <div className="node-body-prev">Auto-follows the contact's IG to warm them up.</div> : <div className="node-body-prev">{s.body || s.note || "—"}</div>}
        {node.kind === "schedule" && <div className="node-sched"><Icon name="calendar" size={11} /> {s.sendAt}</div>}
      </div></React.Fragment>);
  } else if (node.type === "split") {
    body = (<React.Fragment>
      <div className="node-head"><Icon name="branch" size={14} /> A/B split</div>
      <div className="node-pad">
        <div style={{ fontSize: 12, color: "var(--text-2)" }}>{node.data.ratio}% / {100 - node.data.ratio}%</div>
        <div style={{ fontSize: 11, color: "var(--text-3)", marginTop: 4 }}>splits contacts down two paths</div>
      </div></React.Fragment>);
  } else if (node.type === "tag") {
    const tm = TAG_KINDS[node.tag] || { label: node.tag, color: "var(--text-2)", icon: "flag" };
    body = (
      <div className="node-pad" style={{ padding: "13px 15px", display: "flex", alignItems: "center", gap: 9 }}>
        <Icon name={tm.icon} size={16} style={{ color: tm.color }} />
        <span style={{ fontSize: 14, fontWeight: 700, color: tm.color }}>{tm.label}</span>
      </div>
    );
  } else if (node.type === "convo") {
    body = (<React.Fragment>
      <div className="node-head"><Icon name="reply" size={14} /> Reply received</div>
      <div className="node-pad"><div style={{ fontSize: 12.5, fontWeight: 600 }}>→ In conversation</div>
        <div style={{ fontSize: 11.5, color: "var(--amber)", marginTop: 4, display: "flex", alignItems: "center", gap: 5 }}><Icon name="pause" size={11} /> cadence pauses</div></div>
    </React.Fragment>);
  } else if (node.type === "won" || node.type === "lost") {
    const won = node.type === "won";
    body = (<div className="node-pad" style={{ padding: 15 }}>
      <div style={{ display: "flex", alignItems: "center", gap: 8 }}><Icon name={won ? "trend" : "x"} size={16} style={{ color: won ? "var(--green)" : "var(--red)" }} />
        <span style={{ fontSize: 15, fontWeight: 700, color: won ? "var(--green)" : "var(--red)" }}>{won ? "Won" : "Lost"}</span></div>
      <div style={{ fontSize: 11.5, color: "var(--text-3)", marginTop: 4 }}>{won ? "→ create Project" : "→ capture reason"}</div></div>);
  } else {
    body = (<div className="node-pad" style={{ padding: 15 }}>
      <div style={{ display: "flex", alignItems: "center", gap: 8 }}><Icon name="archive" size={15} style={{ color: "var(--text-2)" }} /><span style={{ fontSize: 13.5, fontWeight: 650 }}>Finished — no reply</span></div>
      <div style={{ fontSize: 11.5, color: "var(--text-3)", marginTop: 4 }}>sequence complete</div></div>);
  }

  return (
    <div className={"flow-node flow-node--" + node.type + (STEP_KINDS[node.kind] && STEP_KINDS[node.kind].isWait ? " flow-node--wait" : "") + (STEP_KINDS[node.kind] && STEP_KINDS[node.kind].isRevisit ? " flow-node--revisit" : "") + (node.type === "tag" ? " flow-node--tag tag-" + node.tag : "") + sel}
      style={{ left: node.x, top: node.y, width: node.w }} onMouseDown={(e) => onDown(e, node.id)} onClick={stop}>
      {badge}{body}{portEls}
    </div>
  );
}

const FTOKENS = ["{{firstName}}", "{{brand}}", "{{event}}", "{{detail}}"];
function NodeInspector({ node, nodes, onUpdate, onMove, onDelete, onClose }) {
  if (!node) return null;
  if (node.type === "tag") {
    const tm = TAG_KINDS[node.tag] || { label: node.tag, color: "var(--text-2)", icon: "flag" };
    return (<div className="inspector"><div className="insp-head"><span className="insp-title" style={{ color: tm.color, display: "inline-flex", alignItems: "center", gap: 7 }}><Icon name={tm.icon} size={15} /> {tm.label}</span><button className="btn btn--ghost btn--icon btn--sm" onClick={onClose}><Icon name="x" size={16} /></button></div><div className="insp-body"><div className="insp-note">An outcome tag. Wire a node's output into this to mark contacts that reach this state — it pauses the cadence and becomes a filter in your inbox &amp; pipeline.</div></div><div className="insp-foot"><button className="btn btn--sm btn--danger" onClick={() => onDelete(node.id)} style={{ width: "100%" }}><Icon name="trash" size={14} /> Remove node</button></div></div>);
  }
  if (node.type === "split") {
    const s = node.data;
    return (<div className="inspector"><div className="insp-head"><span className="insp-title" style={{ display: "inline-flex", alignItems: "center", gap: 7 }}><Icon name="branch" size={15} /> A/B split</span><button className="btn btn--ghost btn--icon btn--sm" onClick={onClose}><Icon name="x" size={16} /></button></div>
      <div className="insp-body">
        <div className="insp-note" style={{ marginBottom: 14 }}>Splits enrolled contacts down two paths so you can A/B test subject lines, channels or timing. Wire path A and path B to different next steps.</div>
        <div className="insp-field"><div className="ag-slider-head"><span className="insp-label" style={{ margin: 0 }}>Split ratio</span><span className="num" style={{ color: "var(--accent)", fontWeight: 700 }}>{s.ratio}% / {100 - s.ratio}%</span></div>
          <input type="range" className="ag-range" min="10" max="90" step="5" value={s.ratio} onChange={(e) => onUpdate(node.id, { ratio: +e.target.value })} /></div>
        <div className="insp-field"><label className="insp-label">Path A label</label><input className="input" value={s.labelA} onChange={(e) => onUpdate(node.id, { labelA: e.target.value })} /></div>
        <div className="insp-field"><label className="insp-label">Path B label</label><input className="input" value={s.labelB} onChange={(e) => onUpdate(node.id, { labelB: e.target.value })} /></div>
      </div>
      <div className="insp-foot"><button className="btn btn--sm btn--danger" onClick={() => onDelete(node.id)} style={{ width: "100%" }}><Icon name="trash" size={14} /> Remove node</button></div></div>);
  }
  if (node.type !== "step") {
    const desc = { trigger: "Contacts you assign to this flow enter here. Tag a contact and it auto-enrolls.", convo: "When a contact replies on any channel the cadence pauses and they land in your inbox.", won: "Marking Won converts the target into a Project (and a Client if new).", lost: "Marking Lost captures the reason and archives the target.", finished: "Contacts who never reply reach the end and are archived." }[node.type];
    return (<div className="inspector"><div className="insp-head"><span className="insp-title">{({ trigger: "Enrolled", convo: "Reply received", won: "Won", lost: "Lost", finished: "Finished" })[node.type]}</span><button className="btn btn--ghost btn--icon btn--sm" onClick={onClose}><Icon name="x" size={16} /></button></div><div className="insp-body"><p style={{ fontSize: 13, lineHeight: 1.6, color: "var(--text-2)" }}>{desc}</p><div className="insp-note">System node — connectors editable, content fixed.</div></div></div>);
  }
  const k = STEP_KINDS[node.kind], s = node.data;
  const steps = nodes.filter((n) => n.type === "step");
  const idx = steps.findIndex((x) => x.id === node.id);
  const set = (patch) => onUpdate(node.id, patch);
  return (
    <div className="inspector">
      <div className="insp-head"><span className="insp-title" style={{ color: k.color, display: "inline-flex", alignItems: "center", gap: 7 }}><Icon name={k.icon} size={15} /> {k.label}</span><button className="btn btn--ghost btn--icon btn--sm" onClick={onClose}><Icon name="x" size={16} /></button></div>
      <div className="insp-body">
        <div className="insp-step-meta">Node {idx + 1} of {steps.length}</div>
        {(k.isWait || k.isRevisit) && (<div className="insp-field"><label className="insp-label">{k.isRevisit ? "Re-engage after" : "Delay"}</label><div className="stepper"><button onClick={() => set({ days: Math.max(0, (s.days || 0) - 1) })}>–</button><span className="num">{s.days}</span><button onClick={() => set({ days: (s.days || 0) + 1 })}>+</button><span style={{ fontSize: 12.5, color: "var(--text-3)", marginLeft: 8 }}>{s.days === 1 ? "day" : "days"}</span></div></div>)}
        {k.isRevisit && (<div className="insp-field"><label className="insp-label">Per-contact timing</label>
          <Segmented value={s.mode === "detect" ? "Detect from reply" : "Fixed delay"} onChange={(v) => set({ mode: v === "Detect from reply" ? "detect" : "fixed" })} options={["Fixed delay", "Detect from reply"]} />
          <div className="insp-hint">{s.mode === "detect" ? "Claude reads each reply (\u201Cping me in August\u201D) and sets that contact's own re-engage date. Falls back to the fixed delay if none is found." : "Every contact waits the same number of days. Switch to \u201CDetect from reply\u201D to let each contact set their own date."}</div></div>)}
        {["email", "schedule"].includes(node.kind) && (<div className="insp-field"><label className="insp-label">Subject</label><input className="input" value={s.subject || ""} onChange={(e) => set({ subject: e.target.value })} placeholder="Subject line…" /></div>)}
        {node.kind === "schedule" && (<div className="insp-field"><label className="insp-label">Send at</label><input className="input" value={s.sendAt || ""} onChange={(e) => set({ sendAt: e.target.value })} placeholder="e.g. Tue 9:00 AM, or a date" /><div className="insp-hint">Holds the send until this time (per contact's timezone). Manual mode still surfaces it as a draft first.</div></div>)}
        {["email", "linkedin_msg", "ig_dm", "schedule"].includes(node.kind) && (<React.Fragment>
          <div className="insp-field"><label className="insp-label">Message</label><textarea className="seq-template" value={s.body || ""} onChange={(e) => set({ body: e.target.value })} placeholder="Write the template…" /></div>
          <div className="token-row">{FTOKENS.map((tk) => <button key={tk} className="token-chip" onClick={() => set({ body: (s.body ? s.body + " " : "") + tk })}>{tk}</button>)}</div>
          <div className="insp-field"><label className="insp-label">Send mode</label><button className={"mode-toggle " + (s.mode === "manual" ? "manual" : "auto")} onClick={() => set({ mode: s.mode === "manual" ? "auto" : "manual" })}><span className="mode-dot"></span>{s.mode === "manual" ? "Manual approve" : "Auto-send"}</button><div className="insp-hint">{s.mode === "manual" ? "Surfaces as a draft to approve — nothing auto-fires." : "Sends automatically when due."}</div></div>
        </React.Fragment>)}
        {node.kind === "linkedin_connect" && (<div className="insp-field"><label className="insp-label">Connection note</label><textarea className="seq-template" value={s.body || ""} onChange={(e) => set({ body: e.target.value })} placeholder="Optional note…" /></div>)}
        {node.kind === "ig_follow" && (<div className="insp-note">Automatically follows the contact's Instagram. Pair with a Wait, then an Instagram DM.</div>)}
        {["call", "task"].includes(node.kind) && (<div className="insp-field"><label className="insp-label">{node.kind === "call" ? "Call notes" : "Task"}</label><textarea className="seq-template" value={s.note || ""} onChange={(e) => set({ note: e.target.value })} /><div className="insp-hint">Surfaces as a manual to-do.</div></div>)}
      </div>
      <div className="insp-foot"><button className="btn btn--sm btn--danger" onClick={() => onDelete(node.id)} style={{ width: "100%" }}><Icon name="trash" size={14} /> Remove node</button></div>
    </div>
  );
}

function FlowBuilder({ seq }) {
  const [graph, setGraph] = React.useState(() => buildSeed(seq));
  const [selId, setSelId] = React.useState(null);
  const [selEdge, setSelEdge] = React.useState(null);
  const [offset, setOffset] = React.useState({ x: 24, y: 8 });
  const [zoom, setZoom] = React.useState(0.7);
  const [palette, setPalette] = React.useState(false);
  const [wirePt, setWirePt] = React.useState(null);
  const drag = React.useRef(null);
  const wrapRef = React.useRef(null);

  React.useEffect(() => { setGraph(buildSeed(seq)); setSelId(null); setSelEdge(null); }, [seq.id]);

  const nodes = graph.nodes, edges = graph.edges;
  const nodeById = (id) => nodes.find((n) => n.id === id);
  const portPos = (nid, pid) => { const n = nodeById(nid); if (!n) return { x: 0, y: 0 }; const p = portOf(n, pid); return { x: n.x + (p ? p.x : 0), y: n.y + (p ? p.y : n.h / 2) }; };

  // counts: enrolled contacts per step (by action-step order)
  const enr = (window.WTC.enrollments || {});
  const stepCounts = {};
  Object.values(enr).forEach((e) => { if (e && e.seqId === seq.id) stepCounts[e.step] = (stepCounts[e.step] || 0) + 1; });
  const total = Object.values(enr).filter((e) => e && e.seqId === seq.id).length;
  const actionNodes = nodes.filter((n) => n.type === "step" && !STEP_KINDS[n.kind].isWait);
  const countFor = (node) => {
    if (node.type === "trigger") return total;
    const ai = actionNodes.indexOf(node);
    return ai >= 0 ? (stepCounts[ai + 1] || 0) : 0;
  };

  const fitTo = React.useCallback((list) => {
    const el = wrapRef.current; if (!el) return; const r = el.getBoundingClientRect(); if (!r.width) return;
    let a = Infinity, b = Infinity, c = -Infinity, d = -Infinity;
    list.forEach((n) => { a = Math.min(a, n.x); b = Math.min(b, n.y); c = Math.max(c, n.x + n.w); d = Math.max(d, n.y + n.h); });
    const pad = 50, gw = (c - a) + pad * 2, gh = (d - b) + pad * 2;
    const z = Math.max(0.52, Math.min(r.width / gw, r.height / gh, 1));
    setZoom(z); setOffset({ x: (r.width - (c - a) * z) / 2 - a * z, y: (r.height - (d - b) * z) / 2 - b * z });
  }, []);
  React.useEffect(() => { const t = setTimeout(() => fitTo(graph.nodes), 30); return () => clearTimeout(t); }, [seq.id]);

  const world = (e) => { const r = wrapRef.current.getBoundingClientRect(); return { x: (e.clientX - r.left - offset.x) / zoom, y: (e.clientY - r.top - offset.y) / zoom }; };

  React.useEffect(() => {
    function move(e) {
      const dd = drag.current; if (!dd) return;
      if (dd.type === "pan") setOffset({ x: dd.ox + (e.clientX - dd.sx), y: dd.oy + (e.clientY - dd.sy) });
      else if (dd.type === "node") { const dx = (e.clientX - dd.sx) / dd.zoom, dy = (e.clientY - dd.sy) / dd.zoom; setGraph((g) => ({ ...g, nodes: g.nodes.map((n) => n.id === dd.id ? { ...n, x: dd.nx + dx, y: dd.ny + dy } : n) })); }
      else if (dd.type === "wire") setWirePt({ x: (e.clientX - dd.rl - dd.ox) / dd.zoom, y: (e.clientY - dd.rt - dd.oy) / dd.zoom });
    }
    function up() { if (drag.current && drag.current.type === "wire") setWirePt(null); drag.current = null; document.body.style.cursor = ""; }
    window.addEventListener("mousemove", move); window.addEventListener("mouseup", up);
    return () => { window.removeEventListener("mousemove", move); window.removeEventListener("mouseup", up); };
  }, []);
  React.useEffect(() => { const el = wrapRef.current; if (!el) return; function wheel(e) { e.preventDefault(); setZoom((z) => Math.min(1.5, Math.max(0.35, z * (e.deltaY < 0 ? 1.08 : 0.93)))); } el.addEventListener("wheel", wheel, { passive: false }); return () => el.removeEventListener("wheel", wheel); }, []);

  const onCanvasDown = (e) => {
    if (e.target.closest(".flow-node") || e.target.closest(".inspector") || e.target.closest(".flow-toolbar") || e.target.closest(".flow-controls") || e.target.closest(".edge-hit")) return;
    setSelId(null); setSelEdge(null); setPalette(false);
    drag.current = { type: "pan", sx: e.clientX, sy: e.clientY, ox: offset.x, oy: offset.y }; document.body.style.cursor = "grabbing";
  };
  const onNodeDown = (e, id) => { if (e.target.closest(".port-dot")) return; e.stopPropagation(); const n = nodeById(id); drag.current = { type: "node", id, sx: e.clientX, sy: e.clientY, nx: n.x, ny: n.y, zoom }; };
  const onWireStart = (e, nodeId, portId) => { e.stopPropagation(); const r = wrapRef.current.getBoundingClientRect(); drag.current = { type: "wire", fromNode: nodeId, fromPort: portId, rl: r.left, rt: r.top, ox: offset.x, oy: offset.y, zoom }; const p = portPos(nodeId, portId); setWirePt(p); };
  const onWireEnd = (toNodeId) => {
    const dd = drag.current; if (!dd || dd.type !== "wire") return;
    if (dd.fromNode === toNodeId) return;
    const fp = portOf(nodeById(dd.fromNode), dd.fromPort);
    setGraph((g) => {
      if (g.edges.some((ed) => ed.from.node === dd.fromNode && ed.from.port === dd.fromPort && ed.to.node === toNodeId)) return g;
      return { ...g, edges: [...g.edges, { id: gid("e"), from: { node: dd.fromNode, port: dd.fromPort }, to: { node: toNodeId, port: "in" }, kind: fp ? fp.kind : "flow" }] };
    });
    setWirePt(null);
  };

  const updateNode = (id, patch) => setGraph((g) => ({ ...g, nodes: g.nodes.map((n) => n.id === id ? { ...n, data: { ...n.data, ...patch } } : n) }));
  const deleteNode = (id) => setGraph((g) => ({ nodes: g.nodes.filter((n) => n.id !== id), edges: g.edges.filter((e) => e.from.node !== id && e.to.node !== id) }));
  const deleteEdge = (id) => { setGraph((g) => ({ ...g, edges: g.edges.filter((e) => e.id !== id) })); setSelEdge(null); };
  const addNode = (kind) => {
    const k = STEP_KINDS[kind];
    const data = kind === "wait" ? { days: 3 } : kind === "revisit" ? { days: 30, mode: "fixed" } : kind === "email" ? { subject: "", body: "", mode: "manual" } : kind === "schedule" ? { subject: "", body: "", mode: "manual", sendAt: "In 3 days · 9:00 AM" } : ["linkedin_msg", "ig_dm"].includes(kind) ? { body: "", mode: "manual" } : kind === "linkedin_connect" ? { body: "" } : { note: "" };
    const ref = nodeById(selId);
    const x = ref ? ref.x + (ref.w || 240) + 60 : 360;
    const y = ref ? ref.y : 300 - k.h / 2;
    const node = { id: gid("s"), type: "step", kind, x, y, w: k.w, h: k.h, data };
    setGraph((g) => ({ ...g, nodes: [...g.nodes, node] }));
    setSelId(node.id); setPalette(false);
  };
  const addTag = (tag) => {
    const ref = nodeById(selId);
    const x = ref ? ref.x + (ref.w || 200) + 60 : 360;
    const y = ref ? ref.y : 360;
    const node = { id: gid("t"), type: "tag", tag, x, y, w: 168, h: 64 };
    setGraph((g) => ({ ...g, nodes: [...g.nodes, node] }));
    setSelId(node.id); setPalette(false);
  };
  const addSplit = () => {
    const ref = nodeById(selId);
    const x = ref ? ref.x + (ref.w || 200) + 60 : 360;
    const y = ref ? ref.y : 280;
    const node = { id: gid("sp"), type: "split", x, y, w: 196, h: 104, data: { ratio: 50, labelA: "Variant A", labelB: "Variant B" } };
    setGraph((g) => ({ ...g, nodes: [...g.nodes, node] }));
    setSelId(node.id); setPalette(false);
  };

  const selNode = nodeById(selId);

  return (
    <div className="flow-wrap" ref={wrapRef} onMouseDown={onCanvasDown}>
      <div className="flow-world" style={{ transform: `translate(${offset.x}px, ${offset.y}px) scale(${zoom})` }}>
        <svg className="flow-edges" width="4000" height="1400">
          {edges.map((e) => {
            const p1 = portPos(e.from.node, e.from.port), p2 = portPos(e.to.node, e.to.port);
            const d = fPath(p1, p2), on = selEdge === e.id;
            return (<g key={e.id}>
              <path d={d} fill="none" stroke={FEDGE_COLOR[e.kind]} strokeWidth={on ? 3 : 2} strokeDasharray={e.kind === "flow" ? "5 5" : "0"} opacity={e.kind === "flow" ? 0.8 : 0.95} />
              <path className="edge-hit" d={d} fill="none" stroke="transparent" strokeWidth="16" style={{ cursor: "pointer", pointerEvents: "stroke" }} onMouseDown={(ev) => { ev.stopPropagation(); setSelEdge(e.id); setSelId(null); }} />
              <circle cx={p2.x} cy={p2.y} r="3" fill={FEDGE_COLOR[e.kind]} />
              {on && <g onMouseDown={(ev) => { ev.stopPropagation(); deleteEdge(e.id); }} style={{ cursor: "pointer", pointerEvents: "auto" }}>
                <circle cx={(p1.x + p2.x) / 2} cy={(p1.y + p2.y) / 2} r="11" fill="var(--red)" />
                <path d={`M ${(p1.x + p2.x) / 2 - 4} ${(p1.y + p2.y) / 2 - 4} l 8 8 M ${(p1.x + p2.x) / 2 + 4} ${(p1.y + p2.y) / 2 - 4} l -8 8`} stroke="#fff" strokeWidth="1.8" strokeLinecap="round" style={{ pointerEvents: "none" }} />
              </g>}
            </g>);
          })}
          {wirePt && drag.current && drag.current.type === "wire" && (() => { const p1 = portPos(drag.current.fromNode, drag.current.fromPort); return <path d={fPath(p1, wirePt)} fill="none" stroke="var(--accent)" strokeWidth="2.5" strokeDasharray="4 4" />; })()}
        </svg>
        {nodes.map((n) => <FlowNodeView key={n.id} node={n} selected={selId === n.id} count={countFor(n)} onDown={onNodeDown} onSelect={(id) => { setSelId(id); setSelEdge(null); }} onWireStart={onWireStart} onWireEnd={() => onWireEnd(n.id)} />)}
      </div>

      <div className="flow-toolbar">
        <button className="btn btn--primary btn--sm" onClick={() => setPalette((p) => !p)}><Icon name="plus" size={15} /> Add node</button>
        <span className="num flow-total"><Icon name="contacts" size={13} /> {total} in flow</span>
        {palette && (<div className="palette"><div className="menu-label">Add a step</div>{ADD_ORDER.map((kk) => { const m = STEP_KINDS[kk]; return <button key={kk} className="palette-item" onClick={() => addNode(kk)}><span className="palette-ico" style={{ color: m.color }}><Icon name={m.icon} size={15} /></span>{m.label}</button>; })}<div className="menu-label" style={{ marginTop: 4 }}>Logic</div><button className="palette-item" onClick={addSplit}><span className="palette-ico" style={{ color: "var(--lane-agency)" }}><Icon name="branch" size={15} /></span>A/B split</button><div className="menu-label" style={{ marginTop: 4 }}>Add an outcome / tag</div>{TAG_ORDER.map((tk) => { const m = TAG_KINDS[tk]; return <button key={tk} className="palette-item" onClick={() => addTag(tk)}><span className="palette-ico" style={{ color: m.color }}><Icon name={m.icon} size={15} /></span>{m.label}</button>; })}</div>)}
      </div>

      <div className="flow-controls">
        <button className="flow-ctrl" onClick={() => setZoom((z) => Math.min(1.5, z + 0.12))} title="Zoom in"><Icon name="plus" size={16} /></button>
        <button className="flow-ctrl" onClick={() => setZoom((z) => Math.max(0.35, z - 0.12))} title="Zoom out"><div style={{ width: 14, height: 2, background: "currentColor", borderRadius: 2 }}></div></button>
        <button className="flow-ctrl" onClick={() => fitTo(nodes)} title="Fit"><Icon name="grid" size={15} /></button>
        <div className="flow-zoom num">{Math.round(zoom * 100)}%</div>
      </div>

      <div className="flow-legend">
        <span><span className="lg-dot" style={{ background: "var(--green)" }}></span> reply / won</span>
        <span><span className="lg-dot" style={{ background: "var(--red)" }}></span> lost</span>
        <span><span className="lg-dash"></span> no reply → next</span>
        <span className="flow-legend-hint">drag a port to wire · click a connector to delete · drag nodes to move</span>
      </div>

      {selNode && <NodeInspector node={selNode} nodes={nodes} onUpdate={updateNode} onMove={() => {}} onDelete={deleteNode} onClose={() => setSelId(null)} />}
    </div>
  );
}

Object.assign(window, { FlowBuilder, STEP_KINDS });
