/**
 * TraceViewer.jsx
 * Owns: rendering hierarchical agent execution traces (LLM calls, tool calls, state
 *       transitions, errors, retries) in tree and waterfall modes. AG-UI wiring via
 *       TraceViewerContainer.
 * Does NOT own: routing, session management, or trace storage.
 *
 * AG-UI events consumed (via TraceViewerContainer):
 *   RUN_STARTED        { detail: { traceId } }
 *   RUN_FINISHED       { detail: { traceId, outcome } }
 *   SPAN_STARTED       { detail: { spanId, parentSpanId?, name, type, startTime } }
 *   SPAN_ENDED         { detail: { spanId, status, endTime, duration, error? } }
 *   TOOL_CALL_START    { detail: { spanId, parentSpanId?, tool, args, startTime } }
 *   TOOL_CALL_END      { detail: { spanId, result, duration, error? } }
 *   ERROR              { detail: { traceId, message, stack?, timestamp } }
 *   TRACE_PAYLOAD      { detail: { spanId, payload (raw object) } }
 *
 * Span shape:
 *   { id, parentId?, name, type: 'llm'|'tool'|'state'|'error',
 *     status: 'running'|'done'|'error',
 *     startTime (ms), endTime (ms), duration (ms)?,
 *     depth, children?, payload?, error?, agentState? }
 *
 * Props (TraceViewer — controlled):
 *   spans: Span[]
 *   mode: 'tree'|'waterfall'
 *   theme: 'light'|'dark'|'auto'
 *   filterType: 'all'|'llm'|'tool'|'state'|'error'
 *   filterStatus: 'all'|'done'|'error'|'running'
 *   search: string
 *   startTime: number  — ms epoch, used to compute waterfall offsets
 *
 * Zero dependencies beyond React.
 */

// ─── React hooks (injected externally for zero-build) ─────────────────────
const { useState, useEffect, useRef, useMemo, useCallback, useReducer } = React;

// ─── Design tokens ─────────────────────────────────────────────────────────
const TOKENS = {
  light: {
    bg: '#FAFAF8',
    card: '#FFFFFF',
    border: '#E8E6E0',
    fg: '#1a1a1a',
    fgMuted: '#888',
    accent: '#F5A623',
    llm: '#8b5cf6',
    llmBg: 'rgba(139,92,246,0.1)',
    tool: '#3b82f6',
    toolBg: 'rgba(59,130,246,0.1)',
    state: '#6b7280',
    stateBg: 'rgba(107,114,128,0.08)',
    error: '#ef4444',
    errorBg: 'rgba(239,68,68,0.08)',
    running: '#3b82f6',
    runningBg: 'rgba(59,130,246,0.1)',
    success: '#22c55e',
    connector: '#E8E6E0',
    guide: '#E0DDD6',
    shadow: '0 2px 14px rgba(0,0,0,0.07)',
    codeBg: '#1C1C1A',
    codeFg: '#E8E6E0',
    codeKeyword: '#F5A623',
    codeString: '#4ade80',
    codeNumber: '#60a5fa',
    codeComment: '#555',
    codeKey: '#e2b97e',
    codeNull: '#94a3b8',
    searchBg: 'rgba(0,0,0,0.04)',
    chipActive: '#F5A623',
    chipActiveFg: '#fff',
  },
  dark: {
    bg: '#111110',
    card: '#1C1C1A',
    border: '#2E2E2C',
    fg: '#F0EEE8',
    fgMuted: '#666',
    accent: '#F5A623',
    llm: '#a78bfa',
    llmBg: 'rgba(167,139,250,0.12)',
    tool: '#60a5fa',
    toolBg: 'rgba(96,165,250,0.12)',
    state: '#94a3b8',
    stateBg: 'rgba(148,163,184,0.1)',
    error: '#f87171',
    errorBg: 'rgba(248,113,113,0.1)',
    running: '#60a5fa',
    runningBg: 'rgba(96,165,250,0.12)',
    success: '#4ade80',
    connector: '#2E2E2C',
    guide: '#2E2E2C',
    shadow: '0 2px 14px rgba(0,0,0,0.5)',
    codeBg: '#0d0d0b',
    codeFg: '#E8E6E0',
    codeKeyword: '#F5A623',
    codeString: '#4ade80',
    codeNumber: '#60a5fa',
    codeComment: '#555',
    codeKey: '#e2b97e',
    codeNull: '#94a3b8',
    searchBg: 'rgba(255,255,255,0.05)',
    chipActive: '#F5A623',
    chipActiveFg: '#1a1a1a',
  },
};

function resolveTheme(theme) {
  if (theme !== 'auto') return theme || 'light';
  if (typeof window === 'undefined') return 'light';
  return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}

// ─── CSS keyframes / styles (injected once) ─────────────────────────────────
const TV_STYLES = `
@keyframes tv-fade-in  { from{opacity:0;transform:translateY(2px)} to{opacity:1;transform:translateY(0)} }
@keyframes tv-spin     { to{transform:rotate(360deg)} }
@keyframes tv-slide-down { from{max-height:0;opacity:0} to{max-height:800px;opacity:1} }
@keyframes tv-pulse    { 0%,100%{opacity:1} 50%{opacity:0.4} }
@keyframes tv-bar-in   { from{width:0;opacity:0} to{opacity:1} }
`;

let _tvStylesInjected = false;
function injectTVStyles() {
  if (_tvStylesInjected || typeof document === 'undefined') return;
  const el = document.createElement('style');
  el.textContent = TV_STYLES;
  document.head.appendChild(el);
  _tvStylesInjected = true;
}

// ─── Helpers ───────────────────────────────────────────────────────────────
function formatTime(ms) {
  if (ms == null) return '';
  const d = new Date(ms);
  const hh = String(d.getHours()).padStart(2, '0');
  const mm = String(d.getMinutes()).padStart(2, '0');
  const ss = String(d.getSeconds()).padStart(2, '0');
  const ms3 = String(d.getMilliseconds()).padStart(3, '0');
  return `${hh}:${mm}:${ss}.${ms3}`;
}

function formatDuration(ms) {
  if (ms == null || ms < 0) return '';
  if (ms < 1000) return `${Math.round(ms)}ms`;
  if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
  const m = Math.floor(ms / 60000);
  const s = Math.round((ms % 60000) / 1000);
  return `${m}m ${s}s`;
}

function buildTree(spans) {
  if (!spans || !spans.length) return [];
  const map = {};
  const roots = [];
  spans.forEach(s => { map[s.id] = { ...s, children: [] }; });
  spans.forEach(s => {
    if (s.parentId && map[s.parentId]) {
      map[s.parentId].children.push(map[s.id]);
    } else {
      roots.push(map[s.id]);
    }
  });
  // Sort children by startTime
  function sortChildren(node) {
    node.children.sort((a, b) => a.startTime - b.startTime);
    node.children.forEach(sortChildren);
    return node;
  }
  return roots.map(sortChildren);
}

// Flatten tree to array with depth annotation
function flattenTree(nodes, depth = 0) {
  const result = [];
  nodes.forEach(n => {
    result.push({ ...n, depth });
    if (n.children && n.children.length) {
      result.push(...flattenTree(n.children, depth + 1));
    }
  });
  return result;
}

// ─── Syntax colorizer ───────────────────────────────────────────────────────
function syntaxColorize(obj, tok) {
  if (obj == null) return `<span style="color:${tok.codeNull}">null</span>`;
  if (typeof obj === 'boolean') return `<span style="color:${tok.codeNumber}">${obj}</span>`;
  if (typeof obj === 'number') return `<span style="color:${tok.codeNumber}">${obj}</span>`;
  if (typeof obj === 'string') {
    const escaped = obj.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
    return `<span style="color:${tok.codeString}">"${escaped}"</span>`;
  }
  if (Array.isArray(obj)) {
    if (obj.length === 0) return `<span style="color:${tok.fg}">[]</span>`;
    const items = obj.map(v => syntaxColorize(v, tok)).join(', ');
    return `<span style="color:${tok.fg}">[</span>${items}<span style="color:${tok.fg}">]</span>`;
  }
  if (typeof obj === 'object') {
    const entries = Object.entries(obj);
    if (entries.length === 0) return `<span style="color:${tok.fg}">{}</span>`;
    const inner = entries.map(([k, v]) =>
      `<span style="color:${tok.codeKey}">"${k}"</span><span style="color:${tok.fg}">: </span>${syntaxColorize(v, tok)}`
    ).join(`<span style="color:${tok.fg}">, </span>`);
    return `<span style="color:${tok.fg}">{</span>${inner}<span style="color:${tok.fg}">}</span>`;
  }
  return String(obj);
}

// ─── Type / status icons ───────────────────────────────────────────────────
const TYPE_META = {
  llm:   { label: 'LLM',    colorKey: 'llm' },
  tool:   { label: 'TOOL',   colorKey: 'tool' },
  state:  { label: 'STATE',  colorKey: 'state' },
  error:  { label: 'ERROR',  colorKey: 'error' },
};

function TypeChip({ type, tok }) {
  const meta = TYPE_META[type] || TYPE_META.state;
  const color = tok[meta.colorKey];
  return (
    <span style={{
      display: 'inline-flex', alignItems: 'center',
      fontSize: '0.62rem', fontWeight: 700, letterSpacing: '0.07em',
      padding: '2px 5px', borderRadius: 4,
      background: tok[meta.colorKey + 'Bg'] || tok.stateBg,
      color: color, textTransform: 'uppercase', flexShrink: 0,
    }}>
      {meta.label}
    </span>
  );
}

function StatusDot({ status, tok }) {
  switch (status) {
    case 'done':
      return (
        <span style={{ display: 'inline-flex', alignItems: 'center', color: tok.success, flexShrink: 0 }}>
          <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
            <circle cx="7" cy="7" r="6" fill={tok.success} fillOpacity="0.15"/>
            <path d="M4 7.5l2 2 4-4" stroke={tok.success} strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"/>
          </svg>
        </span>
      );
    case 'error':
      return (
        <span style={{ display: 'inline-flex', alignItems: 'center', color: tok.error, flexShrink: 0 }}>
          <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
            <circle cx="7" cy="7" r="6" fill={tok.error} fillOpacity="0.12"/>
            <path d="M4.5 4.5l5 5M9.5 4.5l-5 5" stroke={tok.error} strokeWidth="1.6" strokeLinecap="round"/>
          </svg>
        </span>
      );
    case 'running':
      return (
        <span style={{ display: 'inline-flex', alignItems: 'center', color: tok.running, flexShrink: 0 }}>
          <svg width="14" height="14" viewBox="0 0 14 14" fill="none" style={{ animation: 'tv-spin 0.8s linear infinite' }}>
            <circle cx="7" cy="7" r="5" stroke={tok.running} strokeWidth="1.5" strokeOpacity="0.25"/>
            <path d="M7 2a5 5 0 0 1 5 5" stroke={tok.running} strokeWidth="1.5" strokeLinecap="round"/>
          </svg>
        </span>
      );
    default:
      return (
        <span style={{ display: 'inline-flex', alignItems: 'center', color: tok.fgMuted, flexShrink: 0 }}>
          <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
            <circle cx="7" cy="7" r="5" stroke={tok.fgMuted} strokeWidth="1.5" strokeOpacity="0.4"/>
          </svg>
        </span>
      );
  }
}

// ─── Payload viewer ─────────────────────────────────────────────────────────
function PayloadViewer({ payload, tok, defaultOpen = false }) {
  const [open, setOpen] = useState(defaultOpen);

  const hasPayload = payload && Object.keys(payload).length > 0;
  if (!hasPayload) return null;

  const jsonStr = JSON.stringify(payload, null, 2);

  // Simple syntax highlight by wrapping in spans
  const highlighted = syntaxColorize(payload, tok);

  return (
    <div style={{ marginTop: 6, borderRadius: 7, overflow: 'hidden', border: `1px solid ${tok.guide}` }}>
      <button
        onClick={() => setOpen(o => !o)}
        style={{
          display: 'flex', alignItems: 'center', gap: 5,
          width: '100%', padding: '5px 10px',
          background: tok.codeBg, border: 'none', cursor: 'pointer',
          fontSize: '0.72rem', fontFamily: "'Fira Code', monospace",
          color: tok.fgMuted, textAlign: 'left',
        }}
      >
        <svg width="10" height="10" viewBox="0 0 10 10" fill="none"
          style={{ transform: open ? 'rotate(90deg)' : 'rotate(0deg)', transition: 'transform 0.15s' }}>
          <path d="M3 2l4 3-4 3V2z" fill={tok.fgMuted}/>
        </svg>
        {open ? 'hide' : 'show'} payload
      </button>
      {open && (
        <pre style={{
          margin: 0, padding: '10px 12px',
          background: tok.codeBg, color: tok.codeFg,
          fontSize: '0.72rem', fontFamily: "'Fira Code', monospace",
          lineHeight: 1.6, overflowX: 'auto', maxHeight: 400, overflowY: 'auto',
        }}
        dangerouslySetInnerHTML={{ __html: highlighted }}
        />
      )}
    </div>
  );
}

// ─── Live elapsed ticker ────────────────────────────────────────────────────
function LiveDuration({ startedAt, tok }) {
  const [elapsed, setElapsed] = useState(() => startedAt ? Date.now() - startedAt : 0);

  useEffect(() => {
    if (!startedAt) return;
    const id = setInterval(() => setElapsed(Date.now() - startedAt), 500);
    return () => clearInterval(id);
  }, [startedAt]);

  return (
    <span style={{ fontSize: '0.68rem', color: tok.running, fontVariantNumeric: 'tabular-nums', animation: 'tv-pulse 1.5s ease-in-out infinite' }}>
      {formatDuration(elapsed)}
    </span>
  );
}

// ─── Tree row ───────────────────────────────────────────────────────────────
function TreeRow({ span, tok, onToggle, expanded }) {
  const hasChildren = span.children && span.children.length > 0;
  const isExpanded = expanded[span.id] !== false; // default true

  return (
    <div style={{
      animation: 'tv-fade-in 0.15s ease',
    }}>
      {/* Main row */}
      <div style={{
        display: 'flex', alignItems: 'center', gap: 6,
        padding: '5px 10px 5px 0',
        borderBottom: `1px solid ${tok.guide}`,
        fontFamily: "'Space Grotesk', sans-serif",
        minHeight: 36,
      }}>
        {/* Indent guide lines */}
        <div style={{ display: 'flex', alignItems: 'stretch', flexShrink: 0 }}>
          {Array.from({ length: span.depth }).map((_, i) => (
            <div key={i} style={{
              width: 20, flexShrink: 0,
              borderLeft: `1.5px solid ${tok.guide}`,
              marginLeft: 5,
              ...(i === span.depth - 1 ? { borderBottom: `1.5px solid ${tok.guide}`, width: 10, height: 14, marginBottom: -4 } : {}),
            }} />
          ))}
        </div>

        {/* Chevron (only if has children) */}
        {hasChildren ? (
          <button
            onClick={() => onToggle(span.id)}
            style={{
              background: 'none', border: 'none', cursor: 'pointer',
              padding: '2px', display: 'flex', alignItems: 'center',
              color: tok.fgMuted, flexShrink: 0,
            }}
          >
            <svg width="10" height="10" viewBox="0 0 10 10" fill="none"
              style={{ transform: isExpanded ? 'rotate(90deg)' : 'rotate(0deg)', transition: 'transform 0.15s' }}>
              <path d="M3 2l4 3-4 3V2z" fill={tok.fgMuted}/>
            </svg>
          </button>
        ) : (
          <div style={{ width: 16, flexShrink: 0 }} />
        )}

        {/* Status dot */}
        <StatusDot status={span.status} tok={tok} />

        {/* Type chip */}
        <TypeChip type={span.type} tok={tok} />

        {/* Span name */}
        <span style={{
          flex: 1, fontSize: '0.82rem', fontWeight: 600,
          color: span.status === 'error' ? tok.error : tok.fg,
          overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
        }}>
          {span.name}
        </span>

        {/* Duration or live ticker */}
        <div style={{ display: 'flex', alignItems: 'center', gap: 6, flexShrink: 0 }}>
          {span.status === 'running'
            ? <LiveDuration startedAt={span.startTime} tok={tok} />
            : span.duration != null
              ? (
                <span style={{ fontSize: '0.72rem', color: span.status === 'error' ? tok.error : tok.fgMuted, fontVariantNumeric: 'tabular-nums' }}>
                  {formatDuration(span.duration)}
                </span>
              )
              : null
          }

          {/* Timestamp */}
          {span.startTime && (
            <span style={{ fontSize: '0.68rem', color: tok.fgMuted, fontFamily: "'Fira Code', monospace" }}>
              {formatTime(span.startTime)}
            </span>
          )}
        </div>
      </div>

      {/* Error message */}
      {span.status === 'error' && span.error && (
        <div style={{
          marginLeft: 16 + span.depth * 20,
          marginBottom: 6, padding: '6px 10px',
          background: tok.errorBg, borderRadius: 6,
          fontSize: '0.72rem', color: tok.error,
          fontFamily: "'Fira Code', monospace",
          border: `1px solid ${tok.error}25`,
        }}>
          {span.error}
        </div>
      )}

      {/* Payload */}
      {(span.payload || span.error) && (
        <div style={{ marginLeft: 16 + span.depth * 20, marginBottom: 4 }}>
          <PayloadViewer payload={span.payload || { error: span.error }} tok={tok} />
        </div>
      )}

      {/* Children (expanded) */}
      {hasChildren && isExpanded && (
        <div style={{
          overflow: 'hidden',
          animation: 'tv-slide-down 0.25s ease',
        }}>
          {span.children.map(child => (
            <TreeRow
              key={child.id}
              span={child}
              tok={tok}
              onToggle={onToggle}
              expanded={expanded}
            />
          ))}
        </div>
      )}
    </div>
  );
}

// ─── Tree view ─────────────────────────────────────────────────────────────
function TreeView({ tree, tok, onToggle, expanded }) {
  if (!tree || tree.length === 0) {
    return (
      <div style={{
        padding: '3rem', textAlign: 'center', color: tok.fgMuted,
        fontSize: '0.875rem',
      }}>
        No spans recorded
      </div>
    );
  }
  return (
    <div style={{ padding: '8px 0' }}>
      {tree.map(span => (
        <TreeRow
          key={span.id}
          span={span}
          tok={tok}
          onToggle={onToggle}
          expanded={expanded}
        />
      ))}
    </div>
  );
}

// ─── Waterfall row ──────────────────────────────────────────────────────────
function WaterfallRow({ span, tok, startTime, totalDuration }) {
  const offsetPct = startTime
    ? ((span.startTime - startTime) / totalDuration) * 100
    : 0;
  const widthPct = span.duration != null && totalDuration > 0
    ? Math.max((span.duration / totalDuration) * 100, 2)
    : 4;

  const color = span.status === 'error' ? tok.error
    : span.status === 'running' ? tok.running
    : tok[span.type + 'Color'] || tok[span.type] || tok.tool;
  const bgColor = span.status === 'error' ? tok.errorBg
    : span.status === 'running' ? tok.runningBg
    : tok[span.type + 'Bg'] || tok.toolBg;

  return (
    <div style={{
      display: 'flex', alignItems: 'center', gap: 8,
      padding: '4px 0', minHeight: 30,
      fontFamily: "'Space Grotesk', sans-serif",
      animation: 'tv-fade-in 0.15s ease',
    }}>
      {/* Time label */}
      <span style={{ fontSize: '0.68rem', color: tok.fgMuted, fontFamily: "'Fira Code', monospace", width: 52, flexShrink: 0 }}>
        {span.startTime ? formatTime(span.startTime).slice(0, 11) : '—'}
      </span>

      {/* Name */}
      <span style={{ fontSize: '0.78rem', fontWeight: 500, color: tok.fg, width: 160, flexShrink: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
        {'  '.repeat(span.depth)}{span.name}
      </span>

      {/* Type chip */}
      <TypeChip type={span.type} tok={tok} />

      {/* Bar */}
      <div style={{ flex: 1, position: 'relative', height: 20, background: tok.searchBg, borderRadius: 4 }}>
        <div style={{
          position: 'absolute', top: 3, bottom: 3,
          left: `${offsetPct}%`,
          width: `${widthPct}%`,
          background: bgColor,
          border: `1.5px solid ${color}`,
          borderRadius: 4,
          minWidth: 3,
          animation: 'tv-bar-in 0.3s ease',
        }}>
          {/* Duration label inside bar */}
          {widthPct > 8 && (
            <span style={{
              position: 'absolute', left: '50%', top: '50%',
              transform: 'translate(-50%,-50%)',
              fontSize: '0.62rem', fontWeight: 700,
              color: color, whiteSpace: 'nowrap',
              fontVariantNumeric: 'tabular-nums',
            }}>
              {formatDuration(span.duration)}
            </span>
          )}
        </div>
      </div>

      {/* Duration label */}
      <span style={{ fontSize: '0.68rem', color: tok.fgMuted, fontVariantNumeric: 'tabular-nums', width: 60, textAlign: 'right', flexShrink: 0 }}>
        {span.duration != null ? formatDuration(span.duration) : span.status === 'running' ? 'running…' : '—'}
      </span>
    </div>
  );
}

// ─── Waterfall view ─────────────────────────────────────────────────────────
function WaterfallView({ flatSpans, tok, startTime, totalDuration }) {
  if (!flatSpans || flatSpans.length === 0) {
    return (
      <div style={{ padding: '3rem', textAlign: 'center', color: tok.fgMuted, fontSize: '0.875rem' }}>
        No spans to display
      </div>
    );
  }
  return (
    <div style={{ overflowX: 'auto', padding: '8px 4px' }}>
      {/* Header */}
      <div style={{
        display: 'flex', alignItems: 'center', gap: 8,
        padding: '4px 0', marginBottom: 4,
        borderBottom: `1px solid ${tok.border}`,
        fontFamily: "'Space Grotesk', sans-serif",
      }}>
        <span style={{ fontSize: '0.65rem', fontWeight: 700, color: tok.fgMuted, textTransform: 'uppercase', letterSpacing: '0.06em', width: 52 }}>time</span>
        <span style={{ fontSize: '0.65rem', fontWeight: 700, color: tok.fgMuted, textTransform: 'uppercase', letterSpacing: '0.06em', width: 160 }}>span</span>
        <span style={{ fontSize: '0.65rem', fontWeight: 700, color: tok.fgMuted, textTransform: 'uppercase', letterSpacing: '0.06em', width: 50 }}>type</span>
        <span style={{ flex: 1 }} />
        <span style={{ fontSize: '0.65rem', fontWeight: 700, color: tok.fgMuted, textTransform: 'uppercase', letterSpacing: '0.06em', width: 60, textAlign: 'right' }}>duration</span>
      </div>
      {flatSpans.map(span => (
        <WaterfallRow
          key={span.id}
          span={span}
          tok={tok}
          startTime={startTime}
          totalDuration={totalDuration}
        />
      ))}
    </div>
  );
}

// ─── Filter bar ─────────────────────────────────────────────────────────────
function FilterBar({ filterType, filterStatus, search, onFilterType, onFilterStatus, onSearch, tok }) {
  const types = ['all', 'llm', 'tool', 'state', 'error'];
  const statuses = ['all', 'done', 'error', 'running'];

  return (
    <div style={{
      display: 'flex', gap: 6, alignItems: 'center', flexWrap: 'wrap',
      padding: '8px 0', marginBottom: 2,
      fontFamily: "'Space Grotesk', sans-serif",
    }}>
      {/* Search */}
      <div style={{ position: 'relative', flex: '1', minWidth: 140, maxWidth: 240 }}>
        <svg style={{ position: 'absolute', left: 8, top: '50%', transform: 'translateY(-50%)', color: tok.fgMuted }}
          width="13" height="13" viewBox="0 0 13 13" fill="none">
          <circle cx="5.5" cy="5.5" r="4" stroke={tok.fgMuted} strokeWidth="1.4"/>
          <path d="M9 9l2.5 2.5" stroke={tok.fgMuted} strokeWidth="1.4" strokeLinecap="round"/>
        </svg>
        <input
          type="text"
          value={search}
          onChange={e => onSearch(e.target.value)}
          placeholder="Search spans…"
          style={{
            width: '100%', paddingLeft: 28, paddingRight: 8,
            background: tok.searchBg, border: `1px solid ${tok.border}`,
            borderRadius: 7, height: 30, fontSize: '0.8rem', color: tok.fg,
            fontFamily: "'Space Grotesk', sans-serif", outline: 'none',
            transition: 'border-color 0.15s',
          }}
          onFocus={e => { e.target.style.borderColor = tok.accent; }}
          onBlur={e => { e.target.style.borderColor = tok.border; }}
        />
        {search && (
          <button
            onClick={() => onSearch('')}
            style={{
              position: 'absolute', right: 6, top: '50%', transform: 'translateY(-50%)',
              background: 'none', border: 'none', cursor: 'pointer',
              color: tok.fgMuted, fontSize: '0.75rem', lineHeight: 1,
            }}
          >
            ×
          </button>
        )}
      </div>

      {/* Type filters */}
      <div style={{ display: 'flex', gap: 3, flexWrap: 'wrap' }}>
        {types.map(t => {
          const isActive = filterType === t;
          const color = t === 'all' ? tok.accent : tok[t + 'Color'] || tok[t];
          return (
            <button
              key={t}
              onClick={() => onFilterType(t)}
              style={{
                padding: '3px 9px', borderRadius: 6, border: 'none',
                fontSize: '0.72rem', fontWeight: 700, letterSpacing: '0.05em',
                cursor: 'pointer',
                background: isActive ? (t === 'all' ? tok.accent : color) : tok.searchBg,
                color: isActive ? (t === 'all' ? '#fff' : tok.card) : tok.fgMuted,
                textTransform: 'uppercase',
                transition: 'all 0.15s',
              }}
            >
              {t}
            </button>
          );
        })}
      </div>

      {/* Status filters */}
      <div style={{ display: 'flex', gap: 3, flexWrap: 'wrap' }}>
        {statuses.map(s => {
          const isActive = filterStatus === s;
          return (
            <button
              key={s}
              onClick={() => onFilterStatus(s)}
              style={{
                padding: '3px 9px', borderRadius: 6, border: 'none',
                fontSize: '0.72rem', fontWeight: 700, letterSpacing: '0.05em',
                cursor: 'pointer',
                background: isActive ? tok.accent : tok.searchBg,
                color: isActive ? '#fff' : tok.fgMuted,
                textTransform: 'uppercase',
                transition: 'all 0.15s',
              }}
            >
              {s}
            </button>
          );
        })}
      </div>
    </div>
  );
}

// ─── Mode toggle ────────────────────────────────────────────────────────────
function ModeToggle({ mode, onChange, tok }) {
  const opts = ['tree', 'waterfall'];
  return (
    <div style={{
      display: 'flex', gap: 3,
      background: tok.searchBg, padding: 3, borderRadius: 8,
      fontFamily: "'Space Grotesk', sans-serif",
    }}>
      {opts.map(o => (
        <button
          key={o}
          onClick={() => onChange(o)}
          style={{
            background: mode === o ? tok.card : 'transparent',
            border: 'none', borderRadius: 6,
            fontSize: '0.78rem', fontWeight: 700, padding: '4px 12px',
            cursor: 'pointer', color: mode === o ? tok.fg : tok.fgMuted,
            textTransform: 'capitalize',
            boxShadow: mode === o ? '0 1px 4px rgba(0,0,0,0.12)' : 'none',
            transition: 'all 0.15s',
          }}
        >
          {o === 'tree' ? '◉ Tree' : '▤ Waterfall'}
        </button>
      ))}
    </div>
  );
}

// ─── TraceViewer (controlled) ──────────────────────────────────────────────
function TraceViewer({
  spans = [],
  mode = 'tree',
  theme = 'light',
  filterType = 'all',
  filterStatus = 'all',
  search = '',
  startTime = null,
}) {
  injectTVStyles();
  const [resolvedTheme, setResolvedTheme] = useState(() => resolveTheme(theme));
  const [expanded, setExpanded] = useState({});

  useEffect(() => {
    if (theme !== 'auto') { setResolvedTheme(theme); return; }
    const mq = window.matchMedia('(prefers-color-scheme: dark)');
    const handler = e => setResolvedTheme(e.matches ? 'dark' : 'light');
    mq.addEventListener('change', handler);
    return () => mq.removeEventListener('change', handler);
  }, [theme]);

  const tok = TOKENS[resolvedTheme] || TOKENS.light;

  // ─── Filter + search ───────────────────────────────────────────────────
  const filtered = useMemo(() => {
    return spans.filter(s => {
      if (filterType !== 'all' && s.type !== filterType) return false;
      if (filterStatus !== 'all' && s.status !== filterStatus) return false;
      if (search) {
        const q = search.toLowerCase();
        if (!s.name.toLowerCase().includes(q)) return false;
      }
      return true;
    });
  }, [spans, filterType, filterStatus, search]);

  // ─── Tree build ───────────────────────────────────────────────────────
  const tree = useMemo(() => buildTree(filtered), [filtered]);
  const flatSpans = useMemo(() => flattenTree(tree), [tree]);

  // ─── Waterfall timing ─────────────────────────────────────────────────
  const waterfallStart = startTime || (spans.length ? Math.min(...spans.map(s => s.startTime)) : null);
  const totalDuration = useMemo(() => {
    if (!waterfallStart || !filtered.length) return 1;
    const maxEnd = Math.max(...filtered.filter(s => s.endTime).map(s => s.endTime));
    return Math.max(maxEnd - waterfallStart, 1);
  }, [filtered, waterfallStart]);

  // ─── Toggle expand/collapse ────────────────────────────────────────────
  const handleToggle = useCallback((id) => {
    setExpanded(prev => ({ ...prev, [id]: prev[id] === false }));
  }, []);

  // ─── Count badge ───────────────────────────────────────────────────────
  const totalCount = spans.length;
  const filteredCount = filtered.length;

  if (!totalCount) {
    return (
      <div style={{
        background: tok.card, border: `1px solid ${tok.border}`,
        borderRadius: 12, padding: '3rem', textAlign: 'center',
        boxShadow: tok.shadow, fontFamily: "'Space Grotesk', sans-serif",
      }}>
        <div style={{ fontSize: '1.5rem', marginBottom: 8, color: tok.fgMuted }}>
          <svg width="32" height="32" viewBox="0 0 32 32" fill="none" style={{ margin: '0 auto' }}>
            <circle cx="16" cy="16" r="14" stroke={tok.border} strokeWidth="1.5"/>
            <path d="M10 16h12M16 10v12" stroke={tok.border} strokeWidth="1.5" strokeLinecap="round"/>
          </svg>
        </div>
        <div style={{ fontSize: '0.9rem', fontWeight: 600, color: tok.fg, marginBottom: 4 }}>No trace data</div>
        <div style={{ fontSize: '0.8rem', color: tok.fgMuted }}>
          Attach a TraceViewerContainer to receive AG-UI events, or pass a spans array directly.
        </div>
      </div>
    );
  }

  return (
    <div style={{
      background: tok.card, border: `1px solid ${tok.border}`,
      borderRadius: 12, overflow: 'hidden',
      boxShadow: tok.shadow, fontFamily: "'Space Grotesk', sans-serif",
      animation: 'tv-fade-in 0.2s ease',
    }}>
      {/* Header row */}
      <div style={{
        display: 'flex', alignItems: 'center', justifyContent: 'space-between',
        padding: '10px 14px', borderBottom: `1px solid ${tok.border}`,
        background: tok.bg,
      }}>
        <span style={{ fontSize: '0.8rem', fontWeight: 700, color: tok.fg }}>
          Trace
        </span>
        <span style={{ fontSize: '0.72rem', color: tok.fgMuted, fontVariantNumeric: 'tabular-nums' }}>
          {filteredCount < totalCount ? `${filteredCount} / ${totalCount}` : `${totalCount}`} spans
        </span>
      </div>

      {/* Controls */}
      <div style={{ padding: '8px 14px', borderBottom: `1px solid ${tok.border}`, display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' }}>
        <ModeToggle mode={mode} onChange={() => {}} tok={tok} />
        {/* mode controlled via external state — mode prop drives rendering */}
        <span style={{ fontSize: '0.68rem', color: tok.fgMuted }}>mode: {mode}</span>
      </div>

      {/* Filter bar (always visible) */}
      <div style={{ padding: '0 14px', borderBottom: `1px solid ${tok.border}` }}>
        <FilterBar
          filterType={filterType} filterStatus={filterStatus} search={search}
          onFilterType={() => {}} onFilterStatus={() => {}} onSearch={() => {}}
          tok={tok}
        />
      </div>

      {/* Body */}
      <div style={{ maxHeight: 520, overflowY: 'auto' }}>
        {mode === 'tree'
          ? <TreeView tree={tree} tok={tok} onToggle={handleToggle} expanded={expanded} />
          : <WaterfallView flatSpans={flatSpans} tok={tok} startTime={waterfallStart} totalDuration={totalDuration} />
        }
      </div>
    </div>
  );
}

// ─── TraceViewerContainer (AG-UI auto-wiring) ───────────────────────────────
/**
 * Subscribes to AG-UI trace events and maintains internal spans list.
 *
 * Events:
 *   SPAN_STARTED      → adds span { running }
 *   SPAN_ENDED        → updates span { done / error, endTime, duration }
 *   TOOL_CALL_START    → adds span { type: 'tool', running }
 *   TOOL_CALL_END      → updates tool span { done/error, duration }
 *   RUN_STARTED       → resets spans, sets traceId
 *   RUN_FINISHED      → marks trace complete
 *   ERROR             → adds span { type: 'error' }
 *   TRACE_PAYLOAD     → attaches payload to existing span
 *
 * Props:
 *   eventSource: EventTarget | EventEmitter
 *   initialSpans?: Span[]
 *   mode, theme → passed through to TraceViewer
 */
function TraceViewerContainer({
  eventSource,
  initialSpans = [],
  mode = 'tree',
  theme = 'light',
}) {
  const [spans, setSpans] = useState(initialSpans);
  const [filterType, setFilterType] = useState('all');
  const [filterStatus, setFilterStatus] = useState('all');
  const [search, setSearch] = useState('');
  const [viewMode, setViewMode] = useState(mode);
  const [startTime, setStartTime] = useState(null);

  // Expose imperative controls via refs for the parent demo
  const containerRef = useRef({});

  useEffect(() => { setViewMode(mode); }, [mode]);

  useEffect(() => {
    if (!eventSource) return;

    function upsertSpan(partial) {
      setSpans(prev => {
        const exists = partial.id && prev.some(s => s.id === partial.id);
        if (exists) return prev.map(s => s.id === partial.id ? { ...s, ...partial } : s);
        // New span
        return [...prev, { status: 'running', depth: 0, ...partial }];
      });
    }

    function onRunStarted(e) {
      const d = e.detail || e;
      setSpans([]);
      setStartTime(Date.now());
      if (d.traceId && containerRef.current) containerRef.current.traceId = d.traceId;
    }

    function onSpanStarted(e) {
      const d = e.detail || e;
      upsertSpan({
        id: d.spanId,
        parentId: d.parentSpanId || null,
        name: d.name || d.spanId,
        type: d.type || 'state',
        status: 'running',
        startTime: d.startTime || Date.now(),
        depth: d.depth || 0,
      });
      if (!startTime) setStartTime(d.startTime || Date.now());
    }

    function onSpanEnded(e) {
      const d = e.detail || e;
      const now = Date.now();
      setSpans(prev => prev.map(s => {
        if (s.id !== d.spanId) return s;
        const endTime = d.endTime || now;
        const duration = d.duration != null ? d.duration : (endTime - (s.startTime || endTime));
        return { ...s, status: d.status || 'done', endTime, duration, error: d.error };
      }));
    }

    function onToolCallStart(e) {
      const d = e.detail || e;
      upsertSpan({
        id: d.spanId,
        parentId: d.parentSpanId || null,
        name: d.tool || d.spanId,
        type: 'tool',
        status: 'running',
        startTime: d.startTime || Date.now(),
        depth: d.depth || 0,
        payload: { args: d.args },
      });
      if (!startTime) setStartTime(d.startTime || Date.now());
    }

    function onToolCallEnd(e) {
      const d = e.detail || e;
      const now = Date.now();
      setSpans(prev => prev.map(s => {
        if (s.id !== d.spanId) return s;
        const endTime = d.endTime || now;
        const duration = d.duration != null ? d.duration : (endTime - (s.startTime || endTime));
        return {
          ...s,
          status: d.error ? 'error' : 'done',
          endTime, duration, error: d.error,
          payload: { ...s.payload, result: d.result },
        };
      }));
    }

    function onRunFinished(e) {
      const d = e.detail || e;
      // Mark any running spans as done
      setSpans(prev => prev.map(s =>
        s.status === 'running' ? { ...s, status: 'done', endTime: Date.now() } : s
      ));
      if (containerRef.current) containerRef.current.outcome = d.outcome;
    }

    function onError(e) {
      const d = e.detail || e;
      upsertSpan({
        id: `error-${Date.now()}-${Math.random()}`,
        name: d.message || 'Error',
        type: 'error',
        status: 'error',
        startTime: d.timestamp || Date.now(),
        error: d.message,
        stack: d.stack,
        depth: 0,
      });
    }

    function onTracePayload(e) {
      const d = e.detail || e;
      setSpans(prev => prev.map(s =>
        s.id === d.spanId ? { ...s, payload: d.payload } : s
      ));
    }

    // Wire listeners
    const evs = [
      ['RUN_STARTED', onRunStarted],
      ['SPAN_STARTED', onSpanStarted],
      ['SPAN_ENDED', onSpanEnded],
      ['TOOL_CALL_START', onToolCallStart],
      ['TOOL_CALL_END', onToolCallEnd],
      ['RUN_FINISHED', onRunFinished],
      ['ERROR', onError],
      ['TRACE_PAYLOAD', onTracePayload],
    ];

    if (typeof eventSource.addEventListener === 'function') {
      evs.forEach(([name, fn]) => eventSource.addEventListener(name, fn));
      return () => evs.forEach(([name, fn]) => eventSource.removeEventListener(name, fn));
    }
    if (typeof eventSource.on === 'function') {
      evs.forEach(([name, fn]) => eventSource.on(name, fn));
      return () => evs.forEach(([name, fn]) => eventSource.off(name, fn));
    }
  }, [eventSource]);

  return (
    <div>
      {/* Inline controls */}
      <div style={{ display: 'flex', gap: 8, marginBottom: 12, flexWrap: 'wrap', alignItems: 'center', fontFamily: "'Space Grotesk', sans-serif" }}>
        <ModeToggle2 viewMode={viewMode} setViewMode={setViewMode} tok={TOKENS[resolveTheme(theme)]} />
        <TypeFilterChips filterType={filterType} setFilterType={setFilterType} tok={TOKENS[resolveTheme(theme)]} />
        <StatusFilterChips filterStatus={filterStatus} setFilterStatus={setFilterStatus} tok={TOKENS[resolveTheme(theme)]} />
        <SearchBox search={search} setSearch={setSearch} tok={TOKENS[resolveTheme(theme)]} />
      </div>
      <TraceViewer
        spans={spans}
        mode={viewMode}
        theme={theme}
        filterType={filterType}
        filterStatus={filterStatus}
        search={search}
        startTime={startTime}
      />
    </div>
  );
}

// ─── Inline controls (for TraceViewerContainer) ────────────────────────────
function ModeToggle2({ viewMode, setViewMode, tok }) {
  return (
    <div style={{ display: 'flex', gap: 3, background: tok.searchBg, padding: 3, borderRadius: 8 }}>
      {['tree', 'waterfall'].map(m => (
        <button key={m} onClick={() => setViewMode(m)} style={{
          background: viewMode === m ? tok.card : 'transparent',
          border: 'none', borderRadius: 6, fontSize: '0.72rem', fontWeight: 700,
          padding: '3px 10px', cursor: 'pointer', color: viewMode === m ? tok.fg : tok.fgMuted,
          textTransform: 'capitalize', boxShadow: viewMode === m ? '0 1px 4px rgba(0,0,0,0.12)' : 'none',
          transition: 'all 0.15s',
        }}>
          {m === 'tree' ? '◉ Tree' : '▤ Waterfall'}
        </button>
      ))}
    </div>
  );
}

function TypeFilterChips({ filterType, setFilterType, tok }) {
  return (
    <div style={{ display: 'flex', gap: 3 }}>
      {['all', 'llm', 'tool', 'state', 'error'].map(t => (
        <button key={t} onClick={() => setFilterType(t)} style={{
          padding: '3px 9px', borderRadius: 6, border: 'none',
          fontSize: '0.68rem', fontWeight: 700, letterSpacing: '0.05em',
          cursor: 'pointer', background: filterType === t ? (t === 'all' ? tok.accent : tok[t] || tok.fgMuted) : tok.searchBg,
          color: filterType === t ? '#fff' : tok.fgMuted, textTransform: 'uppercase', transition: 'all 0.15s',
        }}>
          {t}
        </button>
      ))}
    </div>
  );
}

function StatusFilterChips({ filterStatus, setFilterStatus, tok }) {
  return (
    <div style={{ display: 'flex', gap: 3 }}>
      {['all', 'done', 'error', 'running'].map(s => (
        <button key={s} onClick={() => setFilterStatus(s)} style={{
          padding: '3px 9px', borderRadius: 6, border: 'none',
          fontSize: '0.68rem', fontWeight: 700, letterSpacing: '0.05em',
          cursor: 'pointer', background: filterStatus === s ? tok.accent : tok.searchBg,
          color: filterStatus === s ? '#fff' : tok.fgMuted, textTransform: 'uppercase', transition: 'all 0.15s',
        }}>
          {s}
        </button>
      ))}
    </div>
  );
}

function SearchBox({ search, setSearch, tok }) {
  return (
    <div style={{ position: 'relative' }}>
      <svg style={{ position: 'absolute', left: 7, top: '50%', transform: 'translateY(-50%)', color: tok.fgMuted }}
        width="11" height="11" viewBox="0 0 13 13" fill="none">
        <circle cx="5.5" cy="5.5" r="4" stroke={tok.fgMuted} strokeWidth="1.4"/>
        <path d="M9 9l2.5 2.5" stroke={tok.fgMuted} strokeWidth="1.4" strokeLinecap="round"/>
      </svg>
      <input
        type="text" value={search} onChange={e => setSearch(e.target.value)} placeholder="Search…"
        style={{
          paddingLeft: 24, paddingRight: search ? 24 : 8, height: 28, fontSize: '0.75rem',
          background: tok.searchBg, border: `1px solid ${tok.border}`, borderRadius: 7,
          color: tok.fg, fontFamily: "'Space Grotesk', sans-serif", outline: 'none',
        }}
        onFocus={e => { e.target.style.borderColor = tok.accent; }}
        onBlur={e => { e.target.style.borderColor = tok.border; }}
      />
      {search && (
        <button onClick={() => setSearch('')} style={{
          position: 'absolute', right: 5, top: '50%', transform: 'translateY(-50%)',
          background: 'none', border: 'none', cursor: 'pointer', color: tok.fgMuted, fontSize: '0.85rem',
        }}>
          ×
        </button>
      )}
    </div>
  );
}

// Exports
if (typeof module !== 'undefined') {
  module.exports = { TraceViewer, TraceViewerContainer };
}