// Generate ~50 realistic mock traces.
// Spans have start_offset_ms from the run start, and parent linkage for nesting.
//
// The Traces page is still mock-driven (PR 14 will wire real `/v1/traces`).
// Until then we synthesize a fixed agent pool here instead of depending on
// `window.INITIAL_AGENTS` — that global is now populated from /v1/agents at
// runtime and may be empty (no agents created yet), which used to silently
// produce 0 traces and render the page blank (Codex BUG-3).

const MOCK_TRACE_AGENTS = [
  { id: "agt_general",  name: "General Assistant", color: "#10a37f", initials: "GA", model: "gpt-4o" },
  { id: "agt_coder",    name: "Code Reviewer",     color: "#2563eb", initials: "CR", model: "claude-3.7-sonnet" },
  { id: "agt_analyst",  name: "Data Analyst",      color: "#d97706", initials: "DA", model: "gpt-4o" },
  { id: "agt_research", name: "Research Agent",    color: "#c026d3", initials: "RA", model: "claude-haiku-4.5" },
];
window.MOCK_TRACE_AGENTS = MOCK_TRACE_AGENTS;

const TRACE_SAMPLE_INPUTS = [
  "Summarize the latest Anthropic release notes",
  "Find the top 5 PRs opened this week in acme/web",
  "查一下 2025 年 Q1 北美 SaaS 公司 ARR 增长中位数",
  "Refactor this Python function to be O(n log n)",
  "Compute 17% compounded monthly for 3 years on $4,200",
  "Generate a cohort retention chart from the orders table",
  "What did the team discuss in #eng-platform yesterday?",
  "把这份 PRD 翻译成英文并提炼 5 个 bullet point",
  "Read https://news.ycombinator.com and pick 3 stories about agents",
  "Run: SELECT count(*) FROM users WHERE created_at > now() - interval '7 days'",
  "Why is the checkout p95 latency spiking since Friday?",
  "为什么我的 token 用量这周翻倍了",
  "Find issues labeled 'p0' across all repos",
  "把昨天 #design 频道的讨论整理成一份会议纪要",
  "Diff the schema between staging and prod",
  "Plot daily active users for the last 30 days",
  "Search Notion for the on-call runbook",
  "Calculate the break-even point for our new pricing tier",
  "Suggest 3 prompt tweaks to reduce hallucinations in the analyst agent",
  "Open a PR that bumps lodash to the latest patch version",
  "Tell me what's broken in the last hour",
  "Why did this user fail to sign up? user_id=u_8421",
  "执行迁移脚本并报告耗时",
  "Pull the last 100 errors from the events table",
  "找出和这个 issue 重复的相关 issue",
];

const TRACE_FINAL_OUTPUTS = [
  "I found 5 results. The top one looks most relevant: Anthropic shipped a new memory feature on May 19...",
  "There are 7 open PRs this week. The 3 oldest are PR #4421, #4430, #4438 — all waiting on review.",
  "Q1 2025 median ARR growth for North American SaaS was ~28% YoY based on the search results below.",
  "Here is the refactor — replaced the nested loop with a heap-based merge to get O(n log n).",
  "Final amount: $4,200 × (1 + 0.17/12)^36 ≈ $6,983.42. Effective annual yield ≈ 18.39%.",
  "Generated cohort_retention.png — week 4 retention dropped from 38% → 31% in the latest cohort.",
  "Yesterday in #eng-platform: incident postmortem for the Friday 17:42 deploy + Q2 roadmap review.",
  "Translated. Key points: (1) tighter onboarding flow, (2) audit log v2, (3) per-skill billing...",
  "Picked 3 HN stories about agents — open-source MCP launcher, ReAct vs Plan-Execute benchmarks, and...",
  "Result: 1,842 users created in the last 7 days (+12% vs prior period).",
  "p95 jumped from 480ms → 1.2s on Friday 16:00. Correlates with deploy of commit a3f2d11 to checkout-svc.",
  "Token usage this week is 2.1× last week. The spike is concentrated in the analyst agent — three runs alone burned 142k tokens.",
];

const ERROR_MESSAGES = [
  "tool_call_failed: web_search returned 429 Too Many Requests",
  "kernel_timeout: code_interpreter exceeded 30s wall clock",
  "schema_violation: tool returned an unexpected field 'foo'",
  "context_length_exceeded: prompt was 198,432 tokens (max 128,000)",
  "mcp_disconnected: slack-mcp lost connection mid-call",
  "rate_limit: org-level rpm cap hit (10,000 / min)",
];

const SAMPLE_FULL_OUTPUT = `Here are the top 5 results, ranked by relevance:

1. **Anthropic ships persistent memory for Claude** — May 19. Claude can now opt into a memory store that persists across conversations within a workspace.
2. **MCP 1.0 spec finalized** — May 14. The official 1.0 spec adds streaming + auth flows.
3. **Computer Use 2.0 preview** — May 8. New screen-grounded action API.
4. **Claude 3.7 Sonnet release notes** — May 5. Tool-use improvements + cheaper price tier.
5. **Anthropic-Cohere benchmark study** — Apr 30.

Want me to expand any of these?`;

function makeWaterfallSpans(plan, opts = {}) {
  // plan: list of { kind, name, durMs, error?, payload? }
  // returns spans + total duration
  const spans = [];
  let t = 0;
  // RUN_STARTED + final RUN_FINISHED bracket
  spans.push({ id: rid("sp"), kind: "run", name: "agent.run", parent: null, startMs: 0, durMs: 0 });
  // initial system prompt -> LLM
  for (const step of plan) {
    spans.push({
      id: rid("sp"),
      kind: step.kind,
      name: step.name,
      parent: step.parent || null,
      startMs: t,
      durMs: step.durMs,
      error: !!step.error,
      payload: step.payload || null,
    });
    t += step.durMs + (step.gap ?? 18);
  }
  // close root span
  spans[0].durMs = t;
  return { spans, totalMs: t };
}

function makeTrace({ minutesAgo, agent, model, status, scenario }) {
  const userInput = scenario.input;

  // build a step plan
  const plan = [];

  // pre-LLM (always)
  plan.push({ kind: "llm", name: `LLM · plan (${model})`, durMs: 180 + Math.random() * 280, payload: {
    prompt_tokens: 642 + Math.floor(Math.random() * 200),
    role: "planner",
    messages_in: 2,
  }});

  // tools
  scenario.tools.forEach((tk) => {
    plan.push({
      kind: tk.kind || "tool",
      name: tk.name,
      durMs: tk.durMs ?? (200 + Math.random() * 600),
      payload: {
        args: tk.args || {},
        result: tk.error ? { error: tk.errorMsg } : tk.result || { ok: true },
      },
      error: !!tk.error,
    });
    // post-tool LLM
    if (tk.followLLM !== false) {
      plan.push({ kind: "llm", name: `LLM · reflect (${model})`, durMs: 120 + Math.random() * 200,
        payload: { reflecting_on: tk.name, prompt_tokens: 420 + Math.floor(Math.random() * 600) } });
    }
  });

  // final synthesis LLM
  plan.push({ kind: "llm", name: `LLM · respond (${model})`, durMs: 400 + Math.random() * 800,
    payload: { completion_tokens: 280 + Math.floor(Math.random() * 600), streaming: true } });

  // append a terminal error if status === error
  if (status === "error") {
    plan.push({ kind: "err", name: "fatal_error", durMs: 20,
      payload: { message: ERROR_MESSAGES[Math.floor(Math.random() * ERROR_MESSAGES.length)] }, error: true });
  }

  const { spans, totalMs } = makeWaterfallSpans(plan);

  const toolCalls = plan.filter((p) => p.kind === "tool" || p.kind === "mcp").length;
  const llmRounds = plan.filter((p) => p.kind === "llm").length;
  const tokensIn  = 800 + Math.floor(Math.random() * 4000) + llmRounds * 300;
  const tokensOut = 200 + Math.floor(Math.random() * 1200) + llmRounds * 80;
  const costUSD   = +((tokensIn * 0.000005 + tokensOut * 0.000015)).toFixed(4);

  const startedAt = Date.now() - minutesAgo * 60_000;

  return {
    id: rid("tr"),
    runId: rid("run"),
    threadId: rid("thread"),
    agentId: agent.id,
    agentName: agent.name,
    agentColor: agent.color,
    agentInitials: agent.initials,
    model,
    startedAt,
    durationMs: Math.round(totalMs),
    status,
    statusReason: status === "error" ? plan[plan.length - 1].payload?.message : null,
    tokensIn, tokensOut, costUSD,
    toolCalls, llmRounds,
    userInput,
    finalOutput: status === "ok"
      ? (TRACE_FINAL_OUTPUTS[Math.floor(Math.random() * TRACE_FINAL_OUTPUTS.length)])
      : null,
    spans,
  };
}

function generateTraces(n = 50) {
  const scenarios = [
    { input: TRACE_SAMPLE_INPUTS[0], tools: [
      { name: "web_search", args: { q: "anthropic release notes" }, result: { hits: 5 }, durMs: 620 },
    ]},
    { input: TRACE_SAMPLE_INPUTS[1], tools: [
      { name: "github.list_prs", kind: "mcp", args: { repo: "acme/web", state: "open" }, result: { count: 7 }, durMs: 340 },
      { name: "github.get_pr_files", kind: "mcp", args: { pr: 4421 }, result: { files: 14 }, durMs: 240 },
    ]},
    { input: TRACE_SAMPLE_INPUTS[2], tools: [
      { name: "web_search", args: { q: "saas median ARR Q1 2025" }, result: { hits: 8 }, durMs: 720 },
      { name: "url_reader", args: { url: "https://saastr.com/…" }, result: { kb: 24 }, durMs: 460 },
      { name: "calculator", args: { expr: "median([22,28,31,40,25])" }, result: { value: 28 }, durMs: 60 },
    ]},
    { input: TRACE_SAMPLE_INPUTS[3], tools: [
      { name: "code_interpreter", args: { language: "python", lines: 18 }, result: { ok: true }, durMs: 540 },
    ]},
    { input: TRACE_SAMPLE_INPUTS[4], tools: [
      { name: "calculator", args: { expr: "4200*(1+.17/12)^36" }, result: { value: 6983.42 }, durMs: 30 },
    ]},
    { input: TRACE_SAMPLE_INPUTS[5], tools: [
      { name: "sql_runner", args: { sql: "SELECT … FROM orders" }, result: { rows: 12400 }, durMs: 820 },
      { name: "chart_builder", args: { type: "line", x: "week" }, result: { png: "..." }, durMs: 380 },
    ]},
    { input: TRACE_SAMPLE_INPUTS[6], tools: [
      { name: "slack.fetch_channel", kind: "mcp", args: { channel: "eng-platform", since: "yesterday" }, result: { messages: 142 }, durMs: 440 },
    ]},
    { input: TRACE_SAMPLE_INPUTS[7], tools: []},
    { input: TRACE_SAMPLE_INPUTS[8], tools: [
      { name: "url_reader", args: { url: "https://news.ycombinator.com" }, result: { stories: 30 }, durMs: 380 },
      { name: "url_reader", args: { url: "https://item/..." }, result: { kb: 12 }, durMs: 290 },
    ]},
    { input: TRACE_SAMPLE_INPUTS[9], tools: [
      { name: "sql_runner", args: { sql: "SELECT count(*)…" }, result: { rows: 1, value: 1842 }, durMs: 220 },
    ]},
    { input: TRACE_SAMPLE_INPUTS[10], tools: [
      { name: "sql_runner", args: { sql: "SELECT percentile_cont…" }, result: { rows: 30 }, durMs: 980, followLLM: true },
      { name: "github.list_commits", kind: "mcp", args: { repo: "acme/checkout-svc", since: "friday" }, result: { count: 14 }, durMs: 420 },
    ]},
    // failing ones
    { input: TRACE_SAMPLE_INPUTS[11], tools: [
      { name: "sql_runner", args: { sql: "…" }, durMs: 1820, error: true, errorMsg: "connection_timeout" },
    ]},
    { input: TRACE_SAMPLE_INPUTS[12], tools: [
      { name: "github.list_issues", kind: "mcp", args: { label: "p0" }, result: { count: 9 }, durMs: 360 },
    ]},
    { input: TRACE_SAMPLE_INPUTS[13], tools: [
      { name: "slack.fetch_channel", kind: "mcp", args: { channel: "design", since: "yesterday" }, durMs: 1240, error: true, errorMsg: "channel_not_found" },
    ]},
  ];

  // Prefer real agents when available so the trace samples match what the
  // user actually has installed; fall back to the fixed mock pool so the
  // page is never blank.
  const agentsPool = (window.INITIAL_AGENTS && window.INITIAL_AGENTS.length)
    ? window.INITIAL_AGENTS
    : MOCK_TRACE_AGENTS;
  const modelsPool = ["gpt-4o", "gpt-4o-mini", "claude-3.7-sonnet", "claude-haiku-4.5"];

  const traces = [];
  for (let i = 0; i < n; i++) {
    const minutesAgo = Math.floor(Math.pow(i / n, 1.6) * 60 * 36); // last ~36h, weighted recent
    const agent = agentsPool[i % agentsPool.length];
    const model = agent.model || modelsPool[Math.floor(Math.random() * modelsPool.length)];
    const scenario = scenarios[i % scenarios.length];
    // sprinkle errors
    const willError = scenario.tools.some((t) => t.error) || (Math.random() < 0.07);
    const status = willError ? "error" : (Math.random() < 0.03 ? "warn" : "ok");
    traces.push(makeTrace({ minutesAgo, agent, model, status, scenario }));
  }
  // newest first
  return traces.sort((a, b) => b.startedAt - a.startedAt);
}

// time bucketing for sparkline
function bucketByHour(traces, hours = 24) {
  const now = Date.now();
  const buckets = Array(hours).fill(0).map((_, i) => ({
    t: now - (hours - 1 - i) * 3_600_000,
    count: 0, errors: 0, totalLatency: 0,
  }));
  traces.forEach((tr) => {
    const idx = Math.floor((now - tr.startedAt) / 3_600_000);
    const b = buckets[hours - 1 - idx];
    if (!b) return;
    b.count += 1;
    b.totalLatency += tr.durationMs;
    if (tr.status === "error") b.errors += 1;
  });
  return buckets;
}

// ---------- API → UI shape adapters (PR 14b) ----------
//
// The backend /v1/traces returns a flat summary; the UI was originally built
// against the rich mock shape in makeTrace(). traceFromApiSummary maps the
// summary fields to that shape so existing components (TraceRow, SummaryStrip,
// Inspector …) don't need to change. Missing fields (spans, full tools) are
// filled in lazily when the user opens a trace via api.getTraceDetail().

const STATUS_MAP = {
  completed: "ok",
  failed:    "error",
  timeout:   "error",
  cancelled: "error",
  queued:    "warn",
  running:   "warn",
  unknown:   "warn",
};

function agentLookup(agents, agentId) {
  return (agents || []).find((a) => a.id === agentId) || null;
}

function traceFromApiSummary(s, agents) {
  const agent = agentLookup(agents, s.agent_id) || {};
  const usage = s.usage || {};
  const tokensIn  = usage.input_tokens  ?? usage.prompt_tokens ?? 0;
  const tokensOut = usage.output_tokens ?? usage.completion_tokens ?? 0;
  const toolCalls = usage.tool_calls ?? 0;
  const llmRounds = usage.llm_turns ?? usage.llm_rounds ?? 0;
  // The backend calculates a model-specific catalog reference estimate (or
  // preserves a provider-reported cost). Never reprice live runs in the UI.
  const costUSD = Number(usage.cost_usd ?? 0);
  // Runtime timestamps are milliseconds; accept legacy second values too.
  const rawStartedAt = s.started_at ?? s.queued_at;
  const startedAt = rawStartedAt
    ? (Number(rawStartedAt) < 100_000_000_000 ? Number(rawStartedAt) * 1000 : Number(rawStartedAt))
    : Date.now();
  return {
    id: s.run_id,
    runId: s.run_id,
    threadId: s.thread_id || "",
    agentId: s.agent_id,
    agentName: agent.name || (s.agent_id || "agent"),
    agentColor: agent.color || hashColorFallback(s.agent_id || ""),
    agentInitials: agent.initials || initialsFromName(agent.name || s.agent_id || "?"),
    model: agent.model || "—",
    startedAt,
    durationMs: s.duration_ms || 0,
    status: STATUS_MAP[s.status] || "warn",
    statusReason: s.error ? String(s.error) : null,
    tokensIn, tokensOut, costUSD,
    userId: s.user_id || null,
    toolCalls, llmRounds,
    userInput: s.preview || "(no preview)",
    finalOutput: null,           // filled in by detail load
    spans: [],                   // filled in by detail load
    _live: true,                 // marker: backed by real API, not mock
    _detailLoaded: false,
    _selectedSkills: s.selected_skills || [],
    _rawStatus: s.status,
  };
}

// Pair tool.started + tool.completed events into tool spans; collapse
// successive text.delta into a single llm span. The result is a thin
// waterfall — exact timings come from event timestamps, parent linkage
// keeps spans flat under the root.
//
// PR α (pr-trace-a): payload field reads now prefer OpenInference-style
// dotted names (`tool.name`, `tool.call_id`, `tool.args`) but fall back
// to the bare names (`tool_name`, `tool_call_id`, `arguments`) so a
// browser pointing at a pre-PR-α server (e.g. the still-stale PID 29322)
// still renders. When event.parent_event_id is present (future PR β/γ)
// we use it to build a real span tree instead of guessing from time
// windows.
function _pref(payload, ...keys) {
  for (const k of keys) {
    const v = payload?.[k];
    if (v !== undefined && v !== null) return v;
  }
  return undefined;
}
function eventsToSpans(events, runStartMs) {
  const spans = [];
  const rootId = rid("sp");
  spans.push({ id: rootId, kind: "run", name: "agent.run", parent: null, startMs: 0, durMs: 0 });
  const toolByCallId = {};
  let lastTextStart = null;
  let lastTextEnd = null;
  let firstTs = null;
  // Track event_id → span id so when a child event carries
  // parent_event_id we can wire span.parent to the right span (not just
  // null = root).
  const spanByEventId = {};
  for (const ev of events || []) {
    const ts = ev.timestamp ? Math.floor(ev.timestamp * 1000) : null;
    if (ts && firstTs === null) firstTs = ts;
    const baseStart = ts && firstTs ? (ts - firstTs) : 0;
    const parentSpanId = ev.parent_event_id ? (spanByEventId[ev.parent_event_id] || rootId) : null;
    if (ev.type === "tool.started") {
      const callId = _pref(ev.payload, "tool.call_id", "call_id", "tool_call_id") || rid("call");
      const toolName = _pref(ev.payload, "tool.name", "tool_name", "name") || "tool";
      const toolArgs = _pref(ev.payload, "tool.args", "args", "arguments") || ev.payload;
      toolByCallId[callId] = { startMs: baseStart, name: toolName, args: toolArgs };
    } else if (ev.type === "tool.completed") {
      const callId = _pref(ev.payload, "tool.call_id", "call_id", "tool_call_id");
      const prev = callId ? toolByCallId[callId] : null;
      const startMs = prev?.startMs ?? baseStart;
      const durMs = Math.max(1, baseStart - startMs);
      const spanId = rid("sp");
      spans.push({
        id: spanId, kind: "tool",
        name: prev?.name || _pref(ev.payload, "tool.name", "tool_name") || "tool",
        parent: parentSpanId, startMs, durMs,
        payload: { args: prev?.args, result: ev.payload?.result || ev.payload || {} },
        error: ev.payload?.error || ev.payload?.is_error ? true : false,
      });
      if (ev.event_id) spanByEventId[ev.event_id] = spanId;
    } else if (ev.type === "text.delta") {
      if (lastTextStart === null) lastTextStart = baseStart;
      lastTextEnd = baseStart;
    } else if (ev.type === "agent.skill_activated" || ev.type === "skill.activated") {
      spans.push({
        id: rid("sp"), kind: "llm",
        name: "skill · " + (_pref(ev.payload, "skill.name", "skill_name", "name") || "?"),
        parent: parentSpanId, startMs: baseStart, durMs: 50,
        payload: ev.payload || {}, error: false,
      });
    } else if (ev.type === "error" || ev.type === "run.failed") {
      spans.push({
        id: rid("sp"), kind: "err", name: "fatal_error",
        parent: null, startMs: baseStart, durMs: 20,
        payload: { message: ev.payload?.message || ev.payload?.error || "run failed" },
        error: true,
      });
    }
  }
  if (lastTextStart !== null) {
    spans.push({
      id: rid("sp"), kind: "llm", name: "LLM · respond",
      parent: null, startMs: lastTextStart, durMs: Math.max(50, (lastTextEnd ?? lastTextStart) - lastTextStart),
      payload: { streaming: true }, error: false,
    });
  }
  // Root span dur = last endpoint
  const lastEnd = spans.slice(1).reduce((m, s) => Math.max(m, s.startMs + s.durMs), 0);
  spans[0].durMs = lastEnd;
  return spans;
}

function hydrateTraceFromDetail(uiTrace, detail) {
  const events = detail.events || [];
  const summary = detail.summary || {};
  const startedAt = summary.started_at ? Math.floor(summary.started_at * 1000) : uiTrace.startedAt;
  const spans = eventsToSpans(events, startedAt);
  const modelIO = [...(detail.model_io || [])].sort((a, b) => {
    const roundDelta = (a.round_index || 0) - (b.round_index || 0);
    if (roundDelta) return roundDelta;
    return (a.started_at_ms || 0) - (b.started_at_ms || 0);
  });
  // assistant final text = concatenation of text.delta deltas
  const finalText = events.filter((e) => e.type === "text.delta")
    .map((e) => (e.payload || {}).delta || "")
    .join("");
  const persistedOutput = detail.run?.output_text || detail.summary?.output_text || "";
  return {
    ...uiTrace,
    spans,
    finalOutput: finalText || persistedOutput || uiTrace.finalOutput,
    durationMs: summary.duration_ms || uiTrace.durationMs,
    modelIO,
    _detailLoaded: true,
    _events: events,
    _detail: detail,
  };
}

// Tiny fallbacks for color/initials when api.hashColor / initialsFor aren't
// wired in (defensive — TracesApp normally passes the live api object's
// helpers via INITIAL_AGENTS, which already have agent.color / .initials).
function hashColorFallback(str) {
  let h = 0;
  for (let i = 0; i < str.length; i++) h = (h * 31 + str.charCodeAt(i)) | 0;
  const hue = Math.abs(h) % 360;
  return `hsl(${hue}, 65%, 55%)`;
}
function initialsFromName(name) {
  const parts = String(name || "").split(/\s+/).filter(Boolean);
  if (!parts.length) return "?";
  return (parts[0][0] + (parts[1]?.[0] || "")).toUpperCase();
}

Object.assign(window, {
  generateTraces,
  bucketByHour,
  SAMPLE_FULL_OUTPUT,
  traceFromApiSummary,
  hydrateTraceFromDetail,
  eventsToSpans,
});
