// SPEC: PJ2026-010401080313 Workbench实时权威 draft-2026-07-09-p1-single-step-debug.
// Responsibility: isolated fake-SSE reducer harness for Workbench debug pages.

import type { WorkbenchRealtimeEvent } from "@/api/workbench-events";
import { mergeRunnerTrace, snapshotToRunnerTrace, type TraceSnapshot } from "@/composables/workbench-trace-snapshot";
import type { ChatMessage, TraceEvent } from "@/types";
import { firstNonEmptyString } from "@/utils";
import { reduceWorkbenchRealtimeEvent } from "./workbench-event-reducer";
import { planWorkbenchRealtimeApply, type WorkbenchRealtimeApplyStep } from "./workbench-realtime-plan";

export interface WorkbenchDebugFakeSseLogEntry {
  id: string;
  eventName: string;
  eventType: string;
  actionType: string;
  stepTypes: string[];
  traceId: string | null;
  accepted: boolean;
  reason: string | null;
  observedAt: string;
}

export interface WorkbenchDebugFakeSseState {
  message: ChatMessage | null;
  logs: WorkbenchDebugFakeSseLogEntry[];
  error: string | null;
}

export function createWorkbenchDebugFakeSseState(): WorkbenchDebugFakeSseState {
  return { message: null, logs: [], error: null };
}

export function applyWorkbenchDebugFakeSseEvent(state: WorkbenchDebugFakeSseState, event: WorkbenchRealtimeEvent, eventName: string): WorkbenchDebugFakeSseState {
  const reduced = reduceWorkbenchRealtimeEvent(event, eventName);
  const plan = planWorkbenchRealtimeApply(reduced.action);
  let next: WorkbenchDebugFakeSseState = {
    ...state,
    logs: [debugLogEntry(event, eventName, reduced.action.type, plan.steps, reduced.action.type === "ignore" ? reduced.action.reason : null), ...state.logs].slice(0, 60)
  };
  for (const step of plan.steps) next = applyStep(next, step);
  if (reduced.action.type === "ignore") next = { ...next, error: reduced.action.reason };
  return next;
}

function applyStep(state: WorkbenchDebugFakeSseState, step: WorkbenchRealtimeApplyStep): WorkbenchDebugFakeSseState {
  switch (step.type) {
    case "apply-message-snapshot":
      return step.realtimeEvent.message ? { ...state, message: normalizeDebugMessage(step.realtimeEvent.message), error: null } : state;
    case "apply-trace-event":
      return applyTraceEvent(state, step.traceId, step.event, step.snapshot, step.realtimeEvent);
    case "apply-trace-snapshot":
      return applyTraceSnapshot(state, step.traceId, step.snapshot);
    case "apply-turn-snapshot":
      return applyTurnSnapshot(state, step.turn);
    case "apply-projection-error":
      return { ...state, error: firstNonEmptyString(step.realtimeEvent.error?.message, step.realtimeEvent.reason, "projection error") ?? "projection error" };
    case "clear-active-trace":
      return state.message?.runnerTrace?.traceId === step.traceId ? { ...state, message: { ...state.message, runnerTrace: null } } : state;
  }
}

function applyTraceEvent(state: WorkbenchDebugFakeSseState, traceId: string | null, event: WorkbenchRealtimeEvent["event"], snapshot: WorkbenchRealtimeEvent["snapshot"], realtimeEvent: WorkbenchRealtimeEvent): WorkbenchDebugFakeSseState {
  const id = firstNonEmptyString(traceId, event?.traceId, snapshot?.traceId, realtimeEvent.traceId);
  if (!id) return state;
  const message = state.message ?? debugMessageForTrace(id, realtimeEvent);
  const trace = traceFromRealtime(id, snapshot, event ? [event] : [], message.runnerTrace ?? null);
  return {
    ...state,
    error: null,
    message: {
      ...message,
      status: trace.status === "completed" ? "completed" : message.status,
      runnerTrace: mergeRunnerTrace(message.runnerTrace, trace),
      updatedAt: new Date().toISOString()
    }
  };
}

function applyTraceSnapshot(state: WorkbenchDebugFakeSseState, traceId: string | null, snapshot: WorkbenchRealtimeEvent["snapshot"]): WorkbenchDebugFakeSseState {
  const id = firstNonEmptyString(traceId, snapshot?.traceId);
  if (!id || !snapshot) return state;
  const message = state.message ?? debugMessageForTrace(id, {
    sessionId: firstNonEmptyString(snapshot.sessionId),
    threadId: firstNonEmptyString(snapshot.threadId),
    traceId: id
  });
  const trace = traceFromRealtime(id, snapshot, Array.isArray(snapshot.events) ? snapshot.events : [], message.runnerTrace ?? null);
  return {
    ...state,
    error: null,
    message: {
      ...message,
      status: trace.status === "completed" ? "completed" : message.status,
      runnerTrace: mergeRunnerTrace(message.runnerTrace, trace),
      updatedAt: new Date().toISOString()
    }
  };
}

function applyTurnSnapshot(state: WorkbenchDebugFakeSseState, turn: NonNullable<WorkbenchRealtimeEvent["turn"]>): WorkbenchDebugFakeSseState {
  const traceId = firstNonEmptyString(turn.traceId);
  if (!traceId || !state.message || firstNonEmptyString(state.message.traceId, state.message.runnerTrace?.traceId) !== traceId) return state;
  const status = normalizeStatus(turn.status);
  return status ? { ...state, message: { ...state.message, status, updatedAt: new Date().toISOString() } } : state;
}

function traceFromRealtime(traceId: string, snapshot: WorkbenchRealtimeEvent["snapshot"], events: TraceEvent[], existing: ChatMessage["runnerTrace"]): NonNullable<ChatMessage["runnerTrace"]> {
  const snapshotEvents = Array.isArray(snapshot?.events) ? snapshot.events : null;
  const previousEvents = Array.isArray(existing?.events) ? existing.events : [];
  const nextEvents = snapshotEvents ?? mergeDebugTraceEvents(previousEvents, events);
  const source = (snapshot ?? {}) as TraceSnapshot;
  return snapshotToRunnerTrace({
    ...source,
    traceId,
    status: firstNonEmptyString(source.status, events.at(-1)?.status, existing?.status) ?? "running",
    sessionId: source.sessionId ?? existing?.sessionId ?? null,
    threadId: source.threadId ?? existing?.threadId ?? null,
    events: nextEvents,
    eventCount: finiteNumber(source.eventCount) ?? nextEvents.length,
    fullTraceLoaded: source.fullTraceLoaded === true,
    hasMore: source.hasMore === true,
    eventSource: "debug-fake-sse"
  } as TraceSnapshot);
}

function mergeDebugTraceEvents(previous: TraceEvent[], incoming: TraceEvent[]): TraceEvent[] {
  if (incoming.length === 0) return previous;
  const keys = new Set(previous.map(traceEventKey).filter((item): item is string => Boolean(item)));
  const merged = [...previous];
  for (const event of incoming) {
    const key = traceEventKey(event);
    if (key && keys.has(key)) continue;
    if (key) keys.add(key);
    merged.push(event);
  }
  return merged.sort((left, right) => (finiteNumber(left.projectedSeq) ?? 0) - (finiteNumber(right.projectedSeq) ?? 0));
}

function traceEventKey(event: TraceEvent): string | null {
  return firstNonEmptyString(event.sourceEventId, event.id, event.itemId, event.projectedSeq != null ? `${event.traceId ?? "trace"}:${event.projectedSeq}` : null);
}

function debugMessageForTrace(traceId: string, event: Pick<WorkbenchRealtimeEvent, "sessionId" | "threadId" | "traceId">): ChatMessage {
  const now = new Date().toISOString();
  return {
    id: `msg_debug_${traceId}`,
    messageId: `msg_debug_${traceId}`,
    role: "agent",
    title: "Code Agent",
    text: "",
    status: "running",
    createdAt: now,
    updatedAt: now,
    traceId,
    turnId: traceId,
    sessionId: firstNonEmptyString(event.sessionId) ?? "ses_debug_fake_sse",
    threadId: firstNonEmptyString(event.threadId) ?? "thr_debug_fake_sse",
    runnerTrace: null,
    timing: { startedAt: now, lastEventAt: now, finishedAt: null, durationMs: null, valuesRedacted: true }
  };
}

function normalizeDebugMessage(message: ChatMessage): ChatMessage {
  const role = (String(message.role ?? "") === "assistant" ? "agent" : message.role) as ChatMessage["role"];
  return {
    ...message,
    role,
    id: firstNonEmptyString(message.messageId, message.id) ?? "msg_debug_agent",
    messageId: firstNonEmptyString(message.messageId, message.id) ?? "msg_debug_agent",
    title: firstNonEmptyString(message.title) ?? (role === "user" ? "用户" : "Code Agent"),
    status: normalizeStatus(message.status) ?? "running",
    createdAt: firstNonEmptyString(message.createdAt) ?? new Date().toISOString(),
    text: role === "agent" && !isTerminalStatus(message.status) ? "" : String(message.text ?? "")
  };
}

function debugLogEntry(event: WorkbenchRealtimeEvent, eventName: string, actionType: string, steps: WorkbenchRealtimeApplyStep[], reason: string | null): WorkbenchDebugFakeSseLogEntry {
  const ignored = actionType === "ignore";
  return {
    id: `${Date.now()}:${eventName}:${event.traceId ?? "none"}:${Math.random().toString(16).slice(2)}`,
    eventName,
    eventType: firstNonEmptyString(event.type) ?? "unknown",
    actionType,
    stepTypes: steps.map((step) => step.type),
    traceId: firstNonEmptyString(event.traceId, event.snapshot?.traceId, event.event?.traceId, event.message?.traceId) ?? null,
    accepted: !ignored,
    reason: ignored ? reason ?? "ignored" : null,
    observedAt: new Date().toISOString()
  };
}

function normalizeStatus(value: unknown): ChatMessage["status"] | null {
  const status = String(value ?? "").trim().toLowerCase().replace(/_/gu, "-");
  if (["pending", "sent", "running", "completed", "failed", "blocked", "timeout", "canceled"].includes(status)) return status as ChatMessage["status"];
  if (status === "cancelled") return "canceled";
  return null;
}

function isTerminalStatus(value: unknown): boolean {
  return ["completed", "failed", "blocked", "timeout", "canceled", "cancelled"].includes(String(value ?? "").trim().toLowerCase().replace(/_/gu, "-"));
}

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