// SPEC: PJ2026-0104010803 Workbench唯一投影 draft-2026-06-18-p0-unique-projection; PJ2026-010401 Web工作台 draft-2026-06-18-r1.
// Responsibility: Trace snapshot helpers and legacy subscription adapter backed by Workbench read-model APIs.

import { workbenchAPI, type ActivityRefSource } from "@/api";
import type { AgentChatResponse, AgentChatResultResponse, AgentRunProvenance, ChatMessage, ProjectionDiagnostic, TraceEvent, WorkbenchTurnTimingProjection } from "@/types";
import { firstNonEmptyString } from "@/utils";

export interface TraceSnapshot {
  traceId?: string;
  status?: string;
  sessionId?: string | null;
  threadId?: string | null;
  events?: TraceEvent[];
  eventCount?: number;
  eventsCompacted?: boolean;
  fullTraceLoaded?: boolean;
  hasMore?: boolean;
  truncated?: boolean;
  nextProjectedSeq?: number | null;
  range?: { afterProjectedSeq?: number; fromProjectedSeq?: number | null; toProjectedSeq?: number | null; limit?: number; returned?: number; total?: number; [key: string]: unknown };
  agentRun?: AgentRunProvenance;
  traceStatus?: string;
  retention?: unknown;
  terminalEvidence?: unknown;
  traceSummary?: unknown;
  error?: AgentChatResultResponse["error"];
  lastEventLabel?: string;
  waitingFor?: string;
  timing?: WorkbenchTurnTimingProjection | null;
  startedAt?: string | null;
  lastEventAt?: string | null;
  finishedAt?: string | null;
  durationMs?: number | null;
  projection?: ProjectionDiagnostic | null;
  projectionStatus?: string | null;
  projectionHealth?: string | null;
  staleMs?: number | null;
  blocker?: ProjectionDiagnostic["blocker"] | null;
  eventSource?: string;
  updatedAt?: string;
}

export interface TraceSubscriptionConfig {
  traceId: string;
  initial: AgentChatResponse;
  onActivity: () => void;
  onSnapshot: (snapshot: TraceSnapshot) => void;
  onComplete: (result: AgentChatResultResponse) => void;
  onInfrastructureError: (error: string) => void;
  signal: AbortSignal;
  inactivityTimeoutMs: number;
  activityRef: ActivityRefSource;
}

const TRACE_POLL_INTERVAL_MS = 1500;
const TRACE_LIVE_PAGE_LIMIT = 80;
const TRACE_LIVE_MAX_PAGES_PER_POLL = 4;

export function isTerminalStatus(status: string | undefined): boolean {
  if (!status) return false;
  return ["completed", "failed", "blocked", "timeout", "cancelled", "canceled"].includes(String(status));
}

export function isResultUrlStatus(response: AgentChatResponse): boolean {
  return Boolean(response.resultUrl) && (response.status === "running" || response.status === "accepted" || isTerminalStatus(response.status));
}

export function snapshotToRunnerTrace(snapshot: TraceSnapshot): NonNullable<ChatMessage["runnerTrace"]> {
  const events = Array.isArray(snapshot.events) ? snapshot.events : [];
  return {
    traceId: snapshot.traceId,
    status: snapshot.status,
    sessionId: snapshot.sessionId ?? undefined,
    threadId: snapshot.threadId ?? undefined,
    events,
    eventCount: snapshot.eventCount ?? events.length,
    eventsCompacted: snapshot.eventsCompacted,
    fullTraceLoaded: snapshot.fullTraceLoaded,
    hasMore: snapshot.hasMore,
    truncated: snapshot.truncated,
    nextProjectedSeq: snapshot.nextProjectedSeq,
    range: snapshot.range,
    traceStatus: snapshot.traceStatus,
    retention: snapshot.retention,
    terminalEvidence: snapshot.terminalEvidence,
    traceSummary: snapshot.traceSummary,
    agentRun: snapshot.agentRun,
    error: snapshot.error,
    eventSource: snapshot.eventSource ?? "trace-api",
    runnerKind: snapshot.agentRun?.adapter,
    sessionMode: snapshot.agentRun?.backendProfile,
    lastEventLabel: snapshot.lastEventLabel ?? undefined,
    waitingFor: snapshot.waitingFor,
    timing: snapshot.timing ?? null,
    startedAt: snapshot.startedAt ?? snapshot.timing?.startedAt ?? null,
    lastEventAt: snapshot.lastEventAt ?? snapshot.timing?.lastEventAt ?? null,
    finishedAt: snapshot.finishedAt ?? snapshot.timing?.finishedAt ?? null,
    durationMs: snapshot.durationMs ?? snapshot.timing?.durationMs ?? null,
    projection: snapshot.projection ?? null,
    projectionStatus: snapshot.projectionStatus ?? snapshot.projection?.projectionStatus ?? null,
    projectionHealth: snapshot.projectionHealth ?? snapshot.projection?.projectionHealth ?? null,
    staleMs: snapshot.staleMs ?? snapshot.projection?.staleMs ?? null,
    blocker: snapshot.blocker ?? snapshot.projection?.blocker ?? null,
    updatedAt: snapshot.updatedAt ?? new Date().toISOString()
  };
}

export function mergeRunnerTrace(previous: ChatMessage["runnerTrace"], next: NonNullable<ChatMessage["runnerTrace"]>): NonNullable<ChatMessage["runnerTrace"]> {
  if (!previous) return next;
  const previousEvents = Array.isArray(previous.events) ? previous.events : [];
  const nextEvents = Array.isArray(next.events) ? next.events : [];
  const nextAuthoritative = next.eventSource === "trace-api";
  const previousAuthoritative = previous.eventSource === "trace-api";
  const keepPreviousEvents = previousEvents.length > 0 && !nextAuthoritative && (previousAuthoritative || next.eventsCompacted === true || nextEvents.length <= previousEvents.length);
  const mergeAuthoritativeEvents = nextAuthoritative && previousEvents.length > 0 && (next.hasMore === true || next.fullTraceLoaded !== true || previous.fullTraceLoaded !== true || nextEvents.length < previousEvents.length);
  const events = nextAuthoritative
    ? mergeAuthoritativeEvents ? mergeTraceEvents(previousEvents, nextEvents) : nextEvents
    : keepPreviousEvents && previousAuthoritative && next.eventsCompacted === true ? previousEvents
      : keepPreviousEvents ? mergeTraceEvents(previousEvents, nextEvents) : nextEvents.length > previousEvents.length ? mergeTraceEvents(previousEvents, nextEvents) : nextEvents;
  const eventCount = keepPreviousEvents ? previous.eventCount ?? events.length : next.eventCount ?? previous.eventCount ?? events.length;
  const timing = mergeTraceTimingProjection(previous, next);
  return {
    ...previous,
    ...next,
    events,
    eventCount,
    timing,
    startedAt: timing?.startedAt ?? next.startedAt ?? previous.startedAt ?? null,
    lastEventAt: timing?.lastEventAt ?? next.lastEventAt ?? previous.lastEventAt ?? null,
    finishedAt: timing?.finishedAt ?? next.finishedAt ?? previous.finishedAt ?? null,
    durationMs: timing?.durationMs ?? next.durationMs ?? previous.durationMs ?? null,
    updatedAt: next.updatedAt ?? previous.updatedAt ?? new Date().toISOString()
  };
}

type TraceTimingMergeOptions = {
  preserveVisibleTerminalDuration?: boolean;
};

function mergeTraceTimingProjection(previous: unknown, next: unknown, options: TraceTimingMergeOptions = {}): TraceSnapshot["timing"] | null {
  const previousTiming = traceTimingCandidate(previous);
  const nextTiming = traceTimingCandidate(next);
  if (!previousTiming && !nextTiming) return null;
  if (!previousTiming) return nextTiming;
  if (!nextTiming) return previousTiming;
  const startedAt = earliestTraceTimestamp(previousTiming.startedAt, nextTiming.startedAt);
  const lastEventAt = latestTraceTimestamp(previousTiming.lastEventAt, nextTiming.lastEventAt);
  const finishedAt = latestTraceTimestamp(previousTiming.finishedAt, nextTiming.finishedAt);
  const runningElapsedDurationMs = traceElapsedMs(startedAt, lastEventAt);
  const terminalElapsedDurationMs = traceElapsedMs(startedAt, finishedAt);
  const terminalTiming = traceTimingIsTerminal(next) || finishedAt !== null;
  const previousTerminalDurationMs = previousTiming.finishedAt ? previousTiming.durationMs : null;
  const preservedVisibleTerminalDurationMs = options.preserveVisibleTerminalDuration && !finishedAt
    ? firstTraceDuration(previousTiming.durationMs)
    : null;
  const durationMs = terminalTiming
    ? maxTraceDuration(finishedAt ? nextTiming.durationMs : null, terminalElapsedDurationMs, previousTerminalDurationMs, preservedVisibleTerminalDurationMs)
    : maxTraceDuration(previousTiming.durationMs, nextTiming.durationMs, runningElapsedDurationMs);
  return {
    ...previousTiming,
    ...nextTiming,
    startedAt,
    lastEventAt,
    finishedAt,
    durationMs,
    observedAt: latestTraceTimestamp(previousTiming.observedAt, nextTiming.observedAt),
    lastEventAgeMs: null,
    valuesRedacted: previousTiming.valuesRedacted !== false && nextTiming.valuesRedacted !== false
  };
}

function traceTimingCandidate(value: unknown): TraceSnapshot["timing"] | null {
  const record = traceRecord(value);
  const timing = traceRecord(record?.timing);
  if (!record && !timing) return null;
  const startedAt = firstTraceTimestamp(timing?.startedAt, record?.startedAt);
  const lastEventAt = firstTraceTimestamp(timing?.lastEventAt, record?.lastEventAt);
  const finishedAt = firstTraceTimestamp(timing?.finishedAt, record?.finishedAt);
  const observedAt = firstTraceTimestamp(timing?.observedAt, record?.observedAt);
  const durationMs = maxTraceDuration(timing?.durationMs, record?.durationMs);
  const lastEventAgeMs = finiteTraceDuration(timing?.lastEventAgeMs ?? record?.lastEventAgeMs);
  if (!startedAt && !lastEventAt && !finishedAt && !observedAt && durationMs === null && lastEventAgeMs === null) return null;
  return { ...(timing ?? {}), startedAt, lastEventAt, finishedAt, observedAt, durationMs, lastEventAgeMs, valuesRedacted: timing?.valuesRedacted !== false };
}

function traceRecord(value: unknown): Record<string, unknown> | null {
  return value && typeof value === "object" ? value as Record<string, unknown> : null;
}

function firstTraceTimestamp(...values: unknown[]): string | null {
  for (const value of values) {
    if (typeof value !== "string") continue;
    const text = value.trim();
    if (!text) continue;
    if (Number.isFinite(Date.parse(text))) return text;
  }
  return null;
}

function earliestTraceTimestamp(...values: unknown[]): string | null {
  return selectTraceTimestamp("earliest", values);
}

function latestTraceTimestamp(...values: unknown[]): string | null {
  return selectTraceTimestamp("latest", values);
}

function selectTraceTimestamp(mode: "earliest" | "latest", values: unknown[]): string | null {
  let selected: { value: string; ms: number } | null = null;
  for (const value of values) {
    const timestamp = firstTraceTimestamp(value);
    if (!timestamp) continue;
    const ms = Date.parse(timestamp);
    if (!selected || (mode === "earliest" ? ms < selected.ms : ms > selected.ms)) selected = { value: timestamp, ms };
  }
  return selected?.value ?? null;
}

function traceElapsedMs(startedAt: unknown, endedAt: unknown): number | null {
  const start = startedAt == null ? NaN : Date.parse(String(startedAt));
  const end = endedAt == null ? NaN : Date.parse(String(endedAt));
  if (!Number.isFinite(start) || !Number.isFinite(end) || end < start) return null;
  return Math.trunc(end - start);
}

function maxTraceDuration(...values: unknown[]): number | null {
  let max: number | null = null;
  for (const value of values) {
    const duration = finiteTraceDuration(value);
    if (duration === null) continue;
    if (max === null || duration > max) max = duration;
  }
  return max;
}

function firstTraceDuration(...values: unknown[]): number | null {
  for (const value of values) {
    const duration = finiteTraceDuration(value);
    if (duration !== null) return duration;
  }
  return null;
}

function finiteTraceDuration(value: unknown): number | null {
  const number = Number(value);
  return Number.isFinite(number) && number >= 0 ? Math.trunc(number) : null;
}

function traceTimingWasRunning(value: unknown): boolean {
  const record = traceRecord(value);
  if (!record) return false;
  const status = typeof record.status === "string" ? record.status.trim().toLowerCase() : "";
  if (["completed", "failed", "cancelled", "canceled", "error"].includes(status)) return false;
  const timing = traceTimingCandidate(value);
  return !timing?.finishedAt;
}

function traceTimingIsTerminal(value: unknown): boolean {
  const record = traceRecord(value);
  const status = typeof record?.status === "string" ? record.status.trim().toLowerCase() : "";
  return ["completed", "failed", "cancelled", "canceled", "error"].includes(status) || traceTimingCandidate(value)?.finishedAt != null;
}

function mergeTraceEvents(previousEvents: TraceEvent[], nextEvents: TraceEvent[]): TraceEvent[] {
  const merged: TraceEvent[] = [];
  const keys = new Set<string>();
  for (const event of [...previousEvents, ...nextEvents]) {
    const key = traceEventIdentity(event);
    if (key && keys.has(key)) continue;
    if (key) keys.add(key);
    merged.push(event);
  }
  return merged.sort((left, right) => traceEventSortSeq(left) - traceEventSortSeq(right));
}

function traceEventSortSeq(event: TraceEvent): number {
  const seq = traceEventProjectedSeq(event);
  return Number.isFinite(seq) ? seq : Number.MAX_SAFE_INTEGER;
}

function traceEventIdentity(event: TraceEvent): string | null {
  const label = typeof event.label === "string" ? event.label : typeof event.type === "string" ? event.type : "";
  const seqValue = event.projectedSeq ?? event.seq;
  const seq = typeof seqValue === "number" || typeof seqValue === "string" ? String(seqValue) : "";
  if (seq) return `projected:${seq}:${label || "event"}`;
  return null;
}

function traceEventProjectedSeq(event: TraceEvent): number {
  const seq = Number(event.projectedSeq ?? event.seq);
  return Number.isFinite(seq) ? Math.trunc(seq) : Number.NaN;
}

export function mergeTraceResults(terminal: AgentChatResultResponse, trace: TraceSnapshot | null): AgentChatResultResponse {
  if (!trace) return terminal;
  const mergedTrace: TraceSnapshot = {
    ...trace,
    sessionId: trace.sessionId ?? terminal.sessionId,
    threadId: trace.threadId ?? terminal.threadId,
    agentRun: trace.agentRun ?? terminal.agentRun,
    terminalEvidence: trace.terminalEvidence ?? terminal.terminalEvidence,
    traceSummary: trace.traceSummary ?? terminal.traceSummary,
    error: trace.error ?? terminal.error,
    projection: trace.projection ?? terminal.projection ?? null,
    projectionStatus: trace.projectionStatus ?? terminal.projectionStatus ?? null,
    projectionHealth: trace.projectionHealth ?? terminal.projectionHealth ?? null,
    staleMs: trace.staleMs ?? terminal.staleMs ?? null,
    blocker: trace.blocker ?? terminal.blocker ?? null
  };
  const events = Array.isArray(trace.events) ? trace.events : [];
  const timing = mergedTrace.timing ?? terminal.timing ?? terminal.runnerTrace?.timing ?? null;
  return {
    ...terminal,
    traceId: mergedTrace.traceId ?? terminal.traceId,
    sessionId: mergedTrace.sessionId ?? terminal.sessionId,
    threadId: mergedTrace.threadId ?? terminal.threadId,
    timing,
    startedAt: mergedTrace.startedAt ?? timing?.startedAt ?? terminal.startedAt ?? terminal.runnerTrace?.startedAt,
    lastEventAt: mergedTrace.lastEventAt ?? timing?.lastEventAt ?? terminal.lastEventAt ?? terminal.runnerTrace?.lastEventAt,
    finishedAt: mergedTrace.finishedAt ?? timing?.finishedAt ?? terminal.finishedAt ?? terminal.runnerTrace?.finishedAt,
    durationMs: mergedTrace.durationMs ?? timing?.durationMs ?? terminal.durationMs ?? terminal.runnerTrace?.durationMs,
    agentRun: mergedTrace.agentRun ?? terminal.agentRun,
    error: mergedTrace.error ?? terminal.error,
    assistantText: terminal.assistantText,
    runnerTrace: snapshotToRunnerTrace(mergedTrace),
    traceEvents: terminal.traceEvents ?? events,
    events,
    eventCount: mergedTrace.eventCount ?? events.length,
    traceStatus: mergedTrace.traceStatus ?? terminal.traceStatus,
    retention: mergedTrace.retention ?? terminal.retention,
    terminalEvidence: mergedTrace.terminalEvidence ?? terminal.terminalEvidence,
    finalResponse: terminal.finalResponse,
    traceSummary: mergedTrace.traceSummary ?? terminal.traceSummary,
    lastEventLabel: mergedTrace.lastEventLabel ?? terminal.lastEventLabel ?? undefined
  };
}

export async function subscribeToTrace(config: TraceSubscriptionConfig): Promise<void> {
  const { traceId, initial, onActivity, onSnapshot, onComplete, onInfrastructureError, signal, inactivityTimeoutMs, activityRef } = config;
  if (isTerminalStatus(initial.status)) {
    const snapshot = resultToTraceSnapshot(traceId, initial as AgentChatResultResponse, "turn-api");
    onSnapshot(snapshot);
    onComplete(mergeTraceResults(initial as AgentChatResultResponse, snapshot));
    return;
  }
  if (!initial.turnUrl && !traceId) {
    onInfrastructureError("Code Agent initial response is missing traceId for turn status polling");
    return;
  }

  let lastSnapshotKey = "";
  let traceAfterProjectedSeq = 0;
  let accumulatedTrace: TraceSnapshot | null = null;

  const fetchLiveTracePages = async (): Promise<TraceSnapshot | null> => {
    for (let page = 0; page < TRACE_LIVE_MAX_PAGES_PER_POLL; page += 1) {
      const previousProjectedSeq = traceAfterProjectedSeq;
      const tracePolled = await workbenchAPI.traceEvents(traceId, inactivityTimeoutMs, activityRef, { afterProjectedSeq: traceAfterProjectedSeq, limit: TRACE_LIVE_PAGE_LIMIT });
      if (signal.aborted) return accumulatedTrace;
      if (!tracePolled.ok || !tracePolled.data) return accumulatedTrace;
      onActivity();
      const pageSnapshot = resultToTraceSnapshot(traceId, tracePolled.data, "trace-api");
      accumulatedTrace = mergeTraceSnapshots(accumulatedTrace, pageSnapshot);
      const nextProjectedSeq = traceSnapshotNextProjectedSeq(pageSnapshot, traceAfterProjectedSeq);
      if (nextProjectedSeq > traceAfterProjectedSeq) traceAfterProjectedSeq = nextProjectedSeq;
      if (pageSnapshot.hasMore !== true || nextProjectedSeq <= previousProjectedSeq) break;
    }
    return accumulatedTrace;
  };

  for (;;) {
    if (signal.aborted) return;
    await sleep(TRACE_POLL_INTERVAL_MS);
    if (signal.aborted) return;

    const turnPolled = await workbenchAPI.turn(traceId, inactivityTimeoutMs, activityRef);
    if (signal.aborted) return;

    if (turnPolled.ok && turnPolled.data) {
      onActivity();
      const turnSnapshot = resultToTraceSnapshot(traceId, turnPolled.data, "turn-api");
      const liveTrace = await fetchLiveTracePages();
      if (signal.aborted) return;
      const snapshot = traceSnapshotWithTurnStatus(liveTrace ?? turnSnapshot, turnSnapshot);
      const snapshotKey = traceSnapshotSignalKey(snapshot);
      if (snapshotKey !== lastSnapshotKey) {
        lastSnapshotKey = snapshotKey;
        onSnapshot(snapshot);
      }
      if (turnPolled.data.terminal === true || isTerminalStatus(turnPolled.data.status)) {
        onComplete(mergeTraceResults(turnPolled.data, accumulatedTrace ?? snapshot));
        return;
      }
    } else if (!turnPolled.ok && turnPolled.status >= 500) {
      // Backend hiccup: keep polling and let inactivity-timeout classify a real outage.
    } else {
      onInfrastructureError(turnPolled.error ?? "Code Agent turn status poll failed (non-5xx)");
      return;
    }
  }
}

function resultToTraceSnapshot(traceId: string, result: AgentChatResultResponse, eventSource = "trace-api"): TraceSnapshot {
  const events = Array.isArray(result.events) ? result.events : Array.isArray(result.traceEvents) ? result.traceEvents : [];
  const lastEvent = events.at(-1);
  const timing = result.timing ?? result.runnerTrace?.timing ?? null;
  return {
    traceId: result.traceId ?? traceId,
    status: result.status,
    sessionId: result.sessionId ?? null,
    threadId: result.threadId ?? null,
    events,
    eventCount: result.eventCount ?? events.length,
    eventsCompacted: result.runnerTrace?.eventsCompacted,
    fullTraceLoaded: result.fullTraceLoaded,
    hasMore: result.hasMore,
    truncated: result.truncated,
    nextProjectedSeq: typeof result.nextProjectedSeq === "number" ? result.nextProjectedSeq : null,
    range: result.range as TraceSnapshot["range"],
    agentRun: result.agentRun,
    traceStatus: result.traceStatus,
    retention: result.retention,
    terminalEvidence: result.terminalEvidence,
    traceSummary: result.traceSummary,
    error: result.error,
    timing,
    startedAt: result.startedAt ?? timing?.startedAt ?? result.runnerTrace?.startedAt ?? null,
    lastEventAt: result.lastEventAt ?? timing?.lastEventAt ?? result.runnerTrace?.lastEventAt ?? null,
    finishedAt: result.finishedAt ?? timing?.finishedAt ?? result.runnerTrace?.finishedAt ?? null,
    durationMs: result.durationMs ?? timing?.durationMs ?? result.runnerTrace?.durationMs ?? null,
    projection: result.projection ?? result.runnerTrace?.projection ?? null,
    projectionStatus: result.projectionStatus ?? result.runnerTrace?.projectionStatus ?? null,
    projectionHealth: result.projectionHealth ?? result.runnerTrace?.projectionHealth ?? null,
    staleMs: result.staleMs ?? result.runnerTrace?.staleMs ?? null,
    blocker: result.blocker ?? result.runnerTrace?.blocker ?? null,
    lastEventLabel: result.lastEventLabel ?? lastEvent?.label ?? lastEvent?.type,
    eventSource,
    updatedAt: new Date().toISOString()
  };
}

function mergeTraceSnapshots(previous: TraceSnapshot | null, next: TraceSnapshot): TraceSnapshot {
  if (!previous) return next;
  const previousEvents = Array.isArray(previous.events) ? previous.events : [];
  const nextEvents = Array.isArray(next.events) ? next.events : [];
  const events = mergeTraceEvents(previousEvents, nextEvents);
  const timing = mergeTraceTimingProjection(previous, next, { preserveVisibleTerminalDuration: traceTimingWasRunning(previous) && traceTimingIsTerminal(next) });
  return {
    ...previous,
    ...next,
    events,
    eventCount: next.eventCount ?? previous.eventCount ?? events.length,
    timing,
    startedAt: timing?.startedAt ?? next.startedAt ?? next.timing?.startedAt ?? previous.startedAt ?? previous.timing?.startedAt ?? null,
    lastEventAt: timing?.lastEventAt ?? next.lastEventAt ?? next.timing?.lastEventAt ?? previous.lastEventAt ?? previous.timing?.lastEventAt ?? null,
    finishedAt: timing?.finishedAt ?? next.finishedAt ?? next.timing?.finishedAt ?? previous.finishedAt ?? previous.timing?.finishedAt ?? null,
    durationMs: timing?.durationMs ?? next.durationMs ?? next.timing?.durationMs ?? previous.durationMs ?? previous.timing?.durationMs ?? null,
    lastEventLabel: next.lastEventLabel ?? previous.lastEventLabel ?? undefined,
    updatedAt: next.updatedAt ?? previous.updatedAt ?? new Date().toISOString()
  };
}

function traceSnapshotWithTurnStatus(trace: TraceSnapshot, turn: TraceSnapshot): TraceSnapshot {
  const turnTiming = traceTimingCandidate(turn);
  const turnIsTerminal = traceTimingIsTerminal(turn) || isTerminalStatus(turn.status);
  // Running card timing belongs to the turn status projection. Trace pages may
  // hydrate older detail rows later; they must not move the visible running
  // card's startedAt/lastEventAt source and create multi-second jumps.
  const timing = !turnIsTerminal && turnTiming
    ? turnTiming
    : mergeTraceTimingProjection(trace, turn, { preserveVisibleTerminalDuration: traceTimingWasRunning(trace) && traceTimingIsTerminal(turn) });
  return {
    ...trace,
    traceId: trace.traceId ?? turn.traceId,
    status: turn.status ?? trace.status,
    sessionId: trace.sessionId ?? turn.sessionId,
    threadId: trace.threadId ?? turn.threadId,
    agentRun: trace.agentRun ?? turn.agentRun,
    traceStatus: trace.traceStatus ?? turn.traceStatus,
    terminalEvidence: trace.terminalEvidence ?? turn.terminalEvidence,
    traceSummary: trace.traceSummary ?? turn.traceSummary,
    error: trace.error ?? turn.error,
    timing,
    startedAt: timing?.startedAt ?? turn.startedAt ?? turn.timing?.startedAt ?? trace.startedAt ?? trace.timing?.startedAt ?? null,
    lastEventAt: timing?.lastEventAt ?? turn.lastEventAt ?? turn.timing?.lastEventAt ?? trace.lastEventAt ?? trace.timing?.lastEventAt ?? null,
    finishedAt: timing?.finishedAt ?? turn.finishedAt ?? turn.timing?.finishedAt ?? trace.finishedAt ?? trace.timing?.finishedAt ?? null,
    durationMs: timing?.durationMs ?? turn.durationMs ?? turn.timing?.durationMs ?? trace.durationMs ?? trace.timing?.durationMs ?? null,
    projection: trace.projection ?? turn.projection ?? null,
    projectionStatus: trace.projectionStatus ?? turn.projectionStatus ?? null,
    projectionHealth: trace.projectionHealth ?? turn.projectionHealth ?? null,
    staleMs: trace.staleMs ?? turn.staleMs ?? null,
    blocker: trace.blocker ?? turn.blocker ?? null,
    waitingFor: trace.waitingFor ?? turn.waitingFor,
    updatedAt: new Date().toISOString()
  };
}

function traceSnapshotNextProjectedSeq(snapshot: TraceSnapshot, fallback: number): number {
  const direct = Number(snapshot.nextProjectedSeq ?? snapshot.range?.toProjectedSeq);
  if (Number.isFinite(direct) && direct >= 0) return Math.trunc(direct);
  const events = Array.isArray(snapshot.events) ? snapshot.events : [];
  return events.reduce((max, event) => {
    const seq = traceEventProjectedSeq(event);
    return Number.isFinite(seq) && seq > max ? Math.trunc(seq) : max;
  }, fallback);
}

function traceSnapshotSignalKey(snapshot: TraceSnapshot): string {
  const events = Array.isArray(snapshot.events) ? snapshot.events : [];
  return [
    snapshot.status ?? "",
    snapshot.traceStatus ?? "",
    snapshot.eventCount ?? events.length,
    events.length,
    snapshot.nextProjectedSeq ?? snapshot.range?.toProjectedSeq ?? "",
    snapshot.lastEventAt ?? snapshot.timing?.lastEventAt ?? "",
    snapshot.durationMs ?? snapshot.timing?.durationMs ?? "",
    snapshot.hasMore === true ? "more" : "caught-up",
    snapshot.fullTraceLoaded === true ? "loaded" : "partial",
    snapshot.lastEventLabel ?? "",
    traceSnapshotErrorKey(snapshot.error),
    snapshot.projectionHealth ?? snapshot.projection?.projectionHealth ?? "",
    snapshot.projection?.blocker?.code ?? snapshot.blocker?.code ?? ""
  ].join("|");
}

function traceSnapshotErrorKey(error: TraceSnapshot["error"]): string {
  if (!error) return "";
  if (typeof error === "string") return error;
  return firstNonEmptyString(error.code, error.message) ?? "";
}

function sleep(ms: number): Promise<void> {
  return new Promise((resolve) => window.setTimeout(resolve, ms));
}
