/* eslint-disable */
// HelmDeck — Worktree Canvas
// node-graph style interface for managing git worktrees

const { useState, useRef, useEffect, useMemo, useCallback } = React;

const HUE = {
  amber: { c: 'oklch(0.78 0.13 215)', soft: 'oklch(0.85 0.10 215)', deep: 'oklch(0.65 0.14 215)', glow: 'rgba(34,211,238,0.32)' },
  iris:  { c: 'oklch(0.68 0.16 280)', soft: 'oklch(0.78 0.12 280)', deep: 'oklch(0.55 0.18 280)', glow: 'rgba(138,122,216,0.32)' },
  sage:  { c: 'oklch(0.72 0.10 145)', soft: 'oklch(0.80 0.08 145)', deep: 'oklch(0.58 0.12 145)', glow: 'rgba(130,181,138,0.28)' },
  ember: { c: 'oklch(0.70 0.17 15)',  soft: 'oklch(0.80 0.13 15)',  deep: 'oklch(0.58 0.19 15)',  glow: 'rgba(251,113,133,0.30)' },
  slate: { c: 'oklch(0.72 0.04 250)', soft: 'oklch(0.80 0.03 250)', deep: 'oklch(0.55 0.05 250)', glow: 'rgba(171,177,189,0.18)' },
};

const STATUS_LABEL = {
  wip:      { text: 'working',  hue: 'iris'  },
  ready:    { text: 'ready to merge', hue: 'sage'  },
  conflict: { text: 'conflict', hue: 'ember' },
  stale:    { text: 'stale',    hue: 'slate' },
  draft:    { text: 'draft',    hue: 'slate' },
};

// ---------- helpers ----------
function clamp(v, a, b) { return Math.max(a, Math.min(b, v)); }

// pending state on a branched edge → pill + flowing-dot count (or null)
// forward = commits/changes to pull from main; reverse = commits ready to send to main
function pendingFor(edge, target) {
  if (edge.kind !== 'branched' || !target || target.kind !== 'worktree') return null;
  const behind = target.behind || 0;
  const dirty = target.dirty || 0;
  const ahead = target.ahead || 0;
  if (behind > 0 || dirty > 0) {
    const dots = clamp(Math.max(behind + Math.ceil(dirty / 4), 3), 3, 6);
    return { dir: 'forward', behind, dirty, ahead, dots };
  }
  if (ahead > 0) {
    return { dir: 'reverse', behind, dirty, ahead, dots: 1 };
  }
  return null;
}

function portPos(card, side) {
  return {
    x: side === 'out' ? card.x + card.w : card.x,
    y: card.y + card.h / 2,
  };
}

function bezierPath(a, b) {
  const dx = Math.max(70, Math.abs(b.x - a.x) * 0.55);
  // bias the source handle outward (right of source) and target handle inward (left of target)
  return `M ${a.x} ${a.y} C ${a.x + dx} ${a.y}, ${b.x - dx} ${b.y}, ${b.x} ${b.y}`;
}

// returns a path that loops back if target is to the left of source
function smartBezier(a, b, kind) {
  const goingLeft = b.x < a.x + 40;
  if (!goingLeft) return bezierPath(a, b);
  // loop above/below
  const verticalOffset = (a.y < b.y ? -120 : 120);
  const midY = (a.y + b.y) / 2 + verticalOffset;
  const c1 = { x: a.x + 160, y: a.y };
  const c2 = { x: a.x + 160, y: midY };
  const c3 = { x: b.x - 160, y: midY };
  const c4 = { x: b.x - 160, y: b.y };
  return `M ${a.x} ${a.y} C ${c1.x} ${c1.y}, ${c2.x} ${c2.y}, ${(a.x + b.x) / 2} ${midY} S ${c4.x} ${c4.y}, ${b.x} ${b.y}`;
}

// ---------- icons ----------
const Icon = {
  branch: (p) => (
    <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" {...p}>
      <circle cx="6" cy="6" r="2"/><circle cx="6" cy="18" r="2"/><circle cx="18" cy="9" r="2"/>
      <path d="M6 8v8"/><path d="M18 11c0 5-6 4-6 9"/>
    </svg>
  ),
  anchor: (p) => (
    <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" {...p}>
      <circle cx="12" cy="5" r="2.2"/><path d="M12 7v14"/><path d="M5 13a7 7 0 0 0 14 0"/><path d="M3 13h4"/><path d="M17 13h4"/>
    </svg>
  ),
  merge: (p) => (
    <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" {...p}>
      <circle cx="6" cy="6" r="2"/><circle cx="6" cy="18" r="2"/><circle cx="18" cy="18" r="2"/>
      <path d="M6 8v8"/><path d="M6 11c0 4 6 3 6 7"/>
    </svg>
  ),
  hand: (p) => (
    <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" {...p}>
      <path d="M9 11V5.5a1.5 1.5 0 0 1 3 0V11"/><path d="M12 11V4.5a1.5 1.5 0 0 1 3 0V11"/>
      <path d="M15 11V6a1.5 1.5 0 0 1 3 0v6c0 4-3 8-7 8-3 0-5-1-6-3l-3-5a1.7 1.7 0 0 1 2.6-2L7 14"/>
    </svg>
  ),
  plus: (p) => <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" {...p}><path d="M12 5v14M5 12h14"/></svg>,
  minus: (p) => <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" {...p}><path d="M5 12h14"/></svg>,
  reset: (p) => <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M3 12a9 9 0 1 0 3-6.7"/><path d="M3 4v5h5"/></svg>,
  layout: (p) => <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" {...p}><rect x="3" y="3" width="7" height="9" rx="1"/><rect x="14" y="3" width="7" height="5" rx="1"/><rect x="14" y="12" width="7" height="9" rx="1"/><rect x="3" y="16" width="7" height="5" rx="1"/></svg>,
  search: (p) => <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" {...p}><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg>,
  doc: (p) => <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M14 3H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"/><path d="M14 3v6h6"/></svg>,
  arrowUp: (p) => <svg width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M12 19V5"/><path d="m5 12 7-7 7 7"/></svg>,
  arrowDown: (p) => <svg width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M12 5v14"/><path d="m5 12 7 7 7-7"/></svg>,
  dot: (p) => <svg width="8" height="8" viewBox="0 0 8 8" {...p}><circle cx="4" cy="4" r="3.2" fill="currentColor"/></svg>,
  warn: (p) => <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M10.3 3.7 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.7a2 2 0 0 0-3.4 0Z"/><path d="M12 9v4"/><path d="M12 17h.01"/></svg>,
  check: (p) => <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M20 6 9 17l-5-5"/></svg>,
  folder: (p) => <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg>,
  x: (p) => <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" {...p}><path d="M18 6 6 18M6 6l12 12"/></svg>,
  clock: (p) => <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...p}><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></svg>,
};

// ---------- card ----------
function CardBody({ card, isSelected, onMouseDown, onPortDown, onSelect }) {
  const hue = HUE[card.hue];
  const isBase = card.kind === 'base';

  return (
    <div
      data-card={card.id}
      className={'wt-card' + (isBase ? ' base' : '') + (isSelected ? ' selected' : '')}
      onMouseDown={(e) => { onSelect(card.id); onMouseDown(card.id, e); }}
      style={{
        position: 'absolute',
        left: card.x, top: card.y, width: card.w, height: card.h,
        background: isBase
          ? 'linear-gradient(180deg, rgba(28,26,22,0.96) 0%, rgba(19,19,22,0.96) 100%)'
          : 'linear-gradient(180deg, rgba(22,22,25,0.96) 0%, rgba(15,15,18,0.96) 100%)',
        border: `1px solid ${isSelected ? hue.c : 'rgba(255,255,255,0.07)'}`,
        borderRadius: 14,
        boxShadow: isSelected
          ? `0 0 0 1px ${hue.c}, 0 0 28px ${hue.glow}, 0 18px 60px rgba(0,0,0,0.55)`
          : isBase
            ? `0 0 36px ${hue.glow}, 0 18px 60px rgba(0,0,0,0.55), inset 0 1px 0 rgba(255,255,255,0.05)`
            : '0 12px 36px rgba(0,0,0,0.45), inset 0 1px 0 rgba(255,255,255,0.04)',
        cursor: 'grab',
        userSelect: 'none',
        transition: 'border-color 140ms ease, box-shadow 140ms ease',
        backdropFilter: 'blur(8px)',
      }}
    >
      {/* left hue stripe */}
      <div style={{
        position: 'absolute', left: 0, top: 10, bottom: 10, width: 3,
        background: hue.c,
        borderRadius: 3,
        boxShadow: `0 0 12px ${hue.glow}`,
      }}/>

      {/* in-port (left) */}
      {!isBase && (
        <div
          data-port-in={card.id}
          style={{
            position: 'absolute', left: -7, top: card.h / 2 - 7, width: 14, height: 14,
            borderRadius: 14, background: '#0c0c0e',
            border: `2px solid ${hue.c}`,
            boxShadow: `0 0 8px ${hue.glow}`,
            zIndex: 3,
          }}
        />
      )}

      {/* out-port (right) — drag handle */}
      <div
        data-port-out={card.id}
        onMouseDown={(e) => onPortDown(card.id, e)}
        title="drag to merge into another branch"
        style={{
          position: 'absolute', right: -7, top: card.h / 2 - 7, width: 14, height: 14,
          borderRadius: 14, background: hue.c,
          border: '2px solid #0c0c0e',
          boxShadow: `0 0 10px ${hue.glow}`,
          cursor: 'crosshair',
          zIndex: 3,
        }}
      />

      {/* content */}
      {isBase ? (
        <BaseInner card={card} hue={hue}/>
      ) : (
        <WorktreeInner card={card} hue={hue}/>
      )}
    </div>
  );
}

function BaseInner({ card, hue }) {
  return (
    <div style={{ padding: '14px 16px 14px 18px', height: '100%', display: 'flex', flexDirection: 'column', gap: 6 }}>
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
          <span style={{ color: hue.c, display: 'inline-flex' }}><Icon.anchor/></span>
          <span style={{ fontFamily: 'ui-monospace, "SF Mono", Menlo, monospace', fontSize: 10, letterSpacing: '0.14em', textTransform: 'uppercase', color: hue.soft, fontWeight: 600 }}>Base · upstream</span>
        </div>
        <span style={{
          fontFamily: 'ui-monospace, "SF Mono", Menlo, monospace', fontSize: 9.5,
          color: 'var(--text-muted)', letterSpacing: '0.06em', textTransform: 'uppercase',
        }}>head</span>
      </div>

      <div style={{
        fontSize: 19, fontWeight: 600, letterSpacing: '-0.015em',
        color: 'var(--paper-300)', lineHeight: 1.1, marginTop: 2,
        fontFamily: 'ui-monospace, "SF Mono", Menlo, monospace',
      }}>main</div>

      <div style={{ fontSize: 11.5, color: 'var(--text-muted)', fontFamily: 'ui-monospace, "SF Mono", Menlo, monospace', marginTop: 2 }}>
        {card.upstream}
      </div>

      <div style={{
        marginTop: 'auto', display: 'flex', alignItems: 'flex-end', justifyContent: 'space-between', gap: 10,
      }}>
        <div>
          <div style={{ fontSize: 11, color: 'var(--text-body)', lineHeight: 1.35, marginBottom: 4, fontWeight: 450 }}>
            {card.headMsg}
          </div>
          <div style={{ display: 'flex', alignItems: 'center', gap: 8, fontFamily: 'ui-monospace, "SF Mono", Menlo, monospace', fontSize: 10.5, color: 'var(--text-muted)' }}>
            <span>{card.head}</span>
            <span style={{ opacity: 0.4 }}>·</span>
            <span>{card.commits} commits</span>
            <span style={{ opacity: 0.4 }}>·</span>
            <span>{card.pushed}</span>
          </div>
        </div>
      </div>
    </div>
  );
}

function WorktreeInner({ card, hue }) {
  const st = STATUS_LABEL[card.status];
  const stHue = HUE[st.hue];
  return (
    <div style={{ padding: '12px 14px 12px 16px', height: '100%', display: 'flex', flexDirection: 'column', gap: 6 }}>
      {/* row 1: parent + status pill */}
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 6, fontFamily: 'ui-monospace, "SF Mono", Menlo, monospace', fontSize: 9.5, color: 'var(--text-muted)', letterSpacing: '0.06em', textTransform: 'uppercase' }}>
          <Icon.branch style={{ width: 10, height: 10 }}/>
          <span>from {card.parent === 'base' ? 'main' : card.parent}</span>
        </div>
        <div style={{
          display: 'inline-flex', alignItems: 'center', gap: 5,
          padding: '2px 7px 2px 7px',
          borderRadius: 100,
          background: `color-mix(in oklab, ${stHue.c} 16%, transparent)`,
          color: stHue.soft,
          fontSize: 10, fontWeight: 600, letterSpacing: '0.02em',
        }}>
          <span style={{ width: 5, height: 5, borderRadius: 5, background: stHue.c }}/>
          {st.text}
        </div>
      </div>

      {/* row 2: branch name */}
      <div style={{
        fontSize: 14.5, fontWeight: 600, letterSpacing: '-0.005em',
        color: 'var(--paper-300)', lineHeight: 1.15,
        fontFamily: 'ui-monospace, "SF Mono", Menlo, monospace',
        wordBreak: 'break-all',
      }}>{card.branch}</div>

      {/* row 3: head commit msg */}
      <div style={{ fontSize: 11, color: 'var(--text-muted)', lineHeight: 1.35 }}>
        {card.headMsg}
      </div>

      {/* row 4: ahead/behind/dirty */}
      <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginTop: 'auto' }}>
        {card.ahead > 0 && (
          <span style={{ display: 'inline-flex', alignItems: 'center', gap: 4, fontSize: 10.5, color: hue.soft, fontFamily: 'ui-monospace, monospace' }}>
            <Icon.arrowUp/>{card.ahead}
          </span>
        )}
        {card.behind > 0 && (
          <span style={{ display: 'inline-flex', alignItems: 'center', gap: 4, fontSize: 10.5, color: 'var(--text-muted)', fontFamily: 'ui-monospace, monospace' }}>
            <Icon.arrowDown/>{card.behind}
          </span>
        )}
        {card.dirty > 0 && (
          <span style={{ display: 'inline-flex', alignItems: 'center', gap: 4, fontSize: 10.5, color: 'oklch(0.80 0.13 15)', fontFamily: 'ui-monospace, monospace' }}>
            <span style={{ width: 5, height: 5, borderRadius: 5, background: 'oklch(0.70 0.17 15)' }}/>
            {card.dirty} dirty
          </span>
        )}
        <span style={{ marginLeft: 'auto', display: 'inline-flex', alignItems: 'center', gap: 4, fontSize: 10, color: 'var(--text-faint)', fontFamily: 'ui-monospace, monospace' }}>
          <Icon.clock/> {card.last}
        </span>
      </div>

      {/* row 5: hash */}
      <div style={{
        display: 'flex', alignItems: 'center', justifyContent: 'space-between',
        paddingTop: 6, borderTop: '1px solid rgba(255,255,255,0.05)',
        fontFamily: 'ui-monospace, "SF Mono", Menlo, monospace', fontSize: 10,
        color: 'var(--text-faint)',
      }}>
        <span>{card.head}</span>
      </div>
    </div>
  );
}

// ---------- edge ----------
const EDGE_AMBER = 'oklch(0.76 0.15 70)';   // dashed lineage line (matches app)
const COMMIT_DOT = 'oklch(0.68 0.16 280)';  // iris — pending commit dots

function Edge({ id, from, to, kind, fromHue, toHue, hovered, onHover, onClick, pending }) {
  const a = portPos(from, 'out');
  const b = portPos(to, 'in');
  const d = smartBezier(a, b, kind);
  const isMerge = kind === 'merge-intent';
  const isConflict = kind === 'conflict';
  const isBranched = kind === 'branched';
  const stroke = isMerge ? HUE.amber.c : isConflict ? HUE.ember.c : isBranched ? EDGE_AMBER : HUE[fromHue].c;
  const dash = isBranched ? '7 7' : isMerge ? '6 5' : isConflict ? '3 4' : null;
  const glow = isBranched ? 'rgba(245,179,71,0.28)' : HUE[fromHue].glow;
  const pathId = 'edgepath-' + id;
  const dots = pending ? pending.dots : 0;
  const dur = 6.5;

  return (
    <g style={{ cursor: 'pointer' }} onMouseEnter={onHover} onMouseLeave={() => onHover(null)} onClick={onClick}>
      {/* glow underlay */}
      <path d={d} stroke={stroke} strokeWidth={hovered ? 7 : 5} fill="none"
            opacity={hovered ? 0.20 : 0.10} strokeLinecap="round"/>
      {/* main stroke */}
      <path id={pathId} d={d} stroke={stroke} strokeWidth={hovered ? 2 : 1.4} fill="none"
            strokeDasharray={dash} strokeLinecap="round"
            style={{ filter: `drop-shadow(0 0 6px ${glow})` }}/>
      {/* arrowhead for merge intent */}
      {isMerge && (
        <circle cx={b.x} cy={b.y} r="3.5" fill={stroke}
                style={{ filter: `drop-shadow(0 0 6px ${HUE.amber.glow})` }}/>
      )}
      {/* flowing commit dots — forward = pull from main, reverse = ready to send to main */}
      {dots > 0 && Array.from({ length: dots }).map((_, i) => (
        <circle key={i} r="4.5" fill={COMMIT_DOT}
                style={{ filter: `drop-shadow(0 0 6px rgba(138,122,216,0.55))` }}>
          <animateMotion dur={dur + 's'} repeatCount="indefinite"
                         begin={(-(i * dur) / dots).toFixed(2) + 's'}
                         calcMode="linear"
                         keyPoints={pending && pending.dir === 'reverse' ? '1;0' : '0;1'}
                         keyTimes="0;1">
            <mpath href={'#' + pathId} />
          </animateMotion>
        </circle>
      ))}
    </g>
  );
}

// ---------- edge pill (behind · dirty, or ahead when ready) ----------
function EdgePill({ x, y, pending }) {
  const reverse = pending.dir === 'reverse';
  return (
    <div style={{
      position: 'absolute', left: x, top: y, transform: 'translate(-50%, -50%)',
      display: 'inline-flex', alignItems: 'center', gap: 7,
      padding: '4px 9px', borderRadius: 100,
      background: 'rgba(18,16,20,0.92)',
      border: '1px solid rgba(255,255,255,0.09)',
      boxShadow: '0 4px 14px rgba(0,0,0,0.5)',
      fontFamily: 'ui-monospace, "SF Mono", Menlo, monospace',
      fontSize: 11, fontWeight: 600, whiteSpace: 'nowrap',
      pointerEvents: 'none', zIndex: 4,
    }}>
      {reverse ? (
        <span style={{ display: 'inline-flex', alignItems: 'center', gap: 3, color: HUE.sage.soft }}>
          <Icon.arrowUp/>{pending.ahead}
        </span>
      ) : (
        <>
          <span style={{ display: 'inline-flex', alignItems: 'center', gap: 3, color: EDGE_AMBER }}>
            <Icon.arrowDown/>{pending.behind}
          </span>
          <span style={{ opacity: 0.4, color: 'var(--text-muted)' }}>·</span>
          <span style={{ display: 'inline-flex', alignItems: 'center', gap: 4, color: 'oklch(0.80 0.12 280)' }}>
            <span style={{ width: 6, height: 6, borderRadius: 6, background: COMMIT_DOT }}/>{pending.dirty}
          </span>
        </>
      )}
    </div>
  );
}

// ---------- temp drag edge ----------
function DragEdge({ from, x, y }) {
  const a = portPos(from, 'out');
  const b = { x, y };
  const d = bezierPath(a, b);
  return (
    <g>
      <path d={d} stroke={HUE.amber.c} strokeWidth="6" fill="none" opacity="0.18" strokeLinecap="round"/>
      <path d={d} stroke={HUE.amber.c} strokeWidth="1.6" fill="none" strokeDasharray="5 5" strokeLinecap="round"
            style={{ filter: `drop-shadow(0 0 8px ${HUE.amber.glow})` }}/>
      <circle cx={b.x} cy={b.y} r="6" fill="none" stroke={HUE.amber.c} strokeWidth="1.5" opacity="0.7"/>
      <circle cx={b.x} cy={b.y} r="2.5" fill={HUE.amber.c}/>
    </g>
  );
}

// ---------- main ----------
function WorktreeCanvas() {
  const [cards, setCards] = useState(window.INITIAL_CARDS);
  const [edges, setEdges] = useState(window.INITIAL_EDGES);
  const [view, setView] = useState({ tx: 40, ty: -20, scale: 0.78 });
  const [selected, setSelected] = useState('w2');
  const [hoveredEdge, setHoveredEdge] = useState(null);
  const [dragPort, setDragPort] = useState(null);
  const [filter, setFilter] = useState('all');
  const stageRef = useRef(null);

  // refs mirroring latest state — used by gesture/zoom handlers to avoid stale closures
  const viewRef = useRef(view); viewRef.current = view;
  const cardsRef = useRef(cards); cardsRef.current = cards;

  const SCALE_MIN = 0.2, SCALE_MAX = 2.4;

  // zoom around a screen point (defaults to stage center)
  const zoomBy = useCallback((factor, clientX, clientY) => {
    const el = stageRef.current; if (!el) return;
    const rect = el.getBoundingClientRect();
    const ox = (clientX == null ? rect.width / 2 : clientX - rect.left);
    const oy = (clientY == null ? rect.height / 2 : clientY - rect.top);
    const v = viewRef.current;
    const scale = clamp(v.scale * factor, SCALE_MIN, SCALE_MAX);
    const k = scale / v.scale;
    setView({ scale, tx: ox - (ox - v.tx) * k, ty: oy - (oy - v.ty) * k });
  }, []);

  // fit all cards into the visible stage, centered
  const fitView = useCallback(() => {
    const el = stageRef.current; if (!el) return;
    const rect = el.getBoundingClientRect();
    if (!rect.width || !rect.height) return;
    const cs = cardsRef.current;
    const bx0 = Math.min(...cs.map(c => c.x));
    const by0 = Math.min(...cs.map(c => c.y));
    const bx1 = Math.max(...cs.map(c => c.x + c.w));
    const by1 = Math.max(...cs.map(c => c.y + c.h));
    const cw = bx1 - bx0, ch = by1 - by0;
    const pad = 44;
    const scale = clamp(Math.min((rect.width - pad * 2) / cw, (rect.height - pad * 2) / ch), SCALE_MIN, 1.1);
    setView({
      scale,
      tx: (rect.width - cw * scale) / 2 - bx0 * scale,
      ty: (rect.height - ch * scale) / 2 - by0 * scale,
    });
  }, []);

  const cardMap = useMemo(() => Object.fromEntries(cards.map(c => [c.id, c])), [cards]);

  const worldFromScreen = useCallback((sx, sy) => {
    const rect = stageRef.current.getBoundingClientRect();
    return {
      x: (sx - rect.left - view.tx) / view.scale,
      y: (sy - rect.top - view.ty) / view.scale,
    };
  }, [view]);

  // wheel: trackpad pinch (ctrlKey) zooms; plain wheel is swallowed so it doesn't scroll the page
  useEffect(() => {
    const el = stageRef.current;
    if (!el) return;
    const onWheel = (e) => {
      e.preventDefault();
      if (e.ctrlKey) zoomBy(Math.exp(-e.deltaY * 0.01), e.clientX, e.clientY);
    };
    el.addEventListener('wheel', onWheel, { passive: false });
    return () => el.removeEventListener('wheel', onWheel);
  }, [zoomBy]);

  // touch: one finger pans, two fingers pinch-zoom
  useEffect(() => {
    const el = stageRef.current;
    if (!el) return;
    const distOf = (t) => Math.hypot(t[0].clientX - t[1].clientX, t[0].clientY - t[1].clientY);
    const midOf = (t) => ({ x: (t[0].clientX + t[1].clientX) / 2, y: (t[0].clientY + t[1].clientY) / 2 });
    let mode = null, startDist = 0, startView = null, startMid = null, panStart = null;
    const onStart = (e) => {
      if (e.target.closest('[data-card], [data-port-in], [data-port-out]')) return;
      if (e.touches.length === 2) {
        mode = 'pinch'; startDist = distOf(e.touches); startMid = midOf(e.touches); startView = viewRef.current;
      } else if (e.touches.length === 1) {
        mode = 'pan'; panStart = { x: e.touches[0].clientX, y: e.touches[0].clientY }; startView = viewRef.current;
        setSelected(null);
      }
    };
    const onMove = (e) => {
      if (!mode) return;
      e.preventDefault();
      const rect = el.getBoundingClientRect();
      if (mode === 'pinch' && e.touches.length === 2) {
        const d = distOf(e.touches), m = midOf(e.touches);
        const scale = clamp(startView.scale * (d / startDist), SCALE_MIN, SCALE_MAX);
        const k = scale / startView.scale;
        const ox = startMid.x - rect.left, oy = startMid.y - rect.top;
        setView({
          scale,
          tx: ox - (ox - startView.tx) * k + (m.x - startMid.x),
          ty: oy - (oy - startView.ty) * k + (m.y - startMid.y),
        });
      } else if (mode === 'pan' && e.touches.length === 1) {
        setView({
          ...startView,
          tx: startView.tx + (e.touches[0].clientX - panStart.x),
          ty: startView.ty + (e.touches[0].clientY - panStart.y),
        });
      }
    };
    const onEnd = (e) => { if (e.touches.length === 0) mode = null; };
    el.addEventListener('touchstart', onStart, { passive: false });
    el.addEventListener('touchmove', onMove, { passive: false });
    el.addEventListener('touchend', onEnd);
    el.addEventListener('touchcancel', onEnd);
    return () => {
      el.removeEventListener('touchstart', onStart);
      el.removeEventListener('touchmove', onMove);
      el.removeEventListener('touchend', onEnd);
      el.removeEventListener('touchcancel', onEnd);
    };
  }, []);

  // fit to content on mount and when the stage resizes
  useEffect(() => {
    const raf = requestAnimationFrame(fitView);
    const onResize = () => fitView();
    window.addEventListener('resize', onResize);
    return () => { cancelAnimationFrame(raf); window.removeEventListener('resize', onResize); };
  }, [fitView]);

  const startPan = (e) => {
    if (e.button !== 0) return;
    if (e.target.closest('[data-card], [data-port-in], [data-port-out]')) return;
    const sx0 = e.clientX, sy0 = e.clientY;
    const { tx, ty } = view;
    const move = (ev) => setView(v => ({ ...v, tx: tx + (ev.clientX - sx0), ty: ty + (ev.clientY - sy0) }));
    const up = () => {
      window.removeEventListener('mousemove', move);
      window.removeEventListener('mouseup', up);
      document.body.style.cursor = '';
    };
    window.addEventListener('mousemove', move);
    window.addEventListener('mouseup', up);
    document.body.style.cursor = 'grabbing';
    setSelected(null);
  };

  const startCardDrag = (id, e) => {
    const start = worldFromScreen(e.clientX, e.clientY);
    const card = cardMap[id];
    const offX = start.x - card.x, offY = start.y - card.y;
    const move = (ev) => {
      const p = worldFromScreen(ev.clientX, ev.clientY);
      setCards(cs => cs.map(c => c.id === id ? { ...c, x: p.x - offX, y: p.y - offY } : c));
    };
    const up = () => {
      window.removeEventListener('mousemove', move);
      window.removeEventListener('mouseup', up);
      document.body.style.cursor = '';
    };
    window.addEventListener('mousemove', move);
    window.addEventListener('mouseup', up);
    document.body.style.cursor = 'grabbing';
  };

  const startPortDrag = (fromId, e) => {
    e.stopPropagation();
    e.preventDefault();
    const fromCard = cardMap[fromId];
    const startWorld = { x: fromCard.x + fromCard.w, y: fromCard.y + fromCard.h / 2 };
    setDragPort({ from: fromId, x: startWorld.x, y: startWorld.y });
    const move = (ev) => {
      const p = worldFromScreen(ev.clientX, ev.clientY);
      setDragPort({ from: fromId, x: p.x, y: p.y });
    };
    const up = (ev) => {
      window.removeEventListener('mousemove', move);
      window.removeEventListener('mouseup', up);
      const el = document.elementFromPoint(ev.clientX, ev.clientY);
      const portEl = el && el.closest('[data-port-in]');
      const cardEl = el && el.closest('[data-card]');
      let toId = null;
      if (portEl) toId = portEl.getAttribute('data-port-in');
      else if (cardEl) toId = cardEl.getAttribute('data-card');
      if (toId && toId !== fromId) {
        setEdges(es => {
          // remove any existing intent for this pair
          const filtered = es.filter(e => !(e.from === fromId && e.to === toId && e.kind === 'merge-intent'));
          return [...filtered, { id: 'mi-' + Date.now(), from: fromId, to: toId, kind: 'merge-intent' }];
        });
      }
      setDragPort(null);
    };
    window.addEventListener('mousemove', move);
    window.addEventListener('mouseup', up);
  };

  const resetView = () => fitView();

  // bounds for SVG world
  const worldW = 1700, worldH = 1100;

  // pending merge intents (for inspector)
  const pendingMerges = edges.filter(e => e.kind === 'merge-intent');
  const selCard = selected ? cardMap[selected] : null;

  // counts for filter chips
  const counts = useMemo(() => {
    const r = { all: 0, wip: 0, ready: 0, conflict: 0, stale: 0 };
    cards.forEach(c => {
      if (c.kind === 'worktree') {
        r.all++;
        if (c.status === 'wip' || c.status === 'draft') r.wip++;
        if (c.status === 'ready') r.ready++;
        if (c.status === 'conflict') r.conflict++;
        if (c.status === 'stale') r.stale++;
      }
    });
    return r;
  }, [cards]);

  const removeEdge = (id) => setEdges(es => es.filter(e => e.id !== id));

  return (
    <div className="canvas-root">
      {/* TOP BAR */}
      <div className="topbar">
        <div className="topbar-left">
          <span className="brand">
            <svg width="18" height="18" viewBox="0 0 44 44" style={{ marginRight: 8 }}>
              <circle cx="22" cy="22" r="13" fill="none" stroke="#f0e7d6" strokeWidth="1.6" opacity="0.92"/>
              <circle cx="22" cy="22" r="1.2" fill="#f0e7d6"/>
              <line x1="22" y1="6.5" x2="22" y2="2.5" stroke="oklch(0.78 0.13 215)" strokeWidth="2.4" strokeLinecap="round"/>
              <line x1="34.5" y1="13" x2="37.4" y2="11.2" stroke="#f0e7d6" strokeWidth="1.5" strokeLinecap="round" opacity="0.85"/>
              <line x1="38" y1="22" x2="41.5" y2="22" stroke="#f0e7d6" strokeWidth="1.5" strokeLinecap="round" opacity="0.85"/>
              <line x1="34.5" y1="31" x2="37.4" y2="32.8" stroke="#f0e7d6" strokeWidth="1.5" strokeLinecap="round" opacity="0.85"/>
              <line x1="22" y1="35.5" x2="22" y2="39.5" stroke="#f0e7d6" strokeWidth="1.5" strokeLinecap="round" opacity="0.85"/>
              <line x1="9.5" y1="31" x2="6.6" y2="32.8" stroke="#f0e7d6" strokeWidth="1.5" strokeLinecap="round" opacity="0.85"/>
              <line x1="6" y1="22" x2="2.5" y2="22" stroke="#f0e7d6" strokeWidth="1.5" strokeLinecap="round" opacity="0.85"/>
              <line x1="9.5" y1="13" x2="6.6" y2="11.2" stroke="#f0e7d6" strokeWidth="1.5" strokeLinecap="round" opacity="0.85"/>
            </svg>
            Helm<span style={{ fontWeight: 400, opacity: 0.78 }}>Deck</span>
          </span>
          <span className="crumb-sep">/</span>
          <span className="crumb"><Icon.folder/> helmdeck</span>
          <span className="crumb-sep">/</span>
          <span className="crumb active">worktree canvas</span>
        </div>
        <div className="topbar-right">
          <div className="search">
            <Icon.search/>
            <input placeholder="filter branches…" />
          </div>
          <button className="iconbtn">
            <Icon.plus/> <span>new worktree</span>
          </button>
        </div>
      </div>

      {/* FILTER CHIPS */}
      <div className="filterbar">
        <span className="bar-label">worktrees</span>
        {[
          { id: 'all', label: 'all', hue: 'amber' },
          { id: 'wip', label: 'working', hue: 'iris' },
          { id: 'ready', label: 'ready', hue: 'sage' },
          { id: 'conflict', label: 'conflict', hue: 'ember' },
          { id: 'stale', label: 'stale', hue: 'slate' },
        ].map(f => {
          const active = filter === f.id;
          const h = HUE[f.hue];
          return (
            <button key={f.id}
              onClick={() => setFilter(f.id)}
              className={'chip ' + (active ? 'active' : '')}
              style={{
                ['--chip-c']: h.c, ['--chip-soft']: h.soft, ['--chip-glow']: h.glow,
              }}
            >
              <span className="chip-dot" />
              {f.label}
              <span className="chip-count">{counts[f.id]}</span>
            </button>
          );
        })}

        <div className="filterbar-spacer"/>

        <div className="merge-queue">
          <Icon.merge/>
          <span>{pendingMerges.length}</span>
          <span className="mq-label">pending merges</span>
        </div>
      </div>

      {/* STAGE */}
      <div className="stage" ref={stageRef} onMouseDown={startPan}>
        <div className="grid-bg" />

        <div className="world" style={{
          transform: `translate(${view.tx}px, ${view.ty}px) scale(${view.scale})`,
          transformOrigin: '0 0',
        }}>
          {/* lineage labels */}
          <div className="lane-label" style={{ left: 80, top: 330 }}>
            <span className="lane-tag" style={{ background: 'color-mix(in oklab, oklch(0.78 0.13 215) 18%, transparent)', color: 'oklch(0.85 0.10 215)' }}>BASE</span>
            <span className="lane-hint">upstream · pull target</span>
          </div>
          <div className="lane-label" style={{ left: 540, top: 30 }}>
            <span className="lane-tag" style={{ background: 'rgba(255,255,255,0.05)', color: 'var(--text-muted)' }}>BRANCHED FROM main</span>
          </div>

          <svg className="edges" width={worldW} height={worldH} style={{ position: 'absolute', inset: 0, pointerEvents: 'none' }}>
            <g style={{ pointerEvents: 'all' }}>
              {edges.map(e => {
                const f = cardMap[e.from], t = cardMap[e.to];
                if (!f || !t) return null;
                const pend = pendingFor(e, t);
                return (
                  <Edge key={e.id} id={e.id} from={f} to={t} kind={e.kind}
                        fromHue={f.hue} toHue={t.hue}
                        hovered={hoveredEdge === e.id}
                        onHover={(v) => setHoveredEdge(v === null ? null : e.id)}
                        onClick={() => e.kind === 'merge-intent' && removeEdge(e.id)}
                        pending={pend}/>
                );
              })}
              {dragPort && <DragEdge from={cardMap[dragPort.from]} x={dragPort.x} y={dragPort.y}/>}
            </g>
          </svg>

          {/* edge pills — behind · dirty */}
          {edges.map(e => {
            const f = cardMap[e.from], t = cardMap[e.to];
            if (!f || !t) return null;
            const pend = pendingFor(e, t);
            if (!pend) return null;
            const a = portPos(f, 'out'), b = portPos(t, 'in');
            return <EdgePill key={'pill-' + e.id} x={(a.x + b.x) / 2} y={(a.y + b.y) / 2}
                             pending={pend}/>;
          })}

          {cards.map(c => (
            <CardBody
              key={c.id}
              card={c}
              isSelected={selected === c.id}
              onMouseDown={startCardDrag}
              onPortDown={startPortDrag}
              onSelect={setSelected}
            />
          ))}
        </div>

        {/* TOOLBAR — top right (zoom + recenter) */}
        <div className="tools tools-zoom">
          <button className="tool-btn" onClick={() => zoomBy(1.25)} title="zoom in"><Icon.plus/></button>
          <button className="tool-btn" onClick={() => zoomBy(1 / 1.25)} title="zoom out"><Icon.minus/></button>
          <button className="tool-btn" onClick={resetView} title="fit to view"><Icon.reset/></button>
        </div>

        {/* HINT */}
        <div className="hint">
          <Icon.hand/>
          <span>drag to move</span>
        </div>

        {/* LEGEND */}
        <div className="legend">
          <span className="lg-title">connection</span>
          <span className="lg-row"><svg width="22" height="6"><line x1="0" y1="3" x2="22" y2="3" stroke="oklch(0.68 0.16 280)" strokeWidth="1.4"/></svg> branched</span>
          <span className="lg-row"><svg width="22" height="6"><line x1="0" y1="3" x2="22" y2="3" stroke="oklch(0.78 0.13 215)" strokeWidth="1.4" strokeDasharray="3 3"/></svg> merge intent</span>
          <span className="lg-row"><svg width="22" height="6"><line x1="0" y1="3" x2="22" y2="3" stroke="oklch(0.70 0.17 15)" strokeWidth="1.4" strokeDasharray="2 2"/></svg> conflict</span>
        </div>

        {/* INSPECTOR */}
        {selCard && <Inspector card={selCard} cardMap={cardMap} edges={edges} setEdges={setEdges} onClose={() => setSelected(null)}/>}
      </div>
    </div>
  );
}

function Inspector({ card, cardMap, edges, setEdges, onClose }) {
  const hue = HUE[card.hue];
  const isBase = card.kind === 'base';
  const incoming = edges.filter(e => e.to === card.id);
  const outgoing = edges.filter(e => e.from === card.id);
  const pendingMerge = outgoing.find(e => e.kind === 'merge-intent');
  const parentCard = card.parent && cardMap[card.parent];

  return (
    <aside className="inspector">
      <div className="ins-head">
        <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
          <span style={{ width: 8, height: 8, borderRadius: 8, background: hue.c, boxShadow: `0 0 10px ${hue.glow}` }}/>
          <span className="ins-eyebrow">{isBase ? 'base branch' : 'worktree · selected'}</span>
        </div>
        <button className="ins-x" onClick={onClose}><Icon.x/></button>
      </div>

      <div className="ins-name" style={{ fontFamily: 'ui-monospace, "SF Mono", Menlo, monospace' }}>{card.branch}</div>
      {!isBase && (
        <div className="ins-path"><Icon.folder/> {card.path}</div>
      )}

      <div className="ins-divider"/>

      {!isBase && (
        <>
          <div className="ins-stats">
            <div className="stat">
              <div className="stat-num" style={{ color: hue.soft }}>{card.ahead}</div>
              <div className="stat-lbl">ahead</div>
            </div>
            <div className="stat">
              <div className="stat-num">{card.behind}</div>
              <div className="stat-lbl">behind</div>
            </div>
            <div className="stat">
              <div className="stat-num" style={{ color: card.dirty > 0 ? 'oklch(0.80 0.13 15)' : undefined }}>{card.dirty}</div>
              <div className="stat-lbl">dirty</div>
            </div>
            <div className="stat">
              <div className="stat-num">{card.files}</div>
              <div className="stat-lbl">files</div>
            </div>
          </div>

          <div className="ins-divider"/>

          <div className="ins-section-title">head</div>
          <div className="ins-head-commit">
            <span className="ins-hash">{card.head}</span>
            <span className="ins-msg">{card.headMsg}</span>
            <span className="ins-meta">{card.last}</span>
          </div>

          <div className="ins-section-title">lineage</div>
          <div className="ins-lineage">
            <span className="line-row">
              <Icon.branch/> branched from
              <span className="line-pill">{card.parent === 'base' ? 'main' : parentCard.branch}</span>
            </span>
          </div>
        </>
      )}

      {isBase && (
        <>
          <div className="ins-stats">
            <div className="stat">
              <div className="stat-num">{card.commits}</div>
              <div className="stat-lbl">commits</div>
            </div>
            <div className="stat" style={{ flex: 2 }}>
              <div className="stat-num" style={{ fontSize: 12, fontFamily: 'ui-monospace, monospace' }}>{card.upstream}</div>
              <div className="stat-lbl">upstream</div>
            </div>
          </div>
          <div className="ins-divider"/>
          <div className="ins-section-title">incoming merges</div>
          <div className="ins-lineage">
            {incoming.length === 0 && <div className="empty">no pending merges into main</div>}
            {incoming.map(e => {
              const src = cardMap[e.from];
              if (!src) return null;
              const sHue = HUE[src.hue];
              return (
                <div key={e.id} className="merge-row">
                  <span className="mr-dot" style={{ background: sHue.c, boxShadow: `0 0 8px ${sHue.glow}` }}/>
                  <span className="mr-name" style={{ fontFamily: 'ui-monospace, monospace' }}>{src.branch}</span>
                  <span className="mr-kind">{e.kind === 'merge-intent' ? 'pending' : 'merged'}</span>
                </div>
              );
            })}
          </div>
        </>
      )}

      {!isBase && (
        <>
          <div className="ins-section-title">merge target</div>
          {pendingMerge ? (
            <div className="merge-target staged">
              <div className="mt-line">
                <span style={{ fontFamily: 'ui-monospace, monospace' }}>{card.branch}</span>
                <span className="mt-arrow">→</span>
                <span style={{ fontFamily: 'ui-monospace, monospace' }}>{pendingMerge.to === 'base' ? 'main' : cardMap[pendingMerge.to].branch}</span>
              </div>
              <div className="mt-meta">staged · drop port on a card to retarget</div>
              <div className="mt-actions">
                <button className="btn primary"><Icon.merge/> merge now</button>
                <button className="btn ghost" onClick={() => setEdges(es => es.filter(x => x.id !== pendingMerge.id))}>cancel</button>
              </div>
            </div>
          ) : (
            <div className="merge-target">
              <div className="mt-empty">no merge target — drag the right port onto another card</div>
            </div>
          )}

          <div className="ins-section-title">actions</div>
          <div className="action-grid">
            <button className="btn ghost"><Icon.layout/> open in editor</button>
            <button className="btn ghost"><Icon.arrowDown/> pull from main</button>
            <button className="btn ghost"><Icon.branch/> new branch</button>
            <button className="btn danger"><Icon.x/> remove worktree</button>
          </div>
        </>
      )}
    </aside>
  );
}

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<WorktreeCanvas/>);
