// agentradar — Main React Application
// Uses React 18 (UMD), Babel standalone, D3, Chart.js

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

// ═══════════════════════════════════════════
// WebSocket Hook
// ═══════════════════════════════════════════
function useAgentRadar() {
  const [connected, setConnected] = useState(false);
  const [runState, dispatch] = useReducer(runReducer, initialRunState());
  const wsRef = useRef(null);
  const reconnectTimer = useRef(null);

  const connect = useCallback(() => {
    const ws = new WebSocket(`ws://${location.host}/ws`);
    wsRef.current = ws;

    ws.onopen = () => {
      setConnected(true);
      clearTimeout(reconnectTimer.current);
    };

    ws.onmessage = (e) => {
      try {
        const event = JSON.parse(e.data);
        dispatch({ type: 'EVENT', payload: event });
      } catch {}
    };

    ws.onclose = () => {
      setConnected(false);
      reconnectTimer.current = setTimeout(connect, 2000);
    };

    ws.onerror = () => ws.close();

    // Ping keepalive
    const pingInterval = setInterval(() => {
      if (ws.readyState === WebSocket.OPEN) ws.send('ping');
    }, 10000);

    return () => { clearInterval(pingInterval); };
  }, []);

  useEffect(() => {
    connect();
    return () => {
      clearTimeout(reconnectTimer.current);
      wsRef.current?.close();
    };
  }, []);

  return { connected, runState, dispatch };
}

function initialRunState() {
  return {
    run_id: null,
    run_name: 'Idle',
    status: 'idle',
    frameworks: [],
    agents: {},
    edges: [],
    steps: [],
    total_tokens: 0,
    total_cost: 0,
    history: [],
  };
}

function runReducer(state, action) {
  const { type, payload } = action;
  if (type === 'RESET') return initialRunState();

  if (type === 'EVENT') {
    const ev = payload;
    switch (ev.event_type) {
      case 'state_sync': {
        const d = ev.data || {};
        return { ...state, ...d, history: [...state.history] };
      }
      case 'run_start': {
        return {
          ...initialRunState(),
          run_id: ev.run_id,
          run_name: ev.run_id,
          status: 'live',
          frameworks: ev.frameworks || [],
          history: state.history,
        };
      }
      case 'frameworks_detected': {
        return { ...state, frameworks: ev.frameworks || [] };
      }
      case 'agent_start': {
        const agents = {
          ...state.agents,
          [ev.agent_id]: {
            ...state.agents[ev.agent_id],
            ...ev,
            status: 'running',
          },
        };
        const edges = addEdge(state.edges, ev.parent_id, ev.agent_id);
        return {
          ...state,
          agents,
          edges,
          steps: [...state.steps, { ...ev, status: 'running' }],
        };
      }
      case 'agent_end': {
        const agents = {
          ...state.agents,
          [ev.agent_id]: {
            ...state.agents[ev.agent_id],
            ...ev,
            status: 'success',
          },
        };
        const totalTokens = state.total_tokens + (ev.tokens?.total || 0);
        return {
          ...state,
          agents,
          total_tokens: totalTokens,
          steps: state.steps.map(s =>
            s.agent_id === ev.agent_id && s.step_number === ev.step_number
              ? { ...s, ...ev, status: 'success' }
              : s
          ),
        };
      }
      case 'agent_error': {
        const agents = {
          ...state.agents,
          [ev.agent_id]: {
            ...state.agents[ev.agent_id],
            ...ev,
            status: 'failed',
          },
        };
        return {
          ...state,
          agents,
          steps: state.steps.map(s =>
            s.agent_id === ev.agent_id && s.step_number === ev.step_number
              ? { ...s, ...ev, status: 'failed' }
              : s
          ),
        };
      }
      case 'run_end': {
        const hasFail = Object.values(state.agents).some(a => a.status === 'failed');
        return {
          ...state,
          status: hasFail ? 'failed' : 'complete',
          history: [
            { ...state, status: hasFail ? 'failed' : 'complete', history: [] },
            ...state.history.slice(0, 4),
          ],
        };
      }
      default:
        return state;
    }
  }
  return state;
}

function addEdge(edges, parentId, agentId) {
  if (!parentId || parentId === agentId) return edges;
  // Support both string (single parent) and array (merge point: all parallel siblings)
  const parents = Array.isArray(parentId) ? parentId : [parentId];
  let result = edges;
  parents.forEach(pid => {
    if (pid === agentId) return;
    const exists = result.some(e => e.source === pid && e.target === agentId);
    if (!exists) {
      result = [...result, { source: pid, target: agentId, id: `${pid}->${agentId}` }];
    }
  });
  return result;
}

// ═══════════════════════════════════════════
// Utility helpers
// ═══════════════════════════════════════════
const STATUS_COLOR = {
  running: '#3b82f6',
  success: '#22c55e',
  failed: '#ef4444',
  pending: '#64748b',
  skipped: '#4b5563',
  ambiguous: '#f97316',
};
const STATUS_ICON = { running: '◎', success: '✓', failed: '✕', pending: '○', skipped: '—', ambiguous: '⚠' };
const STATUS_LABEL = { live: 'LIVE', complete: 'COMPLETE', failed: 'FAILED', idle: 'IDLE' };

function fmtMs(ms) {
  if (!ms) return '—';
  return ms < 1000 ? `${Math.round(ms)}ms` : `${(ms / 1000).toFixed(2)}s`;
}
function fmtTokens(n) { return n ? n.toLocaleString() : '0'; }
function fmtCost(c) { return c ? `$${c.toFixed(4)}` : '$0.0000'; }

function JsonView({ data, maxH = 300 }) {
  const str = useMemo(() => {
    if (data === null || data === undefined) return 'null';
    if (typeof data === 'string') return data;
    try { return JSON.stringify(data, null, 2); } catch { return String(data); }
  }, [data]);
  return (
    <pre style={{ maxHeight: maxH, overflowY: 'auto' }}>
      <code>{str}</code>
    </pre>
  );
}

function CopyBtn({ text }) {
  const [copied, setCopied] = useState(false);
  return (
    <button
      onClick={() => { navigator.clipboard.writeText(text || ''); setCopied(true); setTimeout(() => setCopied(false), 1500); }}
      className="text-xs px-2 py-0.5 rounded bg-gray-200 hover:bg-gray-600 text-gray-700"
    >
      {copied ? '✓ Copied' : 'Copy'}
    </button>
  );
}

function Badge({ label, color = '#6366f1' }) {
  return (
    <span className="text-xs px-2 py-0.5 rounded font-mono font-bold" style={{ background: color + '22', color, border: `1px solid ${color}44` }}>
      {label}
    </span>
  );
}

// ═══════════════════════════════════════════
// DAG Layout engine
// Sequential agents: connected left→right with numbered steps
// Parallel agents:   same depth, stacked vertically, no edge between them
// ═══════════════════════════════════════════
function computeDAGLayout(nodes, edges, W, H) {
  const NODE_W = 160, NODE_H = 86, GAP_X = 100, GAP_Y = 28;

  // Build adjacency lists
  const outEdges = {}, inEdges = {};
  nodes.forEach(n => { outEdges[n.id] = []; inEdges[n.id] = []; });
  edges.forEach(e => {
    if (outEdges[e.source] && inEdges[e.target]) {
      outEdges[e.source].push(e.target);
      inEdges[e.target].push(e.source);
    }
  });

  // Assign depth via longest path from root (handles diamonds/merges correctly)
  const depths = {};
  nodes.forEach(n => depths[n.id] = 0);

  // Iterative relaxation (Bellman-Ford style on DAG)
  for (let iter = 0; iter < nodes.length; iter++) {
    nodes.forEach(n => {
      outEdges[n.id].forEach(child => {
        if (depths[child] < depths[n.id] + 1) depths[child] = depths[n.id] + 1;
      });
    });
  }

  // Group nodes by depth
  const byDepth = {};
  nodes.forEach(n => {
    const d = depths[n.id] || 0;
    if (!byDepth[d]) byDepth[d] = [];
    byDepth[d].push(n);
  });

  const depthLevels = Object.keys(byDepth).map(Number).sort((a, b) => a - b);
  const maxDepth = depthLevels[depthLevels.length - 1] || 0;

  // Total graph width/height based on content
  const totalW = (maxDepth + 1) * (NODE_W + GAP_X);
  const positions = {};

  depthLevels.forEach(depth => {
    const col = byDepth[depth];
    const x = depth * (NODE_W + GAP_X) + NODE_W / 2 + 40;
    const totalColH = col.length * NODE_H + (col.length - 1) * GAP_Y;
    const startY = Math.max(60, (H - totalColH) / 2);

    // Sort nodes within a column by step_number for consistent ordering
    col.sort((a, b) => (a.step || 0) - (b.step || 0));

    col.forEach((n, i) => {
      positions[n.id] = { x, y: startY + i * (NODE_H + GAP_Y) + NODE_H / 2 };
    });
  });

  return positions;
}

// ═══════════════════════════════════════════
// Live Graph View
// ═══════════════════════════════════════════
function LiveGraph({ runState, onNodeSelect }) {
  const svgRef = useRef(null);
  const containerRef = useRef(null);
  const zoomRef = useRef(null);
  const hasAutoFittedRef = useRef(false);
  const [selected, setSelected] = useState(null);

  const SKIP = new Set(['run', 'LangGraph', '__run__', '__langgraph__', '__system__']);
  const agents = Object.values(runState.agents || {})
    .filter(a => !SKIP.has(a.agent_name) && !SKIP.has(a.agent_id));
  const edges = (runState.edges || [])
    .filter(e => !SKIP.has(e.source) && !SKIP.has(e.target));

  useEffect(() => {
    if (!svgRef.current || !containerRef.current) return;
    const svg = d3.select(svgRef.current);
    const W = containerRef.current.clientWidth || 900;
    const H = containerRef.current.clientHeight || 600;

    if (agents.length === 0) return;

    // Save current zoom transform before wiping SVG (preserves user's zoom/pan)
    const savedTransform = (zoomRef.current && svgRef.current)
      ? d3.zoomTransform(svgRef.current)
      : null;

    // Build node objects
    const nodeMap = {};
    agents.forEach(a => {
      nodeMap[a.agent_id] = {
        id: a.agent_id,
        label: a.agent_name,
        status: a.status || 'pending',
        tokens: a.tokens?.total || 0,
        duration: a.duration_ms || 0,
        step: a.step_number || 0,
        data: a,
      };
    });

    const nodes = Object.values(nodeMap);
    const links = edges
      .map(e => ({ source: e.source, target: e.target, id: e.id || `${e.source}->${e.target}` }))
      .filter(l => nodeMap[l.source] && nodeMap[l.target]);

    // Compute deterministic DAG positions
    const positions = computeDAGLayout(nodes, links, W, H);
    nodes.forEach(n => { n.x = positions[n.id]?.x ?? W / 2; n.y = positions[n.id]?.y ?? H / 2; });

    // ── Draw ────────────────────────────────────────────────────────────────
    svg.selectAll('*').remove();
    svg.attr('width', W).attr('height', H);

    // Arrow markers
    const defs = svg.append('defs');
    const mkArrow = (id, color) =>
      defs.append('marker').attr('id', id)
        .attr('viewBox', '0 -4 8 8').attr('refX', 16).attr('refY', 0)
        .attr('markerWidth', 7).attr('markerHeight', 7).attr('orient', 'auto')
        .append('path').attr('d', 'M0,-4L8,0L0,4').attr('fill', color);
    mkArrow('arr-default', '#94a3b8');
    mkArrow('arr-running', '#6366f1');
    mkArrow('arr-failed',  '#ef4444');
    mkArrow('arr-success', '#22c55e');

    const g = svg.append('g').attr('class', 'graph-root');

    // Zoom + pan
    const zoom = d3.zoom().scaleExtent([0.15, 3]).on('zoom', ev => g.attr('transform', ev.transform));
    svg.call(zoom);
    zoomRef.current = zoom;

    // ── Parallel lane dividers (faint vertical bands for same-depth columns) ──
    const byDepthBands = {};
    nodes.forEach(n => {
      const d = Math.round(n.x / 260);
      if (!byDepthBands[d]) byDepthBands[d] = [];
      byDepthBands[d].push(n);
    });
    Object.values(byDepthBands).forEach((col, i) => {
      if (col.length < 2) return; // Only draw band if truly parallel (2+ nodes at same depth)
      const xs = col.map(n => n.x);
      const ys = col.map(n => n.y);
      const bx = Math.min(...xs) - 88, bw = 176;
      const by = Math.min(...ys) - 50, bh = Math.max(...ys) - Math.min(...ys) + 100;
      g.append('rect')
        .attr('x', bx).attr('y', by).attr('width', bw).attr('height', bh).attr('rx', 12)
        .attr('fill', '#0000000a').attr('stroke', '#00000012').attr('stroke-width', 1)
        .attr('stroke-dasharray', '4,4');
      g.append('text')
        .attr('x', bx + bw / 2).attr('y', by - 8)
        .attr('text-anchor', 'middle').attr('font-size', 10).attr('fill', '#94a3b8')
        .text('parallel');
    });

    // ── Edges ──────────────────────────────────────────────────────────────
    const edgeG = g.append('g').attr('class', 'edges');
    links.forEach((lk, i) => {
      const src = nodeMap[lk.source], tgt = nodeMap[lk.target];
      if (!src || !tgt) return;

      const tStatus = tgt.status;
      const stroke = tStatus === 'failed' ? '#ef4444' : tStatus === 'running' ? '#6366f1' : tStatus === 'success' ? '#22c55e88' : '#cbd5e1';
      const marker = tStatus === 'failed' ? 'url(#arr-failed)' : tStatus === 'running' ? 'url(#arr-running)' : tStatus === 'success' ? 'url(#arr-success)' : 'url(#arr-default)';

      // Bezier path: exit right of source, enter left of target
      const sx = src.x + 80, sy = src.y;
      const tx = tgt.x - 80, ty = tgt.y;
      const cx1 = sx + (tx - sx) * 0.45, cy1 = sy;
      const cx2 = tx - (tx - sx) * 0.45, cy2 = ty;
      const d = `M${sx},${sy} C${cx1},${cy1} ${cx2},${cy2} ${tx},${ty}`;

      edgeG.append('path')
        .attr('d', d).attr('fill', 'none')
        .attr('stroke', stroke).attr('stroke-width', tStatus === 'running' ? 2 : 1.5)
        .attr('stroke-dasharray', tStatus === 'running' ? '7,3' : 'none')
        .attr('marker-end', marker)
        .attr('class', tStatus === 'running' ? 'edge-active' : '');

      // Step badge on edge midpoint
      const mx = (sx + tx) / 2, my = (sy + ty) / 2 - 10;
      const stepNum = tgt.step || (i + 1);
      edgeG.append('rect')
        .attr('x', mx - 13).attr('y', my - 9).attr('width', 26).attr('height', 16).attr('rx', 8)
        .attr('fill', '#f8fafc').attr('stroke', stroke).attr('stroke-width', 1);
      edgeG.append('text')
        .attr('x', mx).attr('y', my + 4)
        .attr('text-anchor', 'middle').attr('font-size', 9).attr('fill', stroke).attr('font-weight', '700')
        .text(`${stepNum}`);
    });

    // ── Nodes ──────────────────────────────────────────────────────────────
    const NODE_W = 160, NODE_H = 86;
    const nodeG = g.append('g').attr('class', 'nodes').selectAll('g')
      .data(nodes).enter().append('g')
      .attr('class', 'graph-node')
      .attr('transform', d => `translate(${d.x},${d.y})`)
      .attr('cursor', 'pointer')
      .on('click', (ev, d) => {
        ev.stopPropagation();
        setSelected(d.id === selected ? null : d.id);
        onNodeSelect?.(d.id === selected ? null : d.data);
      });

    // Node shadow for selected
    nodeG.append('rect')
      .attr('x', -NODE_W / 2 - 3).attr('y', -NODE_H / 2 - 3)
      .attr('width', NODE_W + 6).attr('height', NODE_H + 6).attr('rx', 11)
      .attr('fill', 'none')
      .attr('stroke', d => d.id === selected ? '#6366f1' : 'transparent')
      .attr('stroke-width', 3).attr('opacity', 0.6);

    // Node background
    nodeG.append('rect')
      .attr('x', -NODE_W / 2).attr('y', -NODE_H / 2)
      .attr('width', NODE_W).attr('height', NODE_H).attr('rx', 9)
      .attr('fill', '#ffffff')
      .attr('stroke', d => STATUS_COLOR[d.status] || '#d1d5db')
      .attr('stroke-width', 2);

    // Status glow strip (left edge)
    nodeG.append('rect')
      .attr('x', -NODE_W / 2).attr('y', -NODE_H / 2)
      .attr('width', 4).attr('height', NODE_H).attr('rx', 9)
      .attr('fill', d => STATUS_COLOR[d.status] || '#d1d5db');

    // Step number badge (top-right corner)
    nodeG.filter(d => d.step > 0).append('rect')
      .attr('x', NODE_W / 2 - 22).attr('y', -NODE_H / 2 - 1)
      .attr('width', 20).attr('height', 16).attr('rx', 8)
      .attr('fill', d => STATUS_COLOR[d.status] || '#374151');
    nodeG.filter(d => d.step > 0).append('text')
      .attr('x', NODE_W / 2 - 12).attr('y', -NODE_H / 2 + 11)
      .attr('text-anchor', 'middle').attr('font-size', 9).attr('fill', '#fff').attr('font-weight', '700')
      .text(d => `${d.step}`);

    // Agent name
    nodeG.append('text')
      .attr('text-anchor', 'middle').attr('y', -16)
      .attr('font-size', 13).attr('font-weight', '700').attr('fill', '#1e293b')
      .text(d => d.label.length > 15 ? d.label.slice(0, 14) + '…' : d.label);

    // Status row
    nodeG.append('text')
      .attr('text-anchor', 'middle').attr('y', 4)
      .attr('font-size', 11).attr('fill', d => STATUS_COLOR[d.status] || '#64748b')
      .text(d => `${STATUS_ICON[d.status] || '○'} ${d.status.toUpperCase()}`);

    // Duration
    nodeG.append('text')
      .attr('text-anchor', 'middle').attr('y', 22)
      .attr('font-size', 10).attr('fill', '#94a3b8')
      .text(d => d.duration ? fmtMs(d.duration) : '');

    // Token count
    nodeG.filter(d => d.tokens > 0).append('text')
      .attr('text-anchor', 'middle').attr('y', 36)
      .attr('font-size', 10).attr('fill', '#94a3b8')
      .text(d => `${fmtTokens(d.tokens)} tok`);

    // Running pulse ring
    nodeG.filter(d => d.status === 'running').append('rect')
      .attr('x', -NODE_W / 2).attr('y', -NODE_H / 2)
      .attr('width', NODE_W).attr('height', NODE_H).attr('rx', 9)
      .attr('fill', 'none').attr('stroke', '#3b82f6').attr('stroke-width', 2)
      .attr('class', 'node-running').attr('opacity', 0.6);

    // Failed pulse ring
    nodeG.filter(d => d.status === 'failed').append('rect')
      .attr('x', -NODE_W / 2).attr('y', -NODE_H / 2)
      .attr('width', NODE_W).attr('height', NODE_H).attr('rx', 9)
      .attr('fill', 'none').attr('stroke', '#ef4444').attr('stroke-width', 2)
      .attr('class', 'node-failed').attr('opacity', 0.5);

    // Click on background deselects
    svg.on('click', () => { setSelected(null); onNodeSelect?.(null); });

    // Auto-fit only on first render; subsequent updates restore user's zoom/pan
    if (!hasAutoFittedRef.current) {
      const allX = nodes.map(n => n.x), allY = nodes.map(n => n.y);
      const minX = Math.min(...allX) - 100, maxX = Math.max(...allX) + 100;
      const minY = Math.min(...allY) - 60,  maxY = Math.max(...allY) + 60;
      const scaleX = W / (maxX - minX || 1), scaleY = H / (maxY - minY || 1);
      const scale = Math.min(scaleX, scaleY, 1.4) * 0.85;
      const tx = W / 2 - scale * (minX + maxX) / 2;
      const ty = H / 2 - scale * (minY + maxY) / 2;
      svg.call(zoom.transform, d3.zoomIdentity.translate(tx, ty).scale(scale));
      hasAutoFittedRef.current = true;
    } else if (savedTransform) {
      svg.call(zoom.transform, savedTransform);
    }

  }, [JSON.stringify(agents.map(a => `${a.agent_id}:${a.status}:${a.duration_ms}`)), edges.length]);

  if (agents.length === 0) {
    return (
      <div className="flex-1 flex items-center justify-center text-gray-500 flex-col gap-4">
        <div className="text-6xl opacity-20">◎</div>
        <div className="text-lg font-medium">Waiting for agents...</div>
        <div className="text-sm opacity-60">Import agentradar and run your pipeline</div>
        <code className="text-xs bg-gray-50 px-3 py-2 rounded mt-2">import agentradar</code>
      </div>
    );
  }

  const handleZoom = (factor) => {
    if (!svgRef.current || !zoomRef.current) return;
    d3.select(svgRef.current).transition().duration(200).call(zoomRef.current.scaleBy, factor);
  };
  const handleFit = () => {
    if (!svgRef.current || !zoomRef.current) return;
    d3.select(svgRef.current).transition().duration(300).call(zoomRef.current.transform, d3.zoomIdentity);
  };

  return (
    <div ref={containerRef} className="flex-1 relative overflow-hidden">
      <svg ref={svgRef} style={{ width: '100%', height: '100%' }} />

      {/* Legend */}
      <div className="absolute top-3 left-3 flex gap-3 text-xs text-gray-500">
        {[['success','#22c55e'], ['running','#3b82f6'], ['failed','#ef4444'], ['pending','#64748b']].map(([s, c]) => (
          <span key={s} className="flex items-center gap-1">
            <span style={{ color: c }}>{STATUS_ICON[s]}</span> {s}
          </span>
        ))}
        <span className="ml-3 text-gray-600">parallel nodes share a column · step numbers on edges</span>
      </div>

      {/* Zoom controls */}
      <div className="absolute bottom-4 right-4 flex flex-col gap-1">
        {[
          { label: '+', action: () => handleZoom(1.3) },
          { label: '−', action: () => handleZoom(0.75) },
          { label: '⊡', action: handleFit },
        ].map(btn => (
          <button key={btn.label} onClick={btn.action}
            className="w-8 h-8 bg-gray-100 border border-gray-300 rounded text-gray-700 hover:bg-gray-100 text-sm font-mono">
            {btn.label}
          </button>
        ))}
      </div>
    </div>
  );
}

// ═══════════════════════════════════════════
// Inspector Panel
// ═══════════════════════════════════════════
function Inspector({ agent, onClose }) {
  if (!agent) return null;
  const status = agent.status || 'pending';
  const color = STATUS_COLOR[status];

  return (
    <div className="inspector w-96 flex flex-col fade-in">
      <div className="flex items-center justify-between p-4 border-b border-gray-200">
        <div>
          <div className="font-bold text-sm text-gray-900">{agent.agent_name}</div>
          <div className="text-xs mt-0.5" style={{ color }}>{STATUS_ICON[status]} {status.toUpperCase()}</div>
        </div>
        <button onClick={onClose} className="text-gray-500 hover:text-gray-900 text-xl leading-none">×</button>
      </div>

      <div className="flex-1 overflow-y-auto p-4 space-y-4">
        {/* Metrics */}
        <div className="grid grid-cols-2 gap-2">
          {[
            { label: 'Duration', value: fmtMs(agent.duration_ms) },
            { label: 'Step', value: `#${agent.step_number || '?'}` },
            { label: 'Tokens', value: fmtTokens(agent.tokens?.total) },
            { label: 'Type', value: agent.agent_type || 'agent' },
          ].map(m => (
            <div key={m.label} className="bg-gray-50 rounded p-2">
              <div className="text-xs text-gray-500">{m.label}</div>
              <div className="text-sm font-mono font-bold mt-0.5">{m.value}</div>
            </div>
          ))}
        </div>

        {/* Tags */}
        {(agent.metadata?.tag || agent.metadata?.notes) && (
          <div className="flex gap-2 flex-wrap">
            {agent.metadata.tag && <Badge label={`tag: ${agent.metadata.tag}`} color="#818cf8" />}
            {agent.metadata.notes && <Badge label={agent.metadata.notes} color="#64748b" />}
          </div>
        )}

        {/* Input */}
        <Section title="Input" copyText={JSON.stringify(agent.input, null, 2)}>
          <JsonView data={agent.input} />
        </Section>

        {/* Output */}
        <Section title="Output" copyText={JSON.stringify(agent.output, null, 2)}>
          <JsonView data={agent.output} />
        </Section>

        {/* Error */}
        {agent.error && (
          <div className="rounded border border-red-200 bg-red-50 p-3">
            <div className="text-red-600 font-bold text-xs mb-1">✕ {agent.error.type}</div>
            <div className="text-red-700 text-xs mb-2">{agent.error.message}</div>
            <details>
              <summary className="text-xs text-gray-500 cursor-pointer">Stack trace</summary>
              <pre className="mt-2 text-xs text-red-600 overflow-auto max-h-48">{agent.error.traceback}</pre>
            </details>
          </div>
        )}

        {/* Token breakdown */}
        {agent.tokens?.total > 0 && (
          <Section title="Tokens">
            <div className="space-y-1">
              {[
                ['Prompt', agent.tokens.prompt],
                ['Completion', agent.tokens.completion],
                ['Total', agent.tokens.total],
              ].map(([k, v]) => (
                <div key={k} className="key-val">
                  <span className="key">{k}</span>
                  <span className="val font-mono">{fmtTokens(v)}</span>
                </div>
              ))}
              {agent.tokens.model && (
                <div className="key-val">
                  <span className="key">Model</span>
                  <span className="val">{agent.tokens.model}</span>
                </div>
              )}
            </div>
          </Section>
        )}
      </div>
    </div>
  );
}

function Section({ title, children, copyText }) {
  const [open, setOpen] = useState(true);
  return (
    <div>
      <div className="flex items-center justify-between mb-1">
        <button onClick={() => setOpen(o => !o)} className="text-xs font-semibold text-gray-600 hover:text-gray-900 flex items-center gap-1">
          <span>{open ? '▾' : '▸'}</span> {title}
        </button>
        {copyText && open && <CopyBtn text={copyText} />}
      </div>
      {open && <div>{children}</div>}
    </div>
  );
}

// ═══════════════════════════════════════════
// State View
// ═══════════════════════════════════════════
function StateView({ runState }) {
  const [selectedStep, setSelectedStep] = useState(0);
  const [showUnchanged, setShowUnchanged] = useState(false);

  const steps = runState.steps || [];
  const step  = steps[selectedStep];

  // Compute state diff: previous output vs current output (for pipeline state pattern)
  const stateDiff = useMemo(() => {
    if (!step) return [];
    const prev = selectedStep > 0 ? steps[selectedStep - 1]?.output : null;
    const curr = step.output;
    if (!prev || !curr || typeof prev !== 'object' || typeof curr !== 'object') return [];
    return computeDiff(prev, curr);
  }, [selectedStep, steps]);

  const visibleDiff = showUnchanged ? stateDiff : stateDiff.filter(d => d.type !== 'unchanged');

  if (steps.length === 0) {
    return <EmptyState message="No execution steps yet. Run your pipeline to see agent outputs." />;
  }

  return (
    <div className="flex-1 flex overflow-hidden">
      {/* Step timeline */}
      <div className="w-64 border-r border-gray-200 overflow-y-auto flex-shrink-0">
        <div className="p-3 text-xs font-semibold text-gray-500 border-b border-gray-200">
          STEPS ({steps.length})
        </div>
        {steps.map((s, i) => {
          const color = STATUS_COLOR[s.status] || '#64748b';
          const isSelected = i === selectedStep;
          return (
            <button
              key={i}
              onClick={() => setSelectedStep(i)}
              className={`w-full text-left px-3 py-2.5 border-b border-gray-100 hover:bg-gray-50 flex items-center gap-2 text-xs ${isSelected ? 'bg-gray-100' : ''}`}
            >
              <span style={{ color }} className="text-sm">{STATUS_ICON[s.status] || '○'}</span>
              <div className="flex-1 min-w-0">
                <div className="font-medium truncate">{s.agent_name}</div>
                <div className="text-gray-500">Step {i + 1} • {fmtMs(s.duration_ms)}</div>
              </div>
              {s.status === 'failed' && <span className="text-red-500 text-xs">✕</span>}
            </button>
          );
        })}
      </div>

      {/* Detail panel */}
      <div className="flex-1 overflow-y-auto p-4 space-y-4">
        {/* Header */}
        <div className="flex items-center justify-between">
          <div className="flex items-center gap-2 flex-wrap">
            <span className="font-bold">{step?.agent_name}</span>
            <span className="text-gray-500 text-sm">Step {selectedStep + 1}</span>
            {step?.duration_ms > 0 && (
              <span className="text-gray-500 text-xs bg-gray-100 px-2 py-0.5 rounded">{fmtMs(step.duration_ms)}</span>
            )}
            {step?.metadata?.tag && (
              <span className="text-xs bg-indigo-900 text-indigo-300 px-2 py-0.5 rounded">
                {step.metadata.tag}
              </span>
            )}
            {step?.tokens?.total > 0 && (
              <span className="text-xs bg-gray-100 text-yellow-400 px-2 py-0.5 rounded">
                {fmtTokens(step.tokens.total)} tokens
              </span>
            )}
          </div>
          <CopyBtn text={JSON.stringify({ input: step?.input, output: step?.output }, null, 2)} />
        </div>

        {/* Error box */}
        {step?.error && (
          <div className="bg-red-950 border border-red-800 rounded p-3 space-y-1">
            <div className="text-red-600 font-bold text-sm">{step.error.type}: {step.error.message}</div>
            {step.error.traceback && (
              <pre className="text-red-600 text-xs overflow-x-auto whitespace-pre-wrap opacity-80">
                {step.error.traceback}
              </pre>
            )}
          </div>
        )}

        {/* Output — shown first and prominently */}
        <div>
          <div className="text-xs font-semibold text-gray-600 mb-1 uppercase tracking-wide">Output</div>
          <ValueDisplay value={step?.output} />
        </div>

        {/* Input */}
        <div>
          <div className="text-xs font-semibold text-gray-600 mb-1 uppercase tracking-wide">Input</div>
          <ValueDisplay value={step?.input} />
        </div>

        {/* State diff between this step and the previous one */}
        {stateDiff.length > 0 && (
          <div>
            <div className="flex items-center justify-between mb-1">
              <div className="text-xs font-semibold text-gray-600 uppercase tracking-wide">State Changes vs Previous</div>
            </div>
            {visibleDiff.length === 0
              ? <div className="text-gray-600 text-xs">All fields unchanged.</div>
              : <div className="space-y-1">{visibleDiff.map((d, i) => <DiffRow key={i} {...d} />)}</div>
            }
          </div>
        )}
      </div>
    </div>
  );
}

function ValueDisplay({ value }) {
  if (value === null || value === undefined) {
    return <div className="text-gray-600 text-sm italic">—</div>;
  }
  if (typeof value === 'string') {
    // Plain text response (e.g. AutoGen reply, LLM output)
    return (
      <div className="bg-gray-50 border border-gray-200 rounded p-3 text-sm text-gray-800 whitespace-pre-wrap leading-relaxed">
        {value}
      </div>
    );
  }
  // Dict / structured state
  return (
    <pre className="bg-gray-50 border border-gray-200 rounded p-3 text-xs text-gray-800 overflow-x-auto whitespace-pre-wrap leading-relaxed">
      {JSON.stringify(value, null, 2)}
    </pre>
  );
}

function DiffRow({ key: k, type, oldVal, newVal }) {
  const cls = { add: 'diff-add', del: 'diff-del', mod: 'diff-mod', unchanged: '' }[type];
  const icon = { add: '+', del: '−', mod: '~', unchanged: ' ' }[type];
  const fmt = v => {
    if (v === null || v === undefined) return 'null';
    if (typeof v === 'string') return v.length > 120 ? v.slice(0, 120) + '…' : v;
    return JSON.stringify(v);
  };

  return (
    <div className={`${cls} px-3 py-1.5 rounded text-xs font-mono`}>
      <span className="mr-2 font-bold text-gray-600">{icon}</span>
      <span className="text-indigo-400">{k}</span>
      {type === 'mod' && (
        <span className="ml-2">
          <span className="text-red-600 line-through opacity-60">{fmt(oldVal)}</span>
          <span className="mx-1 text-gray-500">→</span>
          <span className="text-green-400">{fmt(newVal)}</span>
        </span>
      )}
      {type === 'add' && <span className="text-green-400 ml-2">{fmt(newVal)}</span>}
      {type === 'del' && <span className="text-red-600 ml-2">{fmt(oldVal)}</span>}
      {type === 'unchanged' && <span className="text-gray-500 ml-2">{fmt(newVal)}</span>}
    </div>
  );
}

function computeDiff(prev, curr) {
  const result = [];
  const allKeys = new Set([...Object.keys(prev), ...Object.keys(curr)]);
  for (const k of allKeys) {
    const hasPrev = k in prev, hasCurr = k in curr;
    if (!hasPrev) result.push({ key: k, type: 'add', newVal: curr[k] });
    else if (!hasCurr) result.push({ key: k, type: 'del', oldVal: prev[k] });
    else if (JSON.stringify(prev[k]) !== JSON.stringify(curr[k]))
      result.push({ key: k, type: 'mod', oldVal: prev[k], newVal: curr[k] });
    else result.push({ key: k, type: 'unchanged', newVal: curr[k] });
  }
  return result;
}

// ═══════════════════════════════════════════
// Time Travel View
// ═══════════════════════════════════════════
function TimeTravel({ runState }) {
  const [currentStep, setCurrentStep] = useState(0);
  const [playing, setPlaying] = useState(false);
  const [speed, setSpeed] = useState(1);
  const playRef = useRef(null);

  const steps = runState.steps || [];
  const total = steps.length;

  useEffect(() => {
    if (playing && currentStep < total - 1) {
      playRef.current = setTimeout(() => setCurrentStep(s => s + 1), 1000 / speed);
    } else if (currentStep >= total - 1) {
      setPlaying(false);
    }
    return () => clearTimeout(playRef.current);
  }, [playing, currentStep, speed, total]);

  const step = steps[currentStep] || null;

  if (total === 0) {
    return <EmptyState message="No completed run to replay. Run your pipeline first." />;
  }

  return (
    <div className="flex-1 flex flex-col overflow-hidden">
      {/* Mini graph for current step */}
      <div className="flex-1 overflow-hidden p-4">
        <TimeTravelGraph steps={steps} currentStep={currentStep} runState={runState} />
      </div>

      {/* Step info */}
      {step && (
        <div className="mx-4 mb-2 p-3 bg-gray-50 rounded border border-gray-200 text-sm">
          <div className="flex justify-between items-center">
            <span className="font-medium">{step.agent_name}</span>
            <span style={{ color: STATUS_COLOR[step.status] }}>{STATUS_ICON[step.status]} {step.status}</span>
          </div>
          {step.error && <div className="text-red-600 text-xs mt-1">✕ {step.error.message}</div>}
        </div>
      )}

      {/* Playback bar */}
      <div className="border-t border-gray-200 p-4 flex items-center gap-4">
        <button onClick={() => setCurrentStep(0)} className="text-gray-600 hover:text-gray-900 text-lg" title="Start">⏮</button>
        <button onClick={() => setCurrentStep(s => Math.max(0, s - 1))} className="text-gray-600 hover:text-gray-900 text-lg" title="Back">⏪</button>
        <button onClick={() => setPlaying(p => !p)} className="text-gray-600 hover:text-gray-900 text-xl" title="Play/Pause">
          {playing ? '⏸' : '▶'}
        </button>
        <button onClick={() => setCurrentStep(total - 1)} className="text-gray-600 hover:text-gray-900 text-lg" title="End">⏭</button>

        <input type="range" min={0} max={Math.max(0, total - 1)} value={currentStep}
          onChange={e => setCurrentStep(+e.target.value)} className="flex-1" />

        <span className="text-xs text-gray-600 whitespace-nowrap">Step {currentStep + 1} / {total}</span>

        <select value={speed} onChange={e => setSpeed(+e.target.value)}
          className="bg-gray-100 border border-gray-300 rounded px-2 py-1 text-xs text-gray-700">
          {[0.5, 1, 2, 4].map(s => <option key={s} value={s}>{s}x</option>)}
        </select>
      </div>
    </div>
  );
}

function TimeTravelGraph({ steps, currentStep, runState }) {
  const pastSteps = steps.slice(0, currentStep + 1);
  const seenAgents = {};
  pastSteps.forEach(s => { seenAgents[s.agent_id] = s; });
  const current = steps[currentStep];

  return (
    <div className="h-full flex flex-wrap gap-3 content-start p-2 overflow-auto">
      {steps.map((step, i) => {
        const isPast = i <= currentStep;
        const isCurrent = i === currentStep;
        const color = isPast ? STATUS_COLOR[step.status] : '#2a2d3a';
        return (
          <div key={step.agent_id + i}
            style={{ borderColor: color, opacity: isPast ? 1 : 0.3, boxShadow: isCurrent ? `0 0 0 3px ${color}44` : 'none' }}
            className="border-2 rounded-lg p-3 w-36 bg-gray-50 transition-all">
            <div className="text-xs font-bold truncate">{step.agent_name}</div>
            <div className="text-xs mt-1" style={{ color }}>{STATUS_ICON[step.status] || '○'} {isPast ? step.status : 'pending'}</div>
            <div className="text-xs text-gray-600 mt-1">{fmtMs(step.duration_ms)}</div>
          </div>
        );
      })}
    </div>
  );
}

// ═══════════════════════════════════════════
// Token Dashboard
// ═══════════════════════════════════════════
function TokenDashboard({ runState }) {
  const barRef = useRef(null);
  const lineRef = useRef(null);
  const donutRef = useRef(null);
  const barChartRef = useRef(null);
  const lineChartRef = useRef(null);
  const donutChartRef = useRef(null);

  const agents = Object.values(runState.agents || {}).filter(a =>
    (a.tokens?.total || 0) > 0 && a.agent_name !== 'run' && a.agent_name !== 'LangGraph'
  );
  const steps = runState.steps || [];
  const totalTokens = runState.total_tokens || 0;

  // Build charts
  useEffect(() => {
    if (!barRef.current || agents.length === 0) return;

    const sorted = [...agents].sort((a, b) => (b.tokens?.total || 0) - (a.tokens?.total || 0));
    const labels = sorted.map(a => a.agent_name);
    const prompts = sorted.map(a => a.tokens?.prompt || 0);
    const completions = sorted.map(a => a.tokens?.completion || 0);
    const colors = sorted.map(a => STATUS_COLOR[a.status] || '#6366f1');

    if (barChartRef.current) barChartRef.current.destroy();
    barChartRef.current = new Chart(barRef.current, {
      type: 'bar',
      data: {
        labels,
        datasets: [
          { label: 'Prompt', data: prompts, backgroundColor: '#3b82f688', borderRadius: 4 },
          { label: 'Completion', data: completions, backgroundColor: '#22c55e88', borderRadius: 4 },
        ],
      },
      options: {
        indexAxis: 'y',
        responsive: true,
        maintainAspectRatio: false,
        plugins: { legend: { labels: { color: '#9ca3af' } } },
        scales: {
          x: { stacked: true, ticks: { color: '#6b7280' }, grid: { color: '#1f2937' } },
          y: { stacked: true, ticks: { color: '#e2e8f0' }, grid: { color: '#1f2937' } },
        },
      },
    });
  }, [agents.length, totalTokens]);

  useEffect(() => {
    if (!lineRef.current || steps.length === 0) return;
    let cumulative = 0;
    const data = steps.map((s, i) => {
      cumulative += s.tokens?.total || 0;
      return { x: i + 1, y: cumulative };
    });

    if (lineChartRef.current) lineChartRef.current.destroy();
    lineChartRef.current = new Chart(lineRef.current, {
      type: 'line',
      data: {
        datasets: [{
          label: 'Cumulative tokens',
          data,
          borderColor: '#6366f1',
          backgroundColor: '#6366f122',
          fill: true,
          tension: 0.3,
          pointRadius: 3,
          pointBackgroundColor: '#6366f1',
        }],
      },
      options: {
        responsive: true,
        maintainAspectRatio: false,
        plugins: { legend: { display: false } },
        scales: {
          x: { type: 'linear', ticks: { color: '#6b7280', stepSize: 1 }, grid: { color: '#1f2937' }, title: { display: true, text: 'Step', color: '#9ca3af' } },
          y: { ticks: { color: '#6b7280' }, grid: { color: '#1f2937' } },
        },
      },
    });
  }, [steps.length]);

  useEffect(() => {
    if (!donutRef.current || agents.length === 0) return;
    if (donutChartRef.current) donutChartRef.current.destroy();
    const sorted = [...agents].sort((a, b) => (b.tokens?.total || 0) - (a.tokens?.total || 0)).slice(0, 8);
    donutChartRef.current = new Chart(donutRef.current, {
      type: 'doughnut',
      data: {
        labels: sorted.map(a => a.agent_name),
        datasets: [{
          data: sorted.map(a => a.tokens?.total || 0),
          backgroundColor: ['#6366f1','#3b82f6','#22c55e','#f97316','#ef4444','#eab308','#8b5cf6','#06b6d4'],
        }],
      },
      options: {
        responsive: true,
        maintainAspectRatio: false,
        plugins: { legend: { position: 'bottom', labels: { color: '#9ca3af', boxWidth: 12 } } },
      },
    });
  }, [agents.length, totalTokens]);

  const hasAnyAgents = Object.keys(runState.agents || {}).length > 0;
  if (!hasAnyAgents) {
    return <EmptyState message="No token data yet. Run your pipeline to see cost breakdown." />;
  }
  if (agents.length === 0) {
    return <EmptyState message="No token usage recorded. Agents ran but reported 0 tokens." />;
  }

  const mostExpensive = agents.reduce((a, b) => (b.tokens?.total || 0) > (a.tokens?.total || 0) ? b : a, agents[0]);
  const avgTokens = Math.round(totalTokens / agents.length);

  return (
    <div className="flex-1 overflow-y-auto p-4 space-y-4">
      {/* Summary cards */}
      <div className="grid grid-cols-3 gap-3">
        <SummaryCard title="Total Tokens" value={fmtTokens(totalTokens)} sub={`${fmtTokens(agents.reduce((s,a)=>s+(a.tokens?.prompt||0),0))} prompt / ${fmtTokens(agents.reduce((s,a)=>s+(a.tokens?.completion||0),0))} completion`} />
        <SummaryCard title="Most Expensive" value={mostExpensive?.agent_name || '—'} sub={`${fmtTokens(mostExpensive?.tokens?.total)} tokens`} />
        <SummaryCard title="Avg per Agent" value={fmtTokens(avgTokens)} sub={`across ${agents.length} agents`} />
      </div>

      {/* Bar chart */}
      <ChartCard title="Tokens by Agent">
        <div style={{ height: Math.max(120, agents.length * 36) }}>
          <canvas ref={barRef}></canvas>
        </div>
      </ChartCard>

      <div className="grid grid-cols-2 gap-4">
        {/* Line chart */}
        <ChartCard title="Token Usage Over Time">
          <div style={{ height: 200 }}>
            <canvas ref={lineRef}></canvas>
          </div>
        </ChartCard>

        {/* Donut */}
        <ChartCard title="Cost Distribution">
          <div style={{ height: 200 }}>
            <canvas ref={donutRef}></canvas>
          </div>
        </ChartCard>
      </div>
    </div>
  );
}

function SummaryCard({ title, value, sub }) {
  return (
    <div className="bg-gray-50 border border-gray-200 rounded-lg p-4">
      <div className="text-xs text-gray-500 mb-1">{title}</div>
      <div className="text-lg font-bold text-gray-900">{value}</div>
      {sub && <div className="text-xs text-gray-500 mt-1">{sub}</div>}
    </div>
  );
}

function ChartCard({ title, children }) {
  return (
    <div className="bg-gray-50 border border-gray-200 rounded-lg p-4">
      <div className="text-xs font-semibold text-gray-600 mb-3">{title}</div>
      {children}
    </div>
  );
}

// ═══════════════════════════════════════════
// Run Comparison View
// ═══════════════════════════════════════════
function RunComparison({ runState }) {
  const [runA, setRunA] = useState(null);
  const [runB, setRunB] = useState(null);

  const history = runState.history || [];
  const options = [
    { label: 'Current', value: 'current', data: runState },
    ...history.map((h, i) => ({ label: `Run ${i + 1} — ${h.status}`, value: i, data: h })),
  ];

  const getRun = (val) => val === 'current' ? runState : (typeof val === 'number' ? history[val] : null);
  const dataA = getRun(runA);
  const dataB = getRun(runB);

  const allAgentNames = useMemo(() => {
    const names = new Set();
    [dataA, dataB].forEach(r => r && Object.values(r.agents || {}).forEach(a => names.add(a.agent_name)));
    return [...names];
  }, [dataA, dataB]);

  return (
    <div className="flex-1 flex flex-col overflow-hidden">
      {/* Selectors */}
      <div className="flex gap-4 p-4 border-b border-gray-200">
        <div className="flex-1">
          <label className="text-xs text-gray-500 block mb-1">Run A</label>
          <select value={runA ?? ''} onChange={e => setRunA(e.target.value === '' ? null : isNaN(+e.target.value) ? e.target.value : +e.target.value)}
            className="w-full bg-gray-100 border border-gray-300 rounded px-3 py-2 text-sm text-gray-700">
            <option value="">Select run...</option>
            {options.map(o => <option key={o.value} value={o.value}>{o.label}</option>)}
          </select>
        </div>
        <div className="flex-1">
          <label className="text-xs text-gray-500 block mb-1">Run B</label>
          <select value={runB ?? ''} onChange={e => setRunB(e.target.value === '' ? null : isNaN(+e.target.value) ? e.target.value : +e.target.value)}
            className="w-full bg-gray-100 border border-gray-300 rounded px-3 py-2 text-sm text-gray-700">
            <option value="">Select run...</option>
            {options.map(o => <option key={o.value} value={o.value}>{o.label}</option>)}
          </select>
        </div>
      </div>

      {(!dataA || !dataB) ? (
        <EmptyState message="Select two runs to compare side by side." />
      ) : (
        <div className="flex-1 overflow-y-auto">
          {/* Summary strip */}
          <div className="grid grid-cols-3 gap-4 p-4 border-b border-gray-200">
            <SummaryCard title="Run A Tokens" value={fmtTokens(dataA.total_tokens)} sub={dataA.status} />
            <SummaryCard title="Token Δ" value={fmtDelta(dataB.total_tokens - dataA.total_tokens, 'tokens')} sub="Run B vs Run A" />
            <SummaryCard title="Run B Tokens" value={fmtTokens(dataB.total_tokens)} sub={dataB.status} />
          </div>

          {/* Comparison table */}
          <table className="w-full text-sm">
            <thead>
              <tr className="text-xs text-gray-500 border-b border-gray-200">
                <th className="text-left px-4 py-2">Agent</th>
                <th className="text-center px-4 py-2">Run A</th>
                <th className="text-center px-4 py-2">Run B</th>
                <th className="text-center px-4 py-2">Token Δ</th>
                <th className="text-center px-4 py-2">Duration Δ</th>
              </tr>
            </thead>
            <tbody>
              {allAgentNames.map(name => {
                const agA = Object.values(dataA.agents || {}).find(a => a.agent_name === name);
                const agB = Object.values(dataB.agents || {}).find(a => a.agent_name === name);
                const tokenDelta = (agB?.tokens?.total || 0) - (agA?.tokens?.total || 0);
                const durDelta = (agB?.duration_ms || 0) - (agA?.duration_ms || 0);
                const isImproved = agA?.status === 'failed' && agB?.status === 'success';
                const isDegraded = agA?.status === 'success' && agB?.status === 'failed';
                const rowBg = isImproved ? 'bg-green-950' : isDegraded ? 'bg-red-950' : '';
                return (
                  <tr key={name} className={`border-b border-gray-100 hover:bg-gray-50 ${rowBg}`}>
                    <td className="px-4 py-2 font-mono text-xs">{name}</td>
                    <td className="px-4 py-2 text-center">
                      <StatusPill status={agA?.status} />
                    </td>
                    <td className="px-4 py-2 text-center">
                      <StatusPill status={agB?.status} />
                    </td>
                    <td className="px-4 py-2 text-center text-xs font-mono" style={{ color: tokenDelta > 0 ? '#ef4444' : tokenDelta < 0 ? '#22c55e' : '#64748b' }}>
                      {tokenDelta > 0 ? '+' : ''}{tokenDelta.toLocaleString()}
                    </td>
                    <td className="px-4 py-2 text-center text-xs font-mono" style={{ color: durDelta > 0 ? '#f97316' : '#64748b' }}>
                      {durDelta > 0 ? '+' : ''}{fmtMs(durDelta)}
                    </td>
                  </tr>
                );
              })}
            </tbody>
          </table>
        </div>
      )}
    </div>
  );
}

function StatusPill({ status }) {
  if (!status) return <span className="text-gray-600 text-xs">—</span>;
  const color = STATUS_COLOR[status] || '#64748b';
  return <span className="text-xs px-2 py-0.5 rounded font-mono" style={{ background: color + '22', color }}>{STATUS_ICON[status]} {status}</span>;
}

function fmtDelta(n, unit) {
  const sign = n > 0 ? '+' : '';
  return `${sign}${typeof n === 'number' ? n.toLocaleString() : n} ${unit}`;
}

// ═══════════════════════════════════════════
// Failure History View
// ═══════════════════════════════════════════
function FailureHistory({ runState }) {
  const [failures, setFailures] = useState([]);
  const [selected, setSelected] = useState(null);
  const [loading, setLoading] = useState(true);

  const fetchFailures = () => {
    fetch('/api/failures').then(r => r.json()).then(data => {
      setFailures(data);
      setLoading(false);
    }).catch(() => setLoading(false));
  };

  // Fetch on mount
  useEffect(() => { fetchFailures(); }, []);

  // Re-fetch whenever a new agent_error appears in the current run
  const failedCount = Object.values(runState.agents || {}).filter(a => a.status === 'failed').length;
  useEffect(() => {
    if (failedCount > 0) fetchFailures();
  }, [failedCount]);

  const clearAll = async () => {
    if (!confirm('Clear all failure history?')) return;
    await fetch('/api/failures', { method: 'DELETE' });
    setFailures([]);
    setSelected(null);
  };

  return (
    <div className="flex-1 flex overflow-hidden">
      {/* Sidebar */}
      <div className="w-72 border-r border-gray-200 flex flex-col">
        <div className="flex items-center justify-between p-3 border-b border-gray-200">
          <span className="text-xs font-semibold text-gray-600">FAILURE HISTORY</span>
          <button onClick={clearAll} className="text-xs text-red-600 hover:text-red-700">Clear all</button>
        </div>
        {loading ? <div className="p-4 text-gray-500 text-xs">Loading...</div> :
         failures.length === 0 ? <div className="p-4 text-gray-500 text-xs">No failures recorded.</div> :
         failures.map(f => (
           <button key={f.id} onClick={() => setSelected(f)}
             className={`text-left p-3 border-b border-gray-100 hover:bg-gray-50 ${selected?.id === f.id ? 'bg-gray-100' : ''}`}>
             <div className="flex items-center gap-2">
               <span className="text-red-500">✕</span>
               <span className="text-xs font-mono truncate">{f.run_id || f.id}</span>
               {f.pinned && <span className="text-yellow-500 text-xs">📌</span>}
             </div>
             <div className="text-xs text-gray-500 mt-1">
               Failed: {f.failed_agents?.join(', ') || 'unknown'}
             </div>
             <div className="text-xs text-gray-600">{new Date(f.created_at).toLocaleString()}</div>
           </button>
         ))
        }
      </div>

      {/* Detail */}
      {selected ? (
        <FailureDetail failure={selected} />
      ) : (
        <EmptyState message="Select a failure from the list to inspect it." />
      )}
    </div>
  );
}

function FailureDetail({ failure }) {
  const [data, setData] = useState(null);

  useEffect(() => {
    fetch(`/api/failures/${failure.id}`).then(r => r.json()).then(d => setData(d));
  }, [failure.id]);

  if (!data) return <div className="flex-1 flex items-center justify-center text-gray-500 text-sm">Loading...</div>;

  const run = data.run || {};
  const agents = Object.values(run.agents || {});
  const failedAgents = agents.filter(a => a.status === 'failed');

  return (
    <div className="flex-1 overflow-y-auto p-4 space-y-4">
      <div className="flex items-center gap-3">
        <span className="text-red-500 text-2xl">✕</span>
        <div>
          <div className="font-bold">{run.run_id}</div>
          <div className="text-xs text-gray-500">{data.created_at}</div>
        </div>
      </div>

      {/* Failed agents */}
      {failedAgents.map(a => (
        <div key={a.agent_id} className="border border-red-900 rounded-lg p-4 bg-red-950">
          <FailureAnalyzer agent={a} />
        </div>
      ))}

      {/* All agents table */}
      <div>
        <div className="text-xs font-semibold text-gray-500 mb-2">ALL AGENTS</div>
        <div className="space-y-1">
          {agents.filter(a => a.agent_name !== 'run').map(a => (
            <div key={a.agent_id} className="flex items-center gap-3 px-3 py-2 bg-gray-50 rounded text-xs">
              <span style={{ color: STATUS_COLOR[a.status] }}>{STATUS_ICON[a.status]}</span>
              <span className="font-mono">{a.agent_name}</span>
              <span className="text-gray-500 ml-auto">{fmtMs(a.duration_ms)}</span>
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

function FailureAnalyzer({ agent }) {
  const cause = classifyCause(agent);
  return (
    <div>
      <div className="font-bold text-red-600 mb-2">✕ Failed: {agent.agent_name}</div>
      {agent.error && (
        <>
          <div className="text-sm text-red-700 mb-1">{agent.error.message}</div>
          <details className="mb-3">
            <summary className="text-xs text-gray-500 cursor-pointer">Stack trace</summary>
            <pre className="text-xs text-red-600 mt-1 overflow-auto max-h-32">{agent.error.traceback}</pre>
          </details>
        </>
      )}
      <div className="flex gap-2 items-start">
        <div className="text-xs bg-red-900 text-red-700 px-2 py-1 rounded font-bold">{cause.label}</div>
        <div className="text-xs text-gray-600">{cause.suggestion}</div>
      </div>
    </div>
  );
}

function classifyCause(agent) {
  const error = agent.error || {};
  const msg = (error.message || '').toLowerCase();
  const tb = (error.traceback || '').toLowerCase();

  if (tb.includes('tool') || msg.includes('tool')) {
    return { label: 'Tool Error', suggestion: 'Check tool arguments in the Step Inspector. Verify the external API or tool is available.' };
  }
  if (agent.duration_ms > 30000 || msg.includes('timeout')) {
    return { label: 'Timeout', suggestion: 'Increase timeout or reduce input size to this agent.' };
  }
  if (agent.output === null || agent.output === '' || agent.output === undefined) {
    return { label: 'Null Return', suggestion: 'Check prompt instructions for explicit output format requirements. Add output validation.' };
  }
  if (msg.includes('validation') || msg.includes('schema') || tb.includes('pydantic')) {
    return { label: 'Schema Mismatch', suggestion: 'Review the output schema. Check if the agent\'s prompt specifies the required format.' };
  }
  if (msg.includes('context length') || msg.includes('token') || msg.includes('maximum')) {
    return { label: 'Token Limit', suggestion: 'Reduce input size. Add a summarization step before this agent.' };
  }
  if (!agent.input || (typeof agent.input === 'object' && Object.keys(agent.input).length === 0)) {
    return { label: 'Empty Input', suggestion: 'Check the previous agent\'s output. The handoff may have passed empty state.' };
  }
  return { label: 'Unknown', suggestion: 'Review the full Step Inspector for this agent. Check the stack trace for the root cause.' };
}

// ═══════════════════════════════════════════
// Shared components
// ═══════════════════════════════════════════
function EmptyState({ message }) {
  return (
    <div className="flex-1 flex items-center justify-center text-gray-500 text-sm p-8 text-center">
      {message}
    </div>
  );
}

// ═══════════════════════════════════════════
// Global Header
// ═══════════════════════════════════════════
function Header({ runState, connected, activeTab, setActiveTab }) {
  const { status, run_name, frameworks, total_tokens } = runState;

  const statusColors = { live: '#22c55e', complete: '#22c55e', failed: '#ef4444', idle: '#64748b' };
  const statusColor = statusColors[status] || '#64748b';

  const tabs = [
    { id: 'graph', label: '⬡ Live Graph' },
    { id: 'state', label: '≋ State' },
    { id: 'timetravel', label: '⏮ Time Travel' },
    { id: 'tokens', label: '◈ Tokens' },
    { id: 'failures', label: '✕ Failures' },
  ];

  return (
    <div className="border-b border-gray-200 bg-white">
      {/* Top bar */}
      <div className="flex items-center justify-between px-4 py-2">
        {/* Left */}
        <div className="flex items-center gap-3">
          <div className="font-bold text-indigo-400 text-sm tracking-tight">agentradar</div>
          <span className="text-gray-600 text-xs">v1.0.0</span>
          <span className="text-gray-500 text-xs font-mono">{run_name || 'idle'}</span>
          {frameworks.length > 0 && frameworks.map(f => <Badge key={f} label={f} color="#6366f1" />)}
        </div>

        {/* Center */}
        <div className="flex items-center gap-4">
          <div className="flex items-center gap-1.5">
            <span className="w-2 h-2 rounded-full" style={{ background: statusColor, boxShadow: status === 'live' ? `0 0 6px ${statusColor}` : 'none' }}></span>
            <span className="text-xs font-bold" style={{ color: statusColor }}>{STATUS_LABEL[status] || 'IDLE'}</span>
          </div>
          {total_tokens > 0 && (
            <span className="text-xs text-gray-600 font-mono">{fmtTokens(total_tokens)} tokens</span>
          )}
        </div>

        {/* Right */}
        <div className="flex items-center gap-3">
          <div className={`w-2 h-2 rounded-full ${connected ? 'bg-green-500' : 'bg-red-500'}`} title={connected ? 'Connected' : 'Reconnecting...'}></div>
          <span className="text-xs text-gray-500">{connected ? 'connected' : 'reconnecting...'}</span>
        </div>
      </div>

      {/* Tab bar */}
      <div className="flex px-4">
        {tabs.map(tab => (
          <button
            key={tab.id}
            onClick={() => setActiveTab(tab.id)}
            className={`px-4 py-2 text-xs font-medium border-b-2 transition-colors ${activeTab === tab.id ? 'border-indigo-500 text-indigo-400' : 'border-transparent text-gray-500 hover:text-gray-900'}`}
          >
            {tab.label}
          </button>
        ))}
      </div>
    </div>
  );
}

// ═══════════════════════════════════════════
// Root App
// ═══════════════════════════════════════════
function App() {
  const [activeTab, setActiveTab] = useState('graph');
  const [selectedAgent, setSelectedAgent] = useState(null);
  const { connected, runState } = useAgentRadar();

  const showInspector = activeTab === 'graph' && selectedAgent;

  return (
    <div id="root" className="bg-white text-gray-900 flex flex-col h-screen">
      <Header
        runState={runState}
        connected={connected}
        activeTab={activeTab}
        setActiveTab={setActiveTab}
      />

      <div className="flex-1 flex overflow-hidden">
        <div className="flex-1 flex overflow-hidden">
          {activeTab === 'graph' && (
            <LiveGraph
              runState={runState}
              onNodeSelect={agent => setSelectedAgent(agent)}
            />
          )}
          {activeTab === 'state' && <StateView runState={runState} />}
          {activeTab === 'timetravel' && <TimeTravel runState={runState} />}
          {activeTab === 'tokens' && <TokenDashboard runState={runState} />}

          {activeTab === 'failures' && <FailureHistory runState={runState} />}
        </div>

        {showInspector && (
          <Inspector
            agent={selectedAgent}
            onClose={() => setSelectedAgent(null)}
          />
        )}
      </div>
    </div>
  );
}

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