// SPEC: PJ2026-0104010803 Workbench live Kafka SSE.
// Responsibility: project one transparent hwlab.event.v1 business event into visible turn state without replay/finalizer/polling.

import { mergeRunnerTrace } from "../composables/workbench-trace-snapshot";
import type { ChatMessage, TraceEvent, WorkbenchSessionRecord, WorkbenchTurnTimingProjection } from "../types";
import { firstNonEmptyString } from "../utils";
import { traceAssistantLifecycleEventIsSuperseded } from "../../../../tools/src/hwlab-cli/trace-renderer.ts";

export interface WorkbenchLiveMessageState {
  text: string;
  status: string;
  terminal: boolean;
}

export interface WorkbenchLiveKafkaProjectionInput {
  previous: ChatMessage | null;
  traceId: string;
  sessionId: string;
  event: TraceEvent;
  receivedAt: string;
  turnStartedAt?: string | null;
  title?: string;
  threadId?: string | null;
}

export interface WorkbenchLiveKafkaUserProjectionInput {
  previous: ChatMessage | null;
  traceId: string;
  sessionId: string;
  event: TraceEvent;
  receivedAt: string;
  threadId?: string | null;
}

export interface WorkbenchLiveKafkaUserSessionProjectionInput {
  previous: WorkbenchSessionRecord | null;
  message: ChatMessage;
  sessionId: string;
  threadId?: string | null;
}

export function reduceWorkbenchLiveKafkaMessageState(previous: WorkbenchLiveMessageState, event: TraceEvent, previousEvents: TraceEvent[] = []): WorkbenchLiveMessageState {
  if (previous.terminal) return previous;
  const terminal = workbenchLiveKafkaEventIsTerminal(event);
  const status = terminal ? firstNonEmptyString(event.status, "completed") ?? "completed" : "running";
  const assistantText = traceAssistantLifecycleEventIsSuperseded(event, previousEvents, previousEvents.length)
    ? null
    : workbenchLiveKafkaAssistantText(event);
  return {
    text: assistantText ?? (terminal ? workbenchLiveKafkaTerminalText(event, previous.text, status) : previous.text),
    status,
    terminal
  };
}

export function workbenchUserMessageIdForTrace(traceId: string): string {
  return workbenchMessageIdForTrace(traceId, "user");
}

export function workbenchAgentMessageIdForTrace(traceId: string): string {
  return workbenchMessageIdForTrace(traceId, "agent");
}

function workbenchMessageIdForTrace(traceId: string, role: "user" | "agent"): string {
  const suffix = firstNonEmptyString(traceId)
    ?.replace(/^trc_/u, "")
    .replace(/[^A-Za-z0-9_.:-]/gu, "_")
    .slice(0, 48);
  if (!suffix) throw new Error("traceId must produce a stable Workbench message identity");
  return `msg_${suffix}_${role}`;
}

export function workbenchLiveKafkaEnvelope(schema: unknown): boolean {
  return firstNonEmptyString(schema) === "hwlab.event.v1";
}

export function workbenchLiveKafkaEventIsUserMessage(event: TraceEvent | null | undefined): boolean {
  return firstNonEmptyString(event?.type, event?.eventType) === "user";
}

export function workbenchLiveKafkaProjectionTarget(event: TraceEvent | null | undefined): "user" | "agent" {
  return workbenchLiveKafkaEventIsUserMessage(event) ? "user" : "agent";
}

export function projectWorkbenchLiveKafkaUserMessage(input: WorkbenchLiveKafkaUserProjectionInput): ChatMessage | null {
  if (!workbenchLiveKafkaEventIsUserMessage(input.event)) return null;
  const messageId = firstNonEmptyString(input.event.userMessageId, input.event.messageId);
  const text = firstNonEmptyString(input.event.text, input.event.message);
  const receivedAt = timestamp(input.receivedAt);
  const createdAt = timestamp(input.event.createdAt) ?? receivedAt;
  if (!messageId || !text || !receivedAt || !createdAt) return null;
  return {
    ...(input.previous ?? {}),
    id: messageId,
    messageId,
    role: "user",
    title: "用户",
    text,
    status: "sent",
    traceId: input.traceId,
    targetTraceId: firstNonEmptyString(input.event.targetTraceId),
    turnId: input.previous?.turnId ?? input.traceId,
    sessionId: input.sessionId,
    threadId: firstNonEmptyString(input.threadId, input.previous?.threadId),
    createdAt: input.previous?.createdAt ?? createdAt,
    updatedAt: createdAt
  } as ChatMessage;
}

export function projectWorkbenchLiveKafkaUserSession(input: WorkbenchLiveKafkaUserSessionProjectionInput): WorkbenchSessionRecord | null {
  const text = input.message.role === "user" ? firstNonEmptyString(input.message.text, input.message.content) : null;
  const lastUserMessageAt = timestamp(input.message.createdAt) ?? timestamp(input.message.updatedAt);
  if (!text || !lastUserMessageAt) return null;
  return {
    ...(input.previous ?? {}),
    sessionId: input.sessionId,
    threadId: firstNonEmptyString(input.threadId, input.message.threadId, input.previous?.threadId),
    firstUserMessagePreview: firstNonEmptyString(input.previous?.firstUserMessagePreview, text),
    lastUserMessageAt,
    updatedAt: lastUserMessageAt
  };
}

export function workbenchLiveKafkaSessionSummaryChanged(previous: WorkbenchSessionRecord, next: WorkbenchSessionRecord): boolean {
  return firstNonEmptyString(previous.threadId) !== firstNonEmptyString(next.threadId)
    || firstNonEmptyString(previous.status) !== firstNonEmptyString(next.status)
    || firstNonEmptyString(previous.lastTraceId) !== firstNonEmptyString(next.lastTraceId)
    || firstNonEmptyString(previous.lastUserMessageAt) !== firstNonEmptyString(next.lastUserMessageAt);
}

export function workbenchLiveKafkaEventIsTerminal(event: TraceEvent | null | undefined): boolean {
  return Boolean(event?.terminal === true || firstNonEmptyString(event?.eventType) === "terminal" || firstNonEmptyString(event?.type) === "result");
}

export function workbenchLiveKafkaAssistantText(event: TraceEvent | null | undefined): string | null {
  if (!event || !["assistant", "assistant_progress"].includes(firstNonEmptyString(event.type, event.eventType) ?? "")) return null;
  const finalResponse = recordValue(event.finalResponse);
  return firstNonEmptyString(finalResponse?.text, event.assistantText, event.text, event.message) ?? null;
}

export function workbenchLiveKafkaTerminalText(event: TraceEvent, previousText: string, status: string): string {
  const finalResponse = recordValue(event.finalResponse);
  const error = recordValue(event.error);
  const failed = ["failed", "blocked", "timeout", "canceled", "cancelled", "interrupted", "expired"].includes(status);
  const text = failed
    ? firstNonEmptyString(finalResponse?.text, event.message, error?.message, event.failureKind, event.errorCode, previousText)
    : firstNonEmptyString(finalResponse?.text, event.assistantText, event.text, previousText, event.message);
  return text ?? `AgentRun ${status}`;
}

export function projectWorkbenchLiveKafkaMessage(input: WorkbenchLiveKafkaProjectionInput): ChatMessage {
  const receivedAt = timestamp(input.receivedAt);
  if (!receivedAt) throw new Error("receivedAt must be a valid timestamp for live Kafka projection");
  const previous = input.previous;
  const eventAt = timestamp(input.event.createdAt) ?? receivedAt;
  const liveState = reduceWorkbenchLiveKafkaMessageState({
    text: previous?.text ?? "",
    status: previous?.status ?? "running",
    terminal: previous?.traceAutoLifecycle === "terminal"
  }, input.event, Array.isArray(previous?.runnerTrace?.events) ? previous.runnerTrace.events : []);
  const startedAt = timestamp(previous?.timing?.startedAt)
    ?? timestamp(previous?.startedAt)
    ?? timestamp(input.turnStartedAt)
    ?? eventAt;
  const previousFinishedAt = timestamp(previous?.timing?.finishedAt) ?? timestamp(previous?.finishedAt);
  const finishedAt = liveState.terminal ? previousFinishedAt ?? eventAt : null;
  const previousDurationMs = nonNegativeNumber(previous?.timing?.durationMs) ?? nonNegativeNumber(previous?.durationMs);
  const durationMs = liveState.terminal
    ? previousFinishedAt && previousDurationMs !== null
      ? previousDurationMs
      : Math.max(0, Date.parse(finishedAt ?? eventAt) - Date.parse(startedAt))
    : null;
  const timing: WorkbenchTurnTimingProjection = {
    startedAt,
    lastEventAt: eventAt,
    finishedAt,
    durationMs,
    observedAt: receivedAt,
    lastEventAgeMs: 0,
    valuesRedacted: true
  };
  const mergedRunnerTrace = mergeRunnerTrace(previous?.runnerTrace ?? null, {
    traceId: input.traceId,
    sessionId: input.sessionId,
    status: liveState.status,
    traceStatus: liveState.status,
    events: [input.event],
    eventCount: 1,
    eventSource: "hwlab-kafka-sse",
    timing,
    startedAt,
    lastEventAt: eventAt,
    finishedAt,
    durationMs,
    updatedAt: eventAt
  });
  const runnerTrace = {
    ...mergedRunnerTrace,
    status: liveState.status,
    traceStatus: liveState.status,
    timing,
    startedAt,
    lastEventAt: eventAt,
    finishedAt,
    durationMs,
    updatedAt: eventAt
  };
  const messageId = workbenchAgentMessageIdForTrace(input.traceId);
  return {
    ...(previous ?? {}),
    id: messageId,
    messageId,
    role: "agent",
    title: input.title ?? previous?.title ?? "Code Agent",
    text: liveState.text,
    status: liveState.status as ChatMessage["status"],
    traceId: input.traceId,
    turnId: previous?.turnId ?? input.traceId,
    sessionId: input.sessionId,
    threadId: firstNonEmptyString(input.threadId, previous?.threadId),
    createdAt: previous?.createdAt ?? startedAt,
    updatedAt: eventAt,
    startedAt,
    lastEventAt: eventAt,
    finishedAt,
    durationMs,
    runnerTrace,
    traceAutoLifecycle: liveState.terminal ? "terminal" : "running",
    timing,
    finalResponse: liveState.terminal && liveState.text
      ? { text: liveState.text, status: liveState.status, traceId: input.traceId }
      : null
  } as ChatMessage;
}

function timestamp(value: unknown): string | null {
  const text = firstNonEmptyString(value);
  return text && Number.isFinite(Date.parse(text)) ? text : null;
}

function nonNegativeNumber(value: unknown): number | null {
  return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null;
}

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