// SPEC: PJ2026-0104010803 Workbench唯一投影 draft-2026-06-18-p0-unique-projection; PJ2026-010401 Web工作台 draft-2026-06-18-r1.
// Responsibility: Trace snapshot helpers for Workbench realtime authority reducers.

import type { 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 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 previousCursor = traceProjectedSeqCursor(previous, previousEvents);
  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 events = nextAuthoritative
    ? previousEvents.length > 0 ? mergeTraceEvents(previousEvents, nextEvents, { previousCursor, nextRange: next.range }) : nextEvents
    : keepPreviousEvents && previousAuthoritative && next.eventsCompacted === true ? previousEvents
      : keepPreviousEvents ? mergeTraceEvents(previousEvents, nextEvents, { previousCursor, nextRange: next.range }) : nextEvents.length > previousEvents.length ? mergeTraceEvents(previousEvents, nextEvents, { previousCursor, nextRange: next.range }) : nextEvents;
  const eventCount = next.eventSource === "hwlab-kafka-sse"
    ? events.length
    : 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()
  };
}

function mergeTraceTimingProjection(previous: unknown, next: unknown): TraceSnapshot["timing"] | null {
  const previousTiming = traceTimingCandidate(previous);
  const nextTiming = traceTimingCandidate(next);
  if (!previousTiming && !nextTiming) return null;
  if (!previousTiming) return nextTiming;
  if (!nextTiming) return previousTiming;
  return {
    ...previousTiming,
    ...nextTiming,
    startedAt: nextTiming.startedAt ?? previousTiming.startedAt ?? null,
    lastEventAt: nextTiming.lastEventAt ?? previousTiming.lastEventAt ?? null,
    finishedAt: nextTiming.finishedAt ?? previousTiming.finishedAt ?? null,
    durationMs: mergeTraceDuration(previousTiming.durationMs, nextTiming.durationMs),
    observedAt: nextTiming.observedAt ?? previousTiming.observedAt ?? null,
    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 = firstTraceDuration(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 firstTraceDuration(...values: unknown[]): number | null {
  for (const value of values) {
    const duration = finiteTraceDuration(value);
    if (duration !== null) return duration;
  }
  return null;
}

function mergeTraceDuration(previous: unknown, next: unknown): number | null {
  const previousDuration = finiteTraceDuration(previous);
  const nextDuration = finiteTraceDuration(next);
  if (previousDuration === null) return nextDuration;
  if (nextDuration === null) return previousDuration;
  if (previousDuration === 0) return nextDuration;
  if (nextDuration === 0) return previousDuration;
  return Math.max(previousDuration, nextDuration);
}

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

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;
}

interface TraceEventMergeOptions {
  previousCursor?: number | null;
  nextRange?: TraceSnapshot["range"] | null;
}

function mergeTraceEvents(previousEvents: TraceEvent[], nextEvents: TraceEvent[], options: TraceEventMergeOptions = {}): TraceEvent[] {
  if (previousEvents.length === 0) return nextEvents;
  if (nextEvents.length === 0 || previousEvents === nextEvents) return previousEvents;
  const previousCursor = finiteTraceCursor(options.previousCursor) ?? traceEventsProjectedSeqCursor(previousEvents);
  const nextToProjectedSeq = traceRangeToProjectedSeq(options.nextRange);
  if (previousCursor !== null && nextToProjectedSeq !== null && nextToProjectedSeq <= previousCursor) return previousEvents;
  const appended = previousCursor !== null ? appendTraceEventDelta(previousEvents, nextEvents, previousCursor) : null;
  if (appended) return appended;
  const merged = [...previousEvents];
  const keys = new Set<string>();
  for (const event of previousEvents) {
    const key = traceEventIdentity(event);
    if (key) keys.add(key);
  }
  let changed = false;
  for (const event of nextEvents) {
    const key = traceEventIdentity(event);
    if (key && keys.has(key)) continue;
    if (key) keys.add(key);
    merged.push(event);
    changed = true;
  }
  if (!changed) return previousEvents;
  return traceEventsAreSorted(merged) ? merged : merged.sort((left, right) => traceEventSortSeq(left) - traceEventSortSeq(right));
}

function appendTraceEventDelta(previousEvents: TraceEvent[], nextEvents: TraceEvent[], previousCursor: number): TraceEvent[] | null {
  const delta: TraceEvent[] = [];
  for (const event of nextEvents) {
    const seq = traceEventProjectedSeq(event);
    if (!Number.isFinite(seq)) return null;
    if (seq > previousCursor) delta.push(event);
  }
  if (delta.length === 0) return previousEvents;
  if (!traceEventsAreSorted(delta)) return null;
  return [...previousEvents, ...delta];
}

function traceProjectedSeqCursor(trace: ChatMessage["runnerTrace"] | TraceSnapshot | null | undefined, events: TraceEvent[] = Array.isArray(trace?.events) ? trace.events : []): number | null {
  return finiteTraceCursor(trace?.nextProjectedSeq) ?? traceRangeToProjectedSeq(trace?.range) ?? traceEventsProjectedSeqCursor(events);
}

function traceRangeToProjectedSeq(range: TraceSnapshot["range"] | null | undefined): number | null {
  return finiteTraceCursor(range?.toProjectedSeq);
}

function traceEventsProjectedSeqCursor(events: TraceEvent[]): number | null {
  let cursor: number | null = null;
  for (const event of events) {
    const seq = traceEventProjectedSeq(event);
    if (Number.isFinite(seq) && (cursor === null || seq > cursor)) cursor = Math.trunc(seq);
  }
  return cursor;
}

function traceEventsAreSorted(events: TraceEvent[]): boolean {
  let previous = Number.NEGATIVE_INFINITY;
  for (const event of events) {
    const seq = traceEventSortSeq(event);
    if (seq < previous) return false;
    previous = seq;
  }
  return true;
}

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

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;
  const seq = typeof seqValue === "number" || typeof seqValue === "string" ? String(seqValue) : "";
  if (seq) return `projected:${seq}:${label || "event"}`;
  const sourceEventId = firstNonEmptyString(event.sourceEventId);
  if (sourceEventId) return `source-event:${sourceEventId}`;
  const runId = firstNonEmptyString(event.runId);
  const sourceSeq = typeof event.sourceSeq === "number" || typeof event.sourceSeq === "string" ? String(event.sourceSeq) : "";
  if (runId && sourceSeq) return `run-source:${runId}:${sourceSeq}`;
  const source = typeof event.source === "string" ? event.source : "";
  if (source && sourceSeq) return `source:${source}:${sourceSeq}:${label || "event"}`;
  return null;
}

function traceEventProjectedSeq(event: TraceEvent): number {
  const seq = Number(event.projectedSeq);
  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 ?? 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,
    lastEventAt: mergedTrace.lastEventAt ?? timing?.lastEventAt ?? terminal.lastEventAt,
    finishedAt: mergedTrace.finishedAt ?? timing?.finishedAt ?? terminal.finishedAt,
    durationMs: mergedTrace.durationMs ?? timing?.durationMs ?? terminal.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
  };
}
