// ========== Traces page ==========

const { useState: tUseState, useEffect: tUseEffect, useMemo: tUseMemo, useRef: tUseRef } = React;

// ----- helpers -----
function dateKey(date) {
  const pad = (value) => String(value).padStart(2, "0");
  return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
}
function fmtTraceTime(timestamp) {
  const date = new Date(timestamp);
  if (Number.isNaN(date.getTime())) return "—";
  const pad = (value) => String(value).padStart(2, "0");
  return `${String(date.getFullYear()).slice(-2)}${pad(date.getMonth() + 1)}${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
}
function fmtCost(c) {
  if (c === null || c === undefined || c === "") return "—";
  const value = Number(c);
  if (!Number.isFinite(value)) return "—";
  return `$${value.toFixed(value > 0 && value < 0.0001 ? 8 : 4)}`;
}
function fmtTokens(n) {
  if (!Number.isFinite(Number(n))) return "—";
  n = Number(n);
  if (n >= 1000) return (n / 1000).toFixed(n >= 10000 ? 0 : 1) + "k";
  return String(n);
}
function modelUsageValue(usage, ...keys) {
  for (const key of keys) {
    const value = usage?.[key];
    if (value !== undefined && value !== null) return value;
  }
  return "—";
}

// ----- summary strip (above list) -----
function SummaryStrip({ traces }) {
  const total = traces.length;
  const errs = traces.filter((t) => t.status === "error").length;
  const errRate = total ? (errs / total * 100) : 0;
  const totalCost = traces.reduce((s, t) => s + t.costUSD, 0);
  const p95Latency = (() => {
    if (!total) return 0;
    const sorted = [...traces].map((t) => t.durationMs).sort((a, b) => a - b);
    return sorted[Math.floor(sorted.length * 0.95)] || 0;
  })();
  return (
    <div className="summary-strip">
      <div className="cell">
        <div className="lbl">Runs</div>
        <div className="val">{total}<span className="unit">total</span></div>
      </div>
      <div className="cell">
        <div className="lbl">Error rate</div>
        <div className="val" style={{ color: errRate > 5 ? "#b91c1c" : "var(--text)" }}>
          {errRate.toFixed(1)}<span className="unit">%</span>
        </div>
      </div>
      <div className="cell">
        <div className="lbl">p95 latency</div>
        <div className="val">{fmtMs(p95Latency)}</div>
      </div>
      <div className="cell">
        <div className="lbl">Cost</div>
        <div className="val">{fmtCost(totalCost)}</div>
      </div>
    </div>
  );
}

// ----- filter dropdown -----
function FilterChip({ label, value, options, onChange }) {
  const [open, setOpen] = tUseState(false);
  const ref = tUseRef(null);
  tUseEffect(() => {
    const fn = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    document.addEventListener("mousedown", fn);
    return () => document.removeEventListener("mousedown", fn);
  }, []);
  const set = !!value;
  const display = options.find((o) => o.id === value)?.label || "Any";
  return (
    <span ref={ref} className="rel">
      <button className={"filter-chip" + (set ? " set" : "")} onClick={() => setOpen(!open)}>
        <span className="k">{label}</span>
        <span className="v">{display}</span>
        {set ? (
          <span className="x" onClick={(e) => { e.stopPropagation(); onChange(null); }}>
            <Icon name="x" size={11} />
          </span>
        ) : <Icon name="chevdown" size={10} />}
      </button>
      {open && (
        <div className="composer-popover" style={{ bottom: "auto", top: "calc(100% + 6px)", left: 0, minWidth: 200 }}>
          <div className="popover-row" onClick={() => { onChange(null); setOpen(false); }}>
            <span>Any</span>
            <span className="check"></span>
          </div>
          {options.map((o) => (
            <div key={o.id} className={"popover-row" + (o.id === value ? " on" : "")}
              onClick={() => { onChange(o.id); setOpen(false); }}>
              {o.color && <span style={{
                width: 14, height: 14, borderRadius: 3, background: o.color,
                display: "inline-block", flexShrink: 0,
              }}></span>}
              <span>{o.label}</span>
              <span className="check"><Icon name="check" size={11} /></span>
            </div>
          ))}
        </div>
      )}
    </span>
  );
}

function SelectFilter({ label, value, options, onChange }) {
  return (
    <label className="trace-select-filter">
      <span>{label}</span>
      <select value={value} onChange={(event) => onChange(event.target.value)}>
        {options.map((option) => <option key={option.id} value={option.id}>{option.label}</option>)}
      </select>
    </label>
  );
}

// ----- trace list row -----
function TraceRow({ trace, active, onClick }) {
  const dotCls = trace.status === "ok" ? "ok" : trace.status === "warn" ? "warn" : "err";
  // bar represents latency relative to 3s = 100%
  const ratio = Math.min(1, trace.durationMs / 3000);
  return (
    <div className={"trace-row" + (active ? " active" : "")} onClick={onClick}>
      <div className="status-col"><span className={"dot " + dotCls}></span></div>
      <div className="main">
        <div className="head">
          <span className="agent-chip">
            <span className="av" style={{ background: trace.agentColor }}>{trace.agentInitials}</span>
            {trace.agentName}
          </span>
          <span className="when">{fmtTraceTime(trace.startedAt)}</span>
        </div>
        <div className="input-preview">{trace.userInput}</div>
        <div className="meta-row">
          <span className="m"><Icon name="cpu" size={10} />{trace.model}</span>
          <span className="m"><Icon name="loop" size={10} />{trace.llmRounds}</span>
          <span className="m"><Icon name="tool" size={10} />{trace.toolCalls}</span>
          <span className="m">{fmtTokens(trace.tokensIn)}↑ {fmtTokens(trace.tokensOut)}↓</span>
          {trace.userId && <span className="m">user {trace.userId}</span>}
        </div>
      </div>
      <div className="dur-col">
        {fmtMs(trace.durationMs)}
        <span className="bar"><span className="fill" style={{ width: (ratio * 100).toFixed(1) + "%" }}></span></span>
      </div>
      <div className="cost-col">{fmtCost(trace.costUSD)}</div>
    </div>
  );
}

// ----- Turn execution -----
function traceNumber(...values) {
  for (const value of values) {
    if (value === null || value === undefined || value === "") continue;
    const number = Number(value);
    if (Number.isFinite(number)) return number;
  }
  return null;
}

function traceUsage(trace) {
  const usage = trace?._detail?.run?.usage || trace?._detail?.summary?.usage || {};
  return {
    inputTokens: traceNumber(usage.input_tokens, usage.prompt_tokens, usage.prompt_token_count, trace?.tokensIn),
    outputTokens: traceNumber(usage.output_tokens, usage.completion_tokens, usage.candidates_token_count, trace?.tokensOut),
    costUsd: traceNumber(usage.cost_usd, trace?.costUSD),
  };
}

function traceTimelineSteps(trace) {
  const events = trace?._events || [];
  const modelIO = trace?.modelIO || [];
  const modelIOByRound = new Map(modelIO.map((call) => [call.round_index, call]));
  const steps = [];
  const completedRounds = new Set();
  const completedToolCalls = new Set();

  for (const event of events) {
    const payload = event.payload || {};
    if (event.type === "llm.completed") {
      const roundIndex = payload.round_index || completedRounds.size + 1;
      const call = modelIOByRound.get(roundIndex);
      completedRounds.add(roundIndex);
      const model = payload.model || call?.model || trace.model;
      steps.push({
        kind: "llm",
        label: `LLM · round ${roundIndex}${model ? ` (${model})` : ""}`,
        durMs: traceNumber(payload.duration_ms, call?.duration_ms) || 1,
      });
      continue;
    }
    if (event.type !== "tool.completed" && event.type !== "tool.failed" && event.type !== "TOOL_CALL_RESULT") continue;

    const callId = payload["tool.call_id"] || payload.tool_call_id || payload.call_id || payload.toolCallId;
    const fallbackId = `${event.type}:${steps.length}`;
    const toolKey = callId || fallbackId;
    if (completedToolCalls.has(toolKey)) continue;
    completedToolCalls.add(toolKey);
    const isError = Boolean(payload.is_error || payload.error || event.type === "tool.failed" || payload.content?.is_error);
    steps.push({
      kind: "tool",
      label: payload.tool_name || payload["tool.name"] || payload.toolCallName || "tool",
      durMs: traceNumber(payload.duration_ms, payload["tool.duration_ms"], payload.content?.duration_ms) || 1,
      isError,
    });
  }

  // Historical traces can have model I/O retained while their event buffer has
  // been pruned. Keep the round count visible instead of rendering an empty run.
  if (!completedRounds.size && modelIO.length) {
    for (const call of modelIO) {
      const roundIndex = call.round_index || steps.length + 1;
      steps.push({
        kind: "llm",
        label: `LLM · round ${roundIndex}${call.model ? ` (${call.model})` : ""}`,
        durMs: traceNumber(call.duration_ms) || 1,
      });
    }
  }

  if (!steps.length && trace?.spans?.length) {
    for (const span of trace.spans.filter((item) => item.kind === "llm" || item.kind === "tool")) {
      steps.push({
        kind: span.kind,
        label: span.name,
        durMs: span.durMs || 1,
        isError: Boolean(span.error),
      });
    }
  }

  steps.push({
    kind: "final",
    label: `RUN FINISHED · ${trace?._rawStatus || (trace?.status === "ok" ? "completed" : trace?.status || "unknown")}`,
    durMs: 1,
  });
  return steps;
}

function TraceTimelineRow({ step }) {
  return (
    <div className={"tl-item trace-timeline-item" + (step.isError ? " error" : "")}>
      <div className={"gut " + step.kind}><div className="node"></div></div>
      <div className="label">
        <span className={"kind " + step.kind}>{step.kind}</span>
        <span className="name" title={step.label}>{step.label}</span>
      </div>
      <div className="dur">{fmtMs(step.durMs)}</div>
    </div>
  );
}

function TraceExecutionCard({ trace, turnNumber }) {
  const [outputOpen, setOutputOpen] = tUseState(false);
  const usage = traceUsage(trace);
  const steps = traceTimelineSteps(trace);
  const rounds = steps.filter((step) => step.kind === "llm").length;
  const tools = steps.filter((step) => step.kind === "tool").length;
  const output = trace.finalOutput || "No assistant text was recorded for this run.";

  return (
    <div className="run-card trace-execution-card">
      <div className="head">
        <div>
          <div className="turn">Turn {turnNumber}</div>
          <div className="ids" style={{ marginTop: 3 }}>
            <span><span className="k">thread</span> {trace.threadId}</span>
            <span><span className="k">run</span> {trace.runId}</span>
          </div>
          <div className="turn-usage">
            in {fmtTokens(usage.inputTokens)} · out {fmtTokens(usage.outputTokens)} · {fmtCost(usage.costUsd)} · {rounds} rounds · {tools} tools
          </div>
        </div>
        <div className="right"><span className="dur">{fmtMs(trace.durationMs)}</span></div>
      </div>
      <div className="timeline">
        {!trace._detailLoaded && trace._live
          ? <div className="trace-execution-loading">Loading run execution…</div>
          : steps.map((step, index) => <TraceTimelineRow key={`${step.kind}-${index}`} step={step} />)}
      </div>
      <div className="trace-final-output">
        <button type="button" onClick={() => setOutputOpen((open) => !open)} aria-expanded={outputOpen}>
          <Icon name={outputOpen ? "chevdown" : "chev"} size={11} />
          <span>Final output</span>
        </button>
        {outputOpen && <pre>{output}</pre>}
      </div>
    </div>
  );
}

// ----- waterfall -----
function Waterfall({ trace, selectedSpanId, onSelect }) {
  const total = Math.max(trace.durationMs, 1);
  // visible spans = all non-root
  const spans = trace.spans.filter((s) => s.kind !== "run");
  // make 5 axis ticks
  const ticks = [];
  const step = total / 4;
  for (let i = 0; i <= 4; i++) ticks.push(Math.round(step * i));

  return (
    <div className="wf-container">
      <div className="wf-summary">
        <span><b style={{ color: "var(--text)" }}>{spans.length}</b> spans</span>
        <span>·</span>
        <span>total <b style={{ color: "var(--text)" }}>{fmtMs(trace.durationMs)}</b></span>
        <div style={{ flex: 1 }}></div>
        <span className="legend"><span className="sw" style={{ background: "#c026d3" }}></span> LLM</span>
        <span className="legend"><span className="sw" style={{ background: "#2563eb" }}></span> Tool</span>
        <span className="legend"><span className="sw" style={{ background: "#7c3aed" }}></span> MCP</span>
        <span className="legend"><span className="sw" style={{ background: "#ef4444" }}></span> Error</span>
      </div>
      <div className="wf-table">
        <div className="wf-header-row">
          <div>Span</div>
          <div className="wf-axis">
            {ticks.map((t, i) => (
              <div key={i} className="tick" style={{ left: `${(i / 4) * 100}%` }}>{fmtMs(t)}</div>
            ))}
          </div>
          <div style={{ textAlign: "right" }}>Dur</div>
        </div>
        {spans.map((s) => {
          const leftPct = (s.startMs / total) * 100;
          const widthPct = Math.max(0.5, (s.durMs / total) * 100);
          const kindCls = s.error ? "err" : s.kind;
          return (
            <div
              key={s.id}
              className={"wf-row" + (selectedSpanId === s.id ? " active" : "")}
              onClick={() => onSelect(s.id)}
            >
              <div className="lbl">
                <span className={"kind " + kindCls}>{s.kind}</span>
                <span className="icn">
                  {s.kind === "llm" ? <Icon name="cpu" size={11} /> :
                   s.kind === "tool" || s.kind === "mcp" ? <Icon name="tool" size={11} /> :
                   <Icon name="x" size={11} />}
                </span>
                <span className="nm">{s.name}</span>
              </div>
              <div className="bar-wrap">
                {ticks.map((_, i) => (
                  <div key={i} className="grid-line" style={{ left: `${(i / 4) * 100}%` }}></div>
                ))}
                <div
                  className={"bar " + kindCls}
                  style={{ left: leftPct + "%", width: widthPct + "%" }}
                  title={`${s.name} · ${fmtMs(s.durMs)}`}
                ></div>
              </div>
              <div className="dur">{fmtMs(s.durMs)}</div>
            </div>
          );
        })}
      </div>
    </div>
  );
}

// ----- inspector -----
function Inspector({ trace, span }) {
  if (!span) {
    // overview when no span selected
    return (
      <div className="inspect">
        <div className="inspect-header">
          <div className="top">
            <span className="kind run">RUN</span>
            <h3>Run overview</h3>
            <span className="dur">{fmtMs(trace.durationMs)}</span>
          </div>
        </div>
        <Section title="User input"><pre className="text">{trace.userInput}</pre></Section>
        {trace.finalOutput && <Section title="Final output"><pre className="text">{trace.finalOutput}</pre></Section>}
        {trace.statusReason && (
          <Section title="Error">
            <div className="kv" style={{ borderColor: "#fecaca", background: "#fef2f2", color: "#b91c1c" }}>
              {trace.statusReason}
            </div>
          </Section>
        )}
        <Section title="Metadata">
          <div className="kv">
            <Row k="run_id" v={trace.runId} />
            <Row k="thread_id" v={trace.threadId} />
            <Row k="agent" v={trace.agentName} />
            <Row k="model" v={trace.model} />
            <Row k="status" v={trace.status} />
            <Row k="tokens_in"  v={trace.tokensIn} />
            <Row k="tokens_out" v={trace.tokensOut} />
            <Row k="cost_usd" v={fmtCost(trace.costUSD)} />
          </div>
        </Section>
      </div>
    );
  }

  const kindCls = span.error ? "err" : span.kind;
  return (
    <div className="inspect">
      <div className="inspect-header">
        <div className="top">
          <span className={"kind " + kindCls}>{span.kind}</span>
          <h3>{span.name}</h3>
          <span className="dur">{fmtMs(span.durMs)}</span>
        </div>
      </div>
      <Section title="Timing">
        <div className="kv">
          <Row k="start" v={fmtMs(span.startMs) + " from run start"} />
          <Row k="duration" v={fmtMs(span.durMs)} />
          <Row k="span_id" v={span.id} />
        </div>
      </Section>
      {span.payload?.args && (
        <Section title="Input / args" copy={JSON.stringify(span.payload.args, null, 2)}>
          <pre dangerouslySetInnerHTML={{ __html: highlightJSON(JSON.stringify(span.payload.args, null, 2)) }}></pre>
        </Section>
      )}
      {span.payload?.result && (
        <Section title={span.error ? "Error result" : "Output / result"} copy={JSON.stringify(span.payload.result, null, 2)}>
          <pre dangerouslySetInnerHTML={{ __html: highlightJSON(JSON.stringify(span.payload.result, null, 2)) }}></pre>
        </Section>
      )}
      {span.kind === "llm" && (
        <>
          {span.payload?.prompt_tokens != null && (
            <Section title="Tokens">
              <div className="kv">
                {span.payload.prompt_tokens != null && <Row k="prompt"     v={span.payload.prompt_tokens} />}
                {span.payload.completion_tokens != null && <Row k="completion" v={span.payload.completion_tokens} />}
                {span.payload.messages_in != null && <Row k="messages" v={span.payload.messages_in} />}
              </div>
            </Section>
          )}
          <Section title={span.name.includes("respond") ? "Generated text (sample)" : "Reasoning summary"}>
            <pre className="text">{span.name.includes("respond")
              ? (trace.finalOutput || SAMPLE_FULL_OUTPUT)
              : "Decide next action: based on the user's question, calling the appropriate tool to gather missing information."}
            </pre>
          </Section>
        </>
      )}
      {span.error && span.payload?.message && (
        <Section title="Error message">
          <div className="kv" style={{ borderColor: "#fecaca", background: "#fef2f2", color: "#b91c1c" }}>
            {span.payload.message}
          </div>
        </Section>
      )}
    </div>
  );
}
function Section({ title, copy, children }) {
  const [copied, setCopied] = tUseState(false);
  const onCopy = () => {
    if (copy) {
      window.copyText?.(copy);
      setCopied(true);
      setTimeout(() => setCopied(false), 1200);
    }
  };
  return (
    <div className="inspect-section">
      <div className="sec-head">
        {title}
        {copy && <span className="copy" onClick={onCopy}>{copied ? "copied" : "copy"}</span>}
      </div>
      {children}
    </div>
  );
}
function Row({ k, v }) {
  return <div className="row"><span className="k">{k}</span><span className="v">{v}</span></div>;
}

// ----- detail header -----
function DetailHeader({ trace, onReplay, onDownloadModelIO, downloadBusy, onAddToDataset }) {
  const sCls = trace.status === "ok" ? "ok" : trace.status === "warn" ? "warn" : "err";
  const copyRunId = () => {
    window.copyText?.(trace.runId);
  };
  return (
    <div className="detail-header">
      <div className="top">
        <span className="av" style={{ background: trace.agentColor }}>{trace.agentInitials}</span>
        <span className="title">{trace.userInput}</span>
        <div className="actions">
          <button className="btn" onClick={copyRunId}><Icon name="copy" size={11} /> Copy ID</button>
          <button className="btn" onClick={onAddToDataset}><Icon name="plus" size={11} /> Add to dataset</button>
          <button className="btn primary" onClick={onReplay}>
            <Icon name="play" size={11} /> Replay
          </button>
          <button className="btn" onClick={onDownloadModelIO} disabled={downloadBusy} title="下载完整 Model IO 诊断包">
            <Icon name="upload" size={11} className="rotate-180" /> {downloadBusy ? "Preparing…" : "Download"}
          </button>
        </div>
      </div>
      <div className="ids">
        <span><span className="k">run</span> <span className="copyable">{trace.runId}</span></span>
        <span><span className="k">thread</span> <span className="copyable">{trace.threadId}</span></span>
        {trace.userId && <span><span className="k">user</span> <span className="copyable">{trace.userId}</span></span>}
        <span><span className="k">started</span> {new Date(trace.startedAt).toLocaleString()}</span>
      </div>
      <div className="badges">
        <span className={"badge-pill " + sCls}>
          {trace.status === "ok" ? <Icon name="check" size={10} /> : <Icon name="x" size={10} />}
          {trace.status}
        </span>
        <span className="badge-pill mono">{trace.agentName}</span>
        <span className="badge-pill mono">{trace.model}</span>
        <span className="badge-pill mono"><Icon name="clock" size={10} /> {fmtMs(trace.durationMs)}</span>
        <span className="badge-pill mono"><Icon name="loop" size={10} /> {trace.llmRounds} LLM</span>
        <span className="badge-pill mono"><Icon name="tool" size={10} /> {trace.toolCalls} tools</span>
        <span className="badge-pill mono"><Icon name="cpu" size={10} /> {fmtTokens(trace.tokensIn + trace.tokensOut)} tok</span>
        <span className="badge-pill mono">{fmtCost(trace.costUSD)}</span>
      </div>
    </div>
  );
}

// ----- dashboard tab -----
function Sparkline({ buckets, valueFn = (b) => b.count, color = "#0d0d0d", errLine = false }) {
  const w = 100, h = 50;
  const vals = buckets.map(valueFn);
  const max = Math.max(1, ...vals);
  const pts = vals.map((v, i) => `${(i / (vals.length - 1)) * w},${h - (v / max) * (h - 6) - 2}`);
  const area = `M0,${h} L${pts.join(" L")} L${w},${h} Z`;
  const line = `M${pts.join(" L")}`;
  const errVals = buckets.map((b) => b.errors);
  const errPts = errVals.map((v, i) => `${(i / (vals.length - 1)) * w},${h - (v / max) * (h - 6) - 2}`);
  const errLinePath = `M${errPts.join(" L")}`;
  return (
    <svg className="spark" viewBox={`0 0 ${w} ${h}`} preserveAspectRatio="none">
      <path className="area" d={area} />
      <path d={line} style={{ stroke: color }} />
      {errLine && <path className="err-line" d={errLinePath} />}
    </svg>
  );
}
function TopBars({ rows, valueLabel = "" }) {
  const max = Math.max(1, ...rows.map((r) => r.value));
  return (
    <div>
      {rows.map((r, i) => (
        <div key={i} className="tool-bar-row">
          <div className="nm">{r.label}</div>
          <div className="bar"><span className="fill" style={{ width: `${(r.value / max) * 100}%`, background: r.color || "#0d0d0d" }}></span></div>
          <div className="num">{r.display ?? r.value}{valueLabel}</div>
        </div>
      ))}
    </div>
  );
}

function Dashboard({ traces }) {
  const buckets = bucketByHour(traces, 24);
  const okCount = traces.filter((t) => t.status === "ok").length;
  const errCount = traces.filter((t) => t.status === "error").length;
  const errRate = traces.length ? (errCount / traces.length * 100) : 0;
  const totalCost = traces.reduce((s, t) => s + t.costUSD, 0);
  const avgLat = traces.length ? traces.reduce((s, t) => s + t.durationMs, 0) / traces.length : 0;
  const p95 = (() => {
    const sorted = traces.map((t) => t.durationMs).sort((a, b) => a - b);
    return sorted[Math.floor(sorted.length * 0.95)] || 0;
  })();

  // top tools
  const toolStats = {};
  traces.forEach((t) => {
    t.spans.forEach((s) => {
      if (s.kind !== "tool" && s.kind !== "mcp") return;
      const key = s.name.split(".")[0];
      toolStats[key] = toolStats[key] || { name: key, calls: 0, totalMs: 0, errs: 0 };
      toolStats[key].calls += 1;
      toolStats[key].totalMs += s.durMs;
      if (s.error) toolStats[key].errs += 1;
    });
  });
  const topByCalls = Object.values(toolStats).sort((a, b) => b.calls - a.calls).slice(0, 6)
    .map((t) => ({ label: t.name, value: t.calls, color: "#2563eb", display: t.calls }));
  const topByLatency = Object.values(toolStats).sort((a, b) => (b.totalMs / b.calls) - (a.totalMs / a.calls)).slice(0, 6)
    .map((t) => ({ label: t.name, value: Math.round(t.totalMs / t.calls), color: "#c026d3", display: fmtMs(t.totalMs / t.calls) }));

  // by agent
  const agentStats = {};
  traces.forEach((t) => {
    agentStats[t.agentName] = agentStats[t.agentName] || { name: t.agentName, color: t.agentColor, runs: 0, cost: 0 };
    agentStats[t.agentName].runs += 1;
    agentStats[t.agentName].cost += t.costUSD;
  });
  const topAgentsByRuns = Object.values(agentStats).sort((a, b) => b.runs - a.runs)
    .map((a) => ({ label: a.name, value: a.runs, color: a.color, display: a.runs }));
  const topAgentsByCost = Object.values(agentStats).sort((a, b) => b.cost - a.cost)
    .map((a) => ({ label: a.name, value: a.cost, color: a.color, display: fmtCost(a.cost) }));

  return (
    <div className="dash">
      <div className="panel span-3">
        <div className="head"><h4>Runs (24h)</h4></div>
        <div className="kpi-num">{traces.length}</div>
        <div className="kpi-sub"><span className="down">↑ 12%</span> vs prev day</div>
        <div style={{ marginTop: 8 }}>
          <Sparkline buckets={buckets} />
        </div>
      </div>
      <div className="panel span-3">
        <div className="head"><h4>Error rate</h4></div>
        <div className="kpi-num" style={{ color: errRate > 5 ? "#b91c1c" : "var(--text)" }}>
          {errRate.toFixed(1)}<span style={{ fontSize: 14, color: "var(--text-muted)" }}>%</span>
        </div>
        <div className="kpi-sub">{errCount} errors / {traces.length} runs</div>
        <div style={{ marginTop: 8 }}>
          <Sparkline buckets={buckets} valueFn={(b) => b.errors} color="#ef4444" />
        </div>
      </div>
      <div className="panel span-3">
        <div className="head"><h4>p95 latency</h4></div>
        <div className="kpi-num">{fmtMs(p95)}</div>
        <div className="kpi-sub">avg {fmtMs(avgLat)}</div>
        <div style={{ marginTop: 8 }}>
          <Sparkline
            buckets={buckets}
            valueFn={(b) => b.count ? b.totalLatency / b.count : 0}
            color="#d97706"
          />
        </div>
      </div>
      <div className="panel span-3">
        <div className="head"><h4>Total cost</h4></div>
        <div className="kpi-num">{fmtCost(totalCost)}</div>
        <div className="kpi-sub">{Math.round(traces.reduce((s, t) => s + t.tokensIn + t.tokensOut, 0) / 1000)}k tokens</div>
        <div style={{ marginTop: 8 }}>
          <Sparkline
            buckets={buckets}
            valueFn={(b) => b.count}
            color="#10a37f"
          />
        </div>
      </div>

      <div className="panel span-6">
        <div className="head"><h4>Top tools by calls</h4></div>
        <TopBars rows={topByCalls} />
      </div>
      <div className="panel span-6">
        <div className="head"><h4>Top tools by avg latency</h4></div>
        <TopBars rows={topByLatency} />
      </div>

      <div className="panel span-6">
        <div className="head"><h4>Runs by agent</h4></div>
        <TopBars rows={topAgentsByRuns} />
      </div>
      <div className="panel span-6">
        <div className="head"><h4>Cost by agent</h4></div>
        <TopBars rows={topAgentsByCost} />
      </div>
    </div>
  );
}

function mergeLiveTraceSummaries(prev, next) {
  const prevById = new Map((prev || []).map((t) => [t.id, t]));
  return (next || []).map((fresh) => {
    const old = prevById.get(fresh.id);
    if (!old || !old._detailLoaded) return fresh;
    return {
      ...fresh,
      spans: old.spans,
      finalOutput: old.finalOutput,
      modelIO: old.modelIO,
      _events: old._events,
      _detail: old._detail,
      _detailLoaded: true,
    };
  });
}

function isActiveTrace(trace) {
  return trace?._rawStatus === "running" || trace?._rawStatus === "queued";
}

// ----- main app -----
function TracesApp() {
  // Real backend traces (PR 14b). Falls back to mock generator only when the
  // API call fails *and* the tenant has no real runs yet — in that case the
  // page shows the mock with a visible "Demo data" banner so the user knows
  // they're not looking at live state.
  const [allTraces, setAllTraces] = tUseState([]);
  const [liveAgents, setLiveAgents] = tUseState([]);  // populated from /v1/agents on mount
  const [loadingList, setLoadingList] = tUseState(true);
  const [loadError, setLoadError] = tUseState(null);
  // PR-S.17d · topbar 上 Users 入口 admin-gate
  const [isAdmin, setIsAdmin] = tUseState(false);
  tUseEffect(() => {
    window.api.authMe()
      .then((m) => setIsAdmin(!!m.is_admin))
      .catch(() => {});
  }, []);
  const [usingMock, setUsingMock] = tUseState(false);
  const [view, setView] = tUseState("list"); // list | dashboard
  const [search, setSearch] = tUseState("");
  const [fAgent, setFAgent] = tUseState(null);
  const [fModel, setFModel] = tUseState(null);
  const [fStatus, setFStatus] = tUseState(null);
  const [fTime, setFTime] = tUseState("today");
  const [fDate, setFDate] = tUseState(() => dateKey(new Date()));
  const [fScope, setFScope] = tUseState("me");
  const [liveTail, setLiveTail] = tUseState(true);
  const [selectedId, setSelectedId] = tUseState(null);
  const [selectedSpanId, setSelectedSpanId] = tUseState(null);
  const [detailTab, setDetailTab] = tUseState("runs"); // runs | events | model | raw
  const [addToDsTrace, setAddToDsTrace] = tUseState(null);
  const [replayBusy, setReplayBusy] = tUseState(false);
  const [downloadBusy, setDownloadBusy] = tUseState(false);
  const userPinnedTraceRef = tUseRef(false);

  // Load real traces from /v1/traces on mount + when liveTail flips on.
  tUseEffect(() => {
    let cancelled = false;
    async function load() {
      setLoadingList(true);
      setLoadError(null);
      try {
        // Fetch agents + traces in parallel. agents may fail (older server
        // / wrong tenant) without breaking trace load; in that case we
        // fall back to MOCK_TRACE_AGENTS / agent_id string for display.
        const [items, agentsResult] = await Promise.all([
          window.api.listTraces({ limit: 200, scope: isAdmin ? fScope : "me" }),
          window.api.listAgents().catch(() => []),
        ]);
        if (cancelled) return;
        const agents = (agentsResult && agentsResult.length)
          ? agentsResult
          : (window.INITIAL_AGENTS || window.MOCK_TRACE_AGENTS || []);
        setLiveAgents(agents);
        const traces = items.map((s) => traceFromApiSummary(s, agents));
        if (traces.length === 0) {
          // No real traces yet — show mock with visible banner so the page
          // isn't blank but the user knows it's not real.
          setAllTraces(generateTraces(60));
          setUsingMock(true);
        } else {
          setAllTraces((prev) => mergeLiveTraceSummaries(prev, traces));
          setUsingMock(false);
        }
      } catch (err) {
        if (cancelled) return;
        setLoadError(String(err && err.message || err));
        setAllTraces(generateTraces(60));
        setUsingMock(true);
      } finally {
        if (!cancelled) setLoadingList(false);
      }
    }
    load();
    let timer = null;
    if (liveTail) {
      timer = setInterval(load, 10000);
    }
    return () => { cancelled = true; if (timer) clearInterval(timer); };
  }, [liveTail, fScope, isAdmin]);

  // filter
  const filtered = tUseMemo(() => {
    const q = search.trim().toLowerCase();
    const selectedDate = fTime === "today" ? dateKey(new Date()) : fDate;
    return allTraces.filter((t) => {
      if (q && !(t.userInput.toLowerCase().includes(q) || t.runId.includes(q) || t.threadId.includes(q) || t.agentName.toLowerCase().includes(q) || (t.userId || "").toLowerCase().includes(q))) return false;
      if (fAgent && t.agentId !== fAgent) return false;
      if (fModel && t.model !== fModel) return false;
      if (fStatus && t.status !== fStatus) return false;
      if (selectedDate && dateKey(new Date(t.startedAt)) !== selectedDate) return false;
      return true;
    });
  }, [allTraces, search, fAgent, fModel, fStatus, fTime, fDate]);

  tUseEffect(() => {
    if (filtered.length && !filtered.find((t) => t.id === selectedId)) {
      setSelectedId(filtered[0].id);
      setSelectedSpanId(null);
      userPinnedTraceRef.current = false;
    }
  }, [filtered, selectedId]);

  const selected = filtered.find((t) => t.id === selectedId) || filtered[0];
  const selectedTurnNumber = tUseMemo(() => {
    if (!selected) return 1;
    const turns = allTraces
      .filter((trace) => trace.threadId === selected.threadId)
      .sort((left, right) => left.startedAt - right.startedAt);
    return Math.max(1, turns.findIndex((trace) => trace.id === selected.id) + 1);
  }, [allTraces, selected?.id, selected?.threadId]);

  // Live tail should actually follow a run while it is running. Without this,
  // a newly-created run can appear at the top of the list while detail polling
  // keeps refreshing an older selected trace, so Model IO never updates round
  // by round unless the user clicks the new row manually.
  tUseEffect(() => {
    if (!liveTail || userPinnedTraceRef.current || !filtered.length) return;
    const active = filtered.find(isActiveTrace);
    if (active && active.id !== selected?.id && !isActiveTrace(selected)) {
      setSelectedId(active.id);
      setSelectedSpanId(null);
    }
  }, [filtered, liveTail, selected?.id, selected?._rawStatus]);

  // Lazy-load detail (events + spans + finalOutput) when a live trace is
  // selected and we haven't fetched its detail yet. Mock traces already have
  // `spans` populated by generateTraces() so the detail call is skipped.
  tUseEffect(() => {
    if (!selected || !selected._live || selected._detailLoaded) return;
    let cancelled = false;
    (async () => {
      try {
        const detail = await window.api.getTraceDetail(selected.runId);
        if (cancelled) return;
        setAllTraces((prev) => prev.map((t) =>
          t.id === selected.id ? hydrateTraceFromDetail(t, detail) : t
        ));
      } catch (err) {
        if (!cancelled) console.warn("getTraceDetail failed", err);
      }
    })();
    return () => { cancelled = true; };
  }, [selected?.id, selected?._live, selected?._detailLoaded]);

  // While live tailing, keep the selected trace detail fresh too. The list
  // refresh alone only updates summaries; Model IO records are stored in the
  // detail payload, including the "running" record written before the model
  // response returns.
  tUseEffect(() => {
    if (!liveTail || !selected || !selected._live) return;
    let cancelled = false;
    async function loadSelectedDetail() {
      try {
        const detail = await window.api.getTraceDetail(selected.runId);
        if (cancelled) return;
        setAllTraces((prev) => prev.map((t) =>
          t.id === selected.id ? hydrateTraceFromDetail(t, detail) : t
        ));
      } catch (err) {
        if (!cancelled) console.warn("getTraceDetail live refresh failed", err);
      }
    }
    loadSelectedDetail();
    const timer = setInterval(loadSelectedDetail, 1500);
    return () => { cancelled = true; clearInterval(timer); };
  }, [liveTail, selected?.id, selected?._live]);

  async function doReplay(trace) {
    if (!trace || replayBusy) return;
    if (!trace._live) {
      window.alert("This is mock data — replay only works on live traces.");
      return;
    }
    setReplayBusy(true);
    try {
      const result = await window.api.replayTrace(trace.runId);
      const newRunId = result?.run?.run_id || result?.run_id || "(no run_id)";
      window.alert(`Replay enqueued — new run ${newRunId}. Open Playground to watch it.`);
    } catch (err) {
      window.alert("Replay failed: " + (err && err.message || err));
    } finally {
      setReplayBusy(false);
    }
  }

  async function doDownloadModelIO(trace) {
    if (!trace || downloadBusy) return;
    setDownloadBusy(true);
    try {
      await window.api.downloadTraceModelIO(trace.runId);
      window.showToast?.("Model IO archive downloaded");
    } catch (err) {
      window.alert("Model IO download failed: " + (err && err.message || err));
    } finally {
      setDownloadBusy(false);
    }
  }

  // Agent source priority: live /v1/agents response (post-PR-1 fix —
  // shows real Agent.name like "agent_default → 'Default Agent'") →
  // window.INITIAL_AGENTS (set by manage-app.jsx if user navigated from
  // there in same SPA) → MOCK_TRACE_AGENTS (last-resort placeholders).
  const agentSource = liveAgents.length
    ? liveAgents
    : ((INITIAL_AGENTS && INITIAL_AGENTS.length) ? INITIAL_AGENTS : (window.MOCK_TRACE_AGENTS || []));
  const agentOpts = agentSource.map((a) => ({ id: a.id, label: a.name, color: a.color }));
  const modelOpts = [...new Set(allTraces.map((t) => t.model))].map((m) => ({ id: m, label: m }));
  const statusOpts = [
    { id: "ok", label: "ok", color: "#10a37f" },
    { id: "error", label: "error", color: "#ef4444" },
    { id: "warn", label: "warn", color: "#d97706" },
  ];
  return (
    <div className="app">
      <div className="topbar">
        <div className="brand">
          <div className="dot">百</div>
          baizhi
          <span style={{ color: "var(--text-faint)" }}>/</span>
          <span style={{ fontWeight: 500, color: "var(--text-muted)" }}>Traces</span>
        </div>
        <div className="tabs">
          <a className="" href="/playground" style={navLinkStyle()}>Playground</a>
          <a className="" href="/manage" style={navLinkStyle()}>Manage</a>
          <a href="/evals" style={navLinkStyle()}>Evals</a>
          <button className="active">Traces</button>
          <a href="/marketplace" style={navLinkStyle()}>Marketplace</a>
          <a href="/apps" style={navLinkStyle()}>Apps</a>
          {isAdmin && <a href="/users" style={navLinkStyle()}>Users</a>}
          <a href="/api-docs" style={navLinkStyle()}>API&nbsp;Docs</a>
        </div>
        <div className="spacer" />
        <div className="actions">
          <div style={{ display: "flex", border: "1px solid var(--border)", borderRadius: 6, overflow: "hidden" }}>
            <button
              className={"btn ghost sm"}
              style={{ borderRadius: 0, background: view === "list" ? "var(--bg-sunken)" : "transparent" }}
              onClick={() => setView("list")}
            ><Icon name="list" size={12} /> List</button>
            <button
              className={"btn ghost sm"}
              style={{ borderRadius: 0, background: view === "dashboard" ? "var(--bg-sunken)" : "transparent" }}
              onClick={() => setView("dashboard")}
            ><Icon name="cpu" size={12} /> Dashboard</button>
          </div>
          <button className="btn ghost sm"><Icon name="share" size={12} /> Export</button>
        </div>
      </div>

      {(usingMock || loadError) && (
        <div style={{
          padding: "6px 16px",
          background: loadError ? "#fee2e2" : "#fef3c7",
          borderBottom: "1px solid var(--border)",
          fontSize: 12,
          color: loadError ? "#991b1b" : "#92400e",
          fontFamily: "var(--font-mono)",
        }}>
          {loadError
            ? `⚠ Failed to load live traces (${loadError}). Showing mock data.`
            : "ⓘ Demo data — no live traces for this tenant yet. Run something in Playground to see real traces here."}
        </div>
      )}
      <div className="filter-bar">
        <div className="search-input">
          <span className="ic"><Icon name="search" size={12} /></span>
          <input
            placeholder="Search input, run id, thread id…"
            value={search}
            onChange={(e) => setSearch(e.target.value)}
          />
        </div>
        <FilterChip label="agent"  value={fAgent}  options={agentOpts}  onChange={setFAgent} />
        <FilterChip label="model"  value={fModel}  options={modelOpts}  onChange={setFModel} />
        <FilterChip label="status" value={fStatus} options={statusOpts} onChange={setFStatus} />
        <SelectFilter label="time" value={fTime} options={[
          { id: "today", label: "Today" },
          { id: "date", label: "Select date" },
        ]} onChange={setFTime} />
        {fTime === "date" && <input
          className="trace-date-filter"
          type="date"
          value={fDate}
          onChange={(event) => setFDate(event.target.value)}
          aria-label="Trace date"
        />}
        {isAdmin && <SelectFilter label="scope" value={fScope} options={[
          { id: "me", label: "Me" },
          { id: "all", label: "All" },
        ]} onChange={setFScope} />}
        <div style={{ flex: 1 }} />
        <button
          className={"live-tail" + (liveTail ? " on" : "")}
          onClick={() => {
            const next = !liveTail;
            if (next) userPinnedTraceRef.current = false;
            setLiveTail(next);
          }}
        >
          <span className="dot"></span>
          {liveTail ? "Live tailing" : "Live tail"}
        </button>
      </div>

      {view === "dashboard" ? (
        <div style={{ flex: 1, overflowY: "auto" }}>
          <Dashboard traces={filtered} />
        </div>
      ) : (
        <div className="traces-shell">
          <div className="trace-list-col">
            <SummaryStrip traces={filtered} />
            <div className="trace-table-head">
              <div></div>
              <div>Trace</div>
              <div style={{ textAlign: "right" }}>Latency</div>
              <div style={{ textAlign: "right" }}>Cost</div>
            </div>
            <div className="trace-list">
              {filtered.length === 0 ? (
                <div className="empty" style={{ padding: 40 }}>
                  <div className="icon"><Icon name="search" size={20} /></div>
                  <h3>No traces match</h3>
                  <p>Adjust your filters or expand the time window.</p>
                </div>
              ) : filtered.map((t) => (
                <TraceRow
                  key={t.id}
                  trace={t}
                  active={t.id === selected?.id}
                  onClick={() => {
                    userPinnedTraceRef.current = true;
                    setSelectedId(t.id);
                    setSelectedSpanId(null);
                  }}
                />
              ))}
            </div>
          </div>

          <div className="trace-detail-col">
            {!selected ? (
              <div className="empty"><p>Select a trace</p></div>
            ) : (
              <>
                <DetailHeader
                  trace={selected}
                  onReplay={() => doReplay(selected)}
                  onDownloadModelIO={() => doDownloadModelIO(selected)}
                  downloadBusy={downloadBusy}
                  onAddToDataset={() => setAddToDsTrace(selected)}
                />
                <div className="detail-tabs">
                  <button className={detailTab === "runs" ? "active" : ""} onClick={() => setDetailTab("runs")}>
                    <Icon name="cpu" size={12} /> Runs <span className="count">{selected.llmRounds} rounds</span>
                  </button>
                  <button className={detailTab === "events" ? "active" : ""} onClick={() => setDetailTab("events")}>
                    <Icon name="list" size={12} /> AG-UI events
                  </button>
                  <button className={detailTab === "model" ? "active" : ""} onClick={() => setDetailTab("model")}>
                    <Icon name="cpu" size={12} /> Model IO <span className="count">{selected.modelIO?.length || 0}</span>
                  </button>
                  <button className={detailTab === "raw" ? "active" : ""} onClick={() => setDetailTab("raw")}>
                    <Icon name="code" size={12} /> Raw JSON
                  </button>
                </div>

                {detailTab === "runs" && (
                  <div className="trace-execution-pane">
                    <TraceExecutionCard trace={selected} turnNumber={selectedTurnNumber} />
                  </div>
                )}
                {detailTab === "events" && (
                  <div style={{ flex: 1, overflowY: "auto", padding: 16 }}>
                    <EventsList trace={selected} />
                  </div>
                )}
                {detailTab === "model" && (
                  <div style={{ flex: 1, overflowY: "auto", padding: 16 }}>
                    <ModelIOList trace={selected} />
                  </div>
                )}
                {detailTab === "raw" && (
                  <div style={{ flex: 1, overflowY: "auto", padding: 16 }}>
                    <RawJson trace={selected} />
                  </div>
                )}
              </>
            )}
          </div>
        </div>
      )}
      {addToDsTrace && (
        <AddToDatasetModal
          trace={addToDsTrace}
          onClose={() => setAddToDsTrace(null)}
        />
      )}
      <ToastContainer />
    </div>
  );
}

function navLinkStyle() {
  return {
    background: "transparent", border: 0, padding: "6px 10px", borderRadius: 6,
    color: "var(--text-muted)", fontSize: 13, textDecoration: "none", cursor: "pointer",
  };
}

function EventsList({ trace }) {
  if (trace._events?.length) {
    const events = [];
    events.push({ type: "RUN_STARTED", threadId: trace.threadId, runId: trace.runId, timestamp: trace.startedAt });
    const msgId = "msg_" + trace.runId.slice(-6);
    events.push({ type: "TEXT_MESSAGE_START", messageId: msgId, role: "assistant", timestamp: trace.startedAt + 10 });
    for (const ev of trace._events || []) {
      const payload = ev.payload || {};
      const ts = ev.timestamp ? Math.floor(ev.timestamp * 1000) : Date.now();
      if (ev.type === "tool.started") {
        const callId = payload["tool.call_id"] || payload.tool_call_id || payload.call_id;
        events.push({
          type: "TOOL_CALL_START",
          toolCallId: callId,
          toolCallName: payload["tool.name"] || payload.tool_name,
          parentMessageId: msgId,
          timestamp: ts,
        });
        events.push({
          type: "TOOL_CALL_ARGS",
          toolCallId: callId,
          toolCallName: payload["tool.name"] || payload.tool_name,
          delta: JSON.stringify(payload["tool.args"] || payload.arguments || {}),
          timestamp: ts + 1,
        });
        events.push({
          type: "TOOL_CALL_END",
          toolCallId: callId,
          timestamp: ts + 2,
        });
      } else if (ev.type === "tool.completed") {
        events.push({
          type: "TOOL_CALL_RESULT",
          messageId: `tool_${payload["tool.call_id"] || payload.tool_call_id || payload.call_id}`,
          toolCallId: payload["tool.call_id"] || payload.tool_call_id || payload.call_id,
          role: "tool",
          content: {
            result: payload.result_text || payload.result || "",
            result_ref: payload.result_ref || null,
            duration_ms: payload.duration_ms || 0,
            is_error: !!payload.is_error,
          },
          timestamp: ts,
        });
      } else if (ev.type === "text.delta") {
        events.push({ type: "TEXT_MESSAGE_CONTENT", messageId: msgId, delta: payload.delta || "", timestamp: ts });
      } else if (ev.type === "thinking.started") {
        events.push({
          type: "REASONING_START",
          messageId: payload.thinking_id,
          parentMessageId: msgId,
          timestamp: ts,
        });
        events.push({
          type: "REASONING_MESSAGE_START",
          messageId: payload.thinking_id,
          role: "assistant",
          parentMessageId: msgId,
          timestamp: ts + 1,
        });
      } else if (ev.type === "thinking.delta") {
        events.push({
          type: "REASONING_MESSAGE_CONTENT",
          messageId: payload.thinking_id,
          delta: payload.delta || "",
          timestamp: ts,
        });
      } else if (ev.type === "thinking.completed") {
        events.push({
          type: "REASONING_MESSAGE_END",
          messageId: payload.thinking_id,
          text: payload.text || "",
          timestamp: ts,
        });
        events.push({
          type: "REASONING_END",
          messageId: payload.thinking_id,
          timestamp: ts + 1,
        });
      } else if (ev.type === "run.awaiting_input") {
        events.push({ type: "CUSTOM", name: payload.event_name || "hil_interruption", value: payload, timestamp: ts });
      }
    }
    events.push({ type: "TEXT_MESSAGE_END", messageId: msgId, timestamp: trace.startedAt + trace.durationMs - 5 });
    events.push({ type: "RUN_FINISHED", threadId: trace.threadId, runId: trace.runId,
      timestamp: trace.startedAt + trace.durationMs, output: { text: trace.finalOutput } });
    return <RenderedEvents events={events} trace={trace} />;
  }
  // Reconstruct an AG-UI events list from spans
  const events = [];
  events.push({ type: "RUN_STARTED", threadId: trace.threadId, runId: trace.runId, timestamp: trace.startedAt });
  const msgId = "msg_" + trace.runId.slice(-6);
  events.push({ type: "TEXT_MESSAGE_START", messageId: msgId, role: "assistant", timestamp: trace.startedAt + 10 });
  trace.spans.filter((s) => s.kind !== "run").forEach((s) => {
    if (s.kind === "tool" || s.kind === "mcp") {
      events.push({ type: "TOOL_CALL_START", toolCallId: s.id, toolCallName: s.name, parentMessageId: msgId,
        timestamp: trace.startedAt + s.startMs });
      if (s.payload?.args) events.push({ type: "TOOL_CALL_ARGS", toolCallId: s.id, toolCallName: s.name, delta: JSON.stringify(s.payload.args),
        timestamp: trace.startedAt + s.startMs + 2 });
      events.push({ type: "TOOL_CALL_END", toolCallId: s.id, timestamp: trace.startedAt + s.startMs + 3 });
      events.push({ type: "TOOL_CALL_RESULT", messageId: `tool_${s.id}`, toolCallId: s.id,
        role: "tool", content: { result: s.payload?.result, duration_ms: Math.round(s.durMs) },
        timestamp: trace.startedAt + s.startMs + s.durMs });
    } else if (s.kind === "llm") {
      events.push({ type: "TEXT_MESSAGE_CONTENT", messageId: msgId, delta: "(chunk)",
        timestamp: trace.startedAt + s.startMs + s.durMs / 2 });
    } else if (s.kind === "err") {
      events.push({ type: "ERROR", message: s.payload?.message, timestamp: trace.startedAt + s.startMs });
    }
  });
  events.push({ type: "TEXT_MESSAGE_END", messageId: msgId, timestamp: trace.startedAt + trace.durationMs - 5 });
  events.push({ type: "RUN_FINISHED", threadId: trace.threadId, runId: trace.runId,
    timestamp: trace.startedAt + trace.durationMs, output: { text: trace.finalOutput } });

  return <RenderedEvents events={events} trace={trace} />;
}

function RenderedEvents({ events, trace }) {
  return (
    <div>
      {events.map((ev, i) => (
        <div key={i} style={{
          background: "white", border: "1px solid var(--border)", borderRadius: 8,
          padding: "8px 10px", marginBottom: 6,
        }}>
          <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 4 }}>
            <span style={{
              fontFamily: "var(--font-mono)", fontSize: 11, fontWeight: 600,
              padding: "1px 6px", borderRadius: 3,
              background: ev.type.includes("TOOL") ? "#eaf1ff" :
                          ev.type.includes("TEXT") ? "#fbe7fd" :
                          ev.type === "ERROR" ? "#fee2e2" :
                          "#e6f4ee",
              color: ev.type.includes("TOOL") ? "#2563eb" :
                     ev.type.includes("TEXT") ? "#c026d3" :
                     ev.type === "ERROR" ? "#b91c1c" :
                     "#10a37f",
            }}>{ev.type}</span>
            <span style={{ fontFamily: "var(--font-mono)", fontSize: 10.5, color: "var(--text-faint)" }}>
              +{fmtMs(ev.timestamp - trace.startedAt)}
            </span>
          </div>
          <pre style={{
            margin: 0, background: "#0d0d0d", color: "#e5e5e5",
            padding: "8px 10px", borderRadius: 6,
            fontFamily: "var(--font-mono)", fontSize: 11, overflow: "auto", lineHeight: 1.5,
          }} dangerouslySetInnerHTML={{ __html: highlightJSON(JSON.stringify(ev, null, 2)) }}></pre>
        </div>
      ))}
    </div>
  );
}

function RawJson({ trace }) {
  return (
    <pre style={{
      margin: 0, background: "#0d0d0d", color: "#e5e5e5",
      padding: 14, borderRadius: 8,
      fontFamily: "var(--font-mono)", fontSize: 11.5, lineHeight: 1.55,
      overflow: "auto",
    }} dangerouslySetInnerHTML={{ __html: highlightJSON(JSON.stringify(trace, null, 2)) }}></pre>
  );
}

function countModelTools(tools) {
  if (!Array.isArray(tools)) return 0;
  return tools.reduce((count, tool) => {
    const decls = tool?.function_declarations;
    return count + (Array.isArray(decls) ? decls.length : 1);
  }, 0);
}

function modelInputMessages(input) {
  if (input?.messages != null) return input.messages;
  if (input?.preview) {
    return [{ truncated: true, reason: "legacy whole-input trace was truncated", preview: input.preview }];
  }
  return [];
}

function modelSystemInstruction(input) {
  return input?.system_instruction ?? input?.config?.system_instruction ?? "";
}

function modelInputTools(input) {
  if (input?.tools != null) return input.tools;
  if (input?.preview) {
    return { truncated: true, reason: "legacy whole-input trace was truncated", preview: input.preview };
  }
  return [];
}

function safeStringify(value, space = 2) {
  try {
    return JSON.stringify(value, null, space);
  } catch (err) {
    return String(value ?? "");
  }
}

function fullText(value) {
  return typeof value === "string" ? value : safeStringify(value);
}

function modelPartText(part) {
  if (!part) return "";
  if (typeof part.text === "string") return part.text;
  if (part.text != null) return fullText(part.text);
  if (part.function_call) return fullText(part.function_call);
  if (part.function_response) return fullText(part.function_response);
  return fullText(part);
}

function flattenModelTools(tools) {
  if (!Array.isArray(tools)) return [];
  const out = [];
  tools.forEach((tool, toolIndex) => {
    const decls = tool?.function_declarations;
    if (Array.isArray(decls) && decls.length) {
      decls.forEach((decl, declIndex) => out.push({
        ...decl,
        _toolIndex: toolIndex,
        _declIndex: declIndex,
      }));
    } else {
      out.push({
        name: tool?.name || `tool_${toolIndex + 1}`,
        description: tool?.description,
        parameters: tool?.parameters || tool,
        _toolIndex: toolIndex,
        _declIndex: 0,
      });
    }
  });
  return out;
}

function JsonBlock({ value }) {
  return <pre>{fullText(value)}</pre>;
}

function ModelMessages({ messages }) {
  if (!Array.isArray(messages) || !messages.length) {
    return <div className="empty" style={{ minHeight: 72 }}>No input messages captured</div>;
  }
  return (
    <div className="model-io-items">
      {messages.map((msg, index) => {
        const parts = Array.isArray(msg?.parts) ? msg.parts : [];
        return (
          <details className="model-io-item" key={index} open={index < 3}>
            <summary>
              <span>Message {index + 1}</span>
              <span className="mono">{msg?.role || "unknown"}</span>
            </summary>
            <button className="model-io-copy" onClick={() => {
              window.copyText?.(safeStringify(msg));
            }}>copy</button>
            {parts.length ? parts.map((part, partIndex) => (
              <pre className="text" key={partIndex}>{modelPartText(part)}</pre>
            )) : (
              <JsonBlock value={msg} />
            )}
          </details>
        );
      })}
    </div>
  );
}

function ModelSystemInstruction({ value }) {
  if (!value) {
    return <div className="empty" style={{ minHeight: 72 }}>No system instruction captured</div>;
  }
  return (
    <div className="model-io-items">
      <div className="model-io-item">
        <pre className="text">{fullText(value)}</pre>
      </div>
    </div>
  );
}

function ModelTools({ tools }) {
  const flat = flattenModelTools(tools);
  if (!flat.length) {
    return <div className="empty" style={{ minHeight: 72 }}>No tools advertised to the model</div>;
  }
  return (
    <div className="model-io-items">
      {flat.map((tool, index) => (
        <details className="model-io-item" key={`${tool.name || "tool"}-${index}`} open={index < 5}>
          <summary>
            <span>{index + 1}. {tool.name || "tool"}</span>
          </summary>
          <button className="model-io-copy" onClick={() => {
            window.copyText?.(safeStringify(tool));
          }}>copy</button>
          {tool.description && <pre className="text">{tool.description}</pre>}
          <JsonBlock value={tool.parameters || tool} />
        </details>
      ))}
    </div>
  );
}

function ModelIOList({ trace }) {
  const calls = trace.modelIO || [];
  if (!calls.length) {
    return (
      <div className="empty" style={{ minHeight: 240 }}>
        <p>No model IO captured for this trace</p>
      </div>
    );
  }
  return (
    <div style={{ display: "grid", gap: 14 }}>
      {calls.map((call) => (
        <div key={call.call_id} className="inspect-section" style={{ margin: 0 }}>
          {(() => {
            const systemInstruction = modelSystemInstruction(call.input);
            const messages = modelInputMessages(call.input);
            const tools = modelInputTools(call.input);
            const toolCount = countModelTools(tools);
            return (
              <React.Fragment>
          <div className="sec-head">
            <span>Round {call.round_index} · {call.model}</span>
            {!call.completed_at_ms && <span className="pill">running</span>}
            <span className="copy" onClick={() => window.copyText?.(JSON.stringify(call, null, 2))}>copy</span>
          </div>
          <div className="kv" style={{ marginBottom: 10 }}>
            <Row k="call_id" v={call.call_id} />
            <Row k="latency_ms" v={call.latency_ms ?? "—"} />
            <Row k="input_tokens" v={modelUsageValue(call.usage, "input_tokens", "prompt_token_count", "prompt_tokens")} />
            <Row k="output_tokens" v={modelUsageValue(call.usage, "output_tokens", "candidates_token_count", "completion_tokens")} />
            <Row k="tools" v={toolCount} />
          </div>
          <Section title="System instruction" copy={fullText(systemInstruction)}>
            <ModelSystemInstruction value={systemInstruction} />
          </Section>
          <Section title="Input messages" copy={JSON.stringify(messages, null, 2)}>
            <ModelMessages messages={messages} />
          </Section>
          <Section title="Tools" copy={JSON.stringify(tools, null, 2)}>
            <ModelTools tools={tools} />
          </Section>
          <Section title="Output" copy={JSON.stringify(call.output, null, 2)}>
            {call.completed_at_ms ? (
              <JsonBlock value={call.output || {}} />
            ) : (
              <div className="empty" style={{ minHeight: 80 }}>Waiting for model output…</div>
            )}
          </Section>
          {call.output?.reasoning?.text && (
            <Section title="Reasoning" copy={call.output.reasoning.text}>
              <pre className="text">{call.output.reasoning.text}</pre>
            </Section>
          )}
              </React.Fragment>
            );
          })()}
        </div>
      ))}
    </div>
  );
}

function TracesAppWithModal() {
  return <TracesApp />;
}

ReactDOM.createRoot(document.getElementById("root")).render(<TracesApp />);
