/*
 * SPEC: PJ2026-0104010803 Workbench唯一投影; HWLAB#2464 transactional Kafka projector.
 * Responsibility: atomically promote durable admission into initial nonterminal Workbench facts and build pure Kafka projection facts.
 * Local admission may create the first running session/message/turn snapshot; all later lifecycle and terminal authority comes only from commitAgentRunKafkaProjection.
 */
import { createHash } from "node:crypto";

import { safeTraceId } from "./server-http-utils.ts";
import { normalizeWorkbenchStatus, terminalFinalResponse, TERMINAL_STATUSES } from "./workbench-turn-projection.ts";

export async function recordWorkbenchSessionOwner({ accessController, traceId, ownerUserId, ownerRole = null, sessionId = null, projectId = null, conversationId = null, threadId = null, status = "active", session = {}, preserveLastTraceId = false } = {}) {
  if (!ownerUserId || typeof accessController?.recordAgentSessionOwner !== "function") return null;
  return accessController.recordAgentSessionOwner({
    ownerUserId,
    ownerRole,
    sessionId,
    projectId,
    conversationId,
    threadId,
    traceId: preserveLastTraceId ? undefined : safeTraceId(traceId),
    status,
    session
  });
}

export async function writeWorkbenchSessionAdmissionFact({ runtimeStore = null, session = null, ownerUserId = null, ownerRole = null, projectId = null, conversationId = null, threadId = null, status = "idle", now = new Date().toISOString() } = {}) {
  if (typeof runtimeStore?.writeWorkbenchSessionAdmissionFact !== "function") {
    throw workbenchProjectionError("workbench_admission_store_unconfigured", "Workbench admission requires the durable session admission store.");
  }
  const fact = buildWorkbenchSessionAdmissionFact({ session, ownerUserId, ownerRole, projectId, conversationId, threadId, status, now });
  return runtimeStore.writeWorkbenchSessionAdmissionFact({ fact }, {
    sessionId: fact.sessionId,
    ownerUserId: fact.ownerUserId,
    projectId: fact.projectId,
    valuesPrinted: false
  });
}

export async function promoteWorkbenchTurnAdmission({ runtimeStore = null, session = null, inputFact = null, payload = {}, params = {}, ownerUserId = null, ownerRole = null, projectId = null, conversationId = null, threadId = null, now = new Date().toISOString() } = {}) {
  if (typeof runtimeStore?.promoteWorkbenchTurnAdmission !== "function") {
    throw workbenchProjectionError("workbench_admission_promotion_store_unconfigured", "Workbench admission promotion requires the durable atomic promotion store.");
  }
  if (!inputFact || typeof inputFact !== "object") {
    throw workbenchProjectionError("workbench_admission_input_required", "Workbench admission promotion requires an input fact.");
  }
  const facts = buildWorkbenchAdmissionPromotionFacts({ session, inputFact, payload, params, ownerUserId, ownerRole, projectId, conversationId, threadId, now });
  const sessionFact = facts.sessions[0];
  return runtimeStore.promoteWorkbenchTurnAdmission({ facts }, {
    sessionId: sessionFact.sessionId,
    traceId: inputFact.traceId ?? null,
    turnId: inputFact.turnId ?? null,
    messageId: inputFact.messageId ?? null,
    ownerUserId: sessionFact.ownerUserId,
    projectId: sessionFact.projectId,
    valuesPrinted: false
  });
}

export function buildWorkbenchAdmissionPromotionFacts({ session = null, inputFact = null, payload = {}, params = {}, ownerUserId = null, ownerRole = null, projectId = null, conversationId = null, threadId = null, now = new Date().toISOString() } = {}) {
  const sessionFact = buildWorkbenchSessionAdmissionFact({ session, ownerUserId, ownerRole, projectId, conversationId, threadId, status: "running", now });
  const traceId = safeTraceId(payload.traceId ?? params.traceId ?? inputFact?.traceId);
  if (!traceId) throw workbenchProjectionError("workbench_admission_trace_required", "Workbench admission promotion requires traceId.");
  const turnId = textValue(payload.turnId ?? params.turnId ?? inputFact?.turnId) || traceId;
  const userMessageId = textValue(payload.userMessageId ?? params.userMessageId ?? inputFact?.messageId) || eventProjectionUserMessageId(traceId, {});
  const assistantMessageId = textValue(payload.assistantMessageId ?? params.assistantMessageId) || eventProjectionAssistantMessageId(traceId, {});
  const sourceEventId = `${traceId}:admission-promotion`;
  const occurredAt = timestampValue(payload.createdAt ?? now);
  const timing = eventTimingProjection({ startedAt: occurredAt, lastEventAt: occurredAt, terminal: false });
  const userText = textValue(params.message ?? params.prompt ?? params.text);
  const common = {
    sessionId: sessionFact.sessionId,
    turnId,
    traceId,
    projectedSeq: 0,
    sourceSeq: 0,
    sourceEventId,
    terminal: false,
    sealed: false,
    timing,
    startedAt: timing.startedAt,
    lastEventAt: timing.lastEventAt,
    finishedAt: null,
    durationMs: null,
    createdAt: occurredAt,
    updatedAt: occurredAt,
    valuesPrinted: false
  };
  const userMessage = { ...common, messageId: userMessageId, role: "user", status: "sent", text: userText };
  const assistantMessage = { ...common, messageId: assistantMessageId, role: "agent", status: "running", text: "" };
  const turn = {
    ...common,
    messageId: assistantMessageId,
    status: "running",
    finalResponse: null,
    assistantText: "",
    failureKind: null,
    diagnostic: {
      authority: "local-admission-promotion",
      admissionState: "promoted",
      sourceRunId: textValue(payload.agentRun?.runId) || null,
      sourceCommandId: textValue(payload.agentRun?.commandId) || null,
      dispatchIntentId: textValue(payload.agentRun?.dispatchIntentId) || null,
      runnerJobId: textValue(payload.agentRun?.runnerJobId) || null,
      valuesRedacted: true
    }
  };
  return {
    inputs: [{
      ...inputFact,
      status: "promoted",
      commandId: textValue(payload.agentRun?.commandId ?? inputFact.commandId) || null,
      runId: textValue(payload.agentRun?.runId) || null,
      dispatchIntentId: textValue(payload.agentRun?.dispatchIntentId) || null,
      runnerJobId: textValue(payload.agentRun?.runnerJobId) || null,
      durableDispatch: payload.agentRun?.durableDispatch === true,
      updatedAt: occurredAt,
      valuesRedacted: true,
      secretMaterialStored: false
    }],
    sessions: [{
      ...sessionFact,
      status: "running",
      lastTraceId: traceId,
      sessionJson: {
        ...sessionFact.sessionJson,
        admissionState: "promoted",
        agentRun: payload.agentRun ?? sessionFact.sessionJson?.agentRun ?? null,
        valuesRedacted: true,
        secretMaterialStored: false
      },
      updatedAt: occurredAt
    }],
    messages: [userMessage, assistantMessage],
    parts: userText ? messagePartFacts(userMessage) : [],
    turns: [turn],
    traceEvents: [],
    checkpoints: []
  };
}

export function buildWorkbenchSessionAdmissionFact({ session = null, ownerUserId = null, ownerRole = null, projectId = null, conversationId = null, threadId = null, status = "idle", now = new Date().toISOString() } = {}) {
  if (!session || typeof session !== "object") {
    throw workbenchProjectionError("workbench_admission_session_required", "Workbench admission requires a session record.");
  }
  const sessionId = textValue(session.id ?? session.sessionId);
  if (!sessionId) {
    throw workbenchProjectionError("workbench_admission_session_required", "Workbench admission requires sessionId.");
  }
  const createdAt = timestampValue(session.startedAt ?? session.createdAt ?? session.session?.createdAt ?? now);
  const updatedAt = timestampValue(session.updatedAt ?? createdAt);
  const normalizedStatus = normalizeWorkbenchStatus(session.status ?? session.session?.sessionStatus ?? status) || "idle";
  const sessionJson = workbenchSessionJsonWithLaunchContext(plainObjectValue(session.session) ?? {}, { payload: session });
  return {
    sessionId,
    ownerUserId: textValue(session.ownerUserId ?? ownerUserId) || null,
    ownerRole: textValue(session.ownerRole ?? ownerRole) || null,
    projectId: textValue(session.projectId ?? projectId) || null,
    agentId: textValue(session.agentId) || "hwlab-code-agent",
    conversationId: textValue(session.conversationId ?? conversationId) || null,
    threadId: textValue(session.threadId ?? threadId) || null,
    status: normalizedStatus,
    lastTraceId: safeTraceId(session.lastTraceId ?? session.traceId) || null,
    projectedSeq: 0,
    sourceSeq: 0,
    sourceEventId: `${sessionId}:session-admission`,
    terminal: false,
    sealed: false,
    sessionJson: {
      ...sessionJson,
      sessionStatus: normalizedStatus,
      source: textValue(sessionJson.source ?? session.session?.source) || "session-admission",
      providerProfile: textValue(sessionJson.providerProfile ?? session.session?.providerProfile) || null,
      valuesRedacted: true,
      secretMaterialStored: false
    },
    createdAt,
    updatedAt,
    valuesRedacted: true
  };
}

export function buildWorkbenchProjectionEventFacts({ event = {}, requestMeta = {}, previousCheckpoint = null, projectedSeq, projectedAt = new Date().toISOString() } = {}) {
  const traceId = safeTraceId(event.traceId ?? requestMeta.traceId);
  if (!traceId) throw workbenchProjectionError("workbench_projection_trace_required", "Kafka projection event requires traceId.");
  const durableProjectedSeq = nonNegativeInteger(projectedSeq);
  if (durableProjectedSeq <= 0) throw workbenchProjectionError("workbench_projection_seq_required", "Kafka projection event requires an allocated projectedSeq.");
  const sourceSeq = nonNegativeInteger(event.sourceSeq ?? event.seq);
  const sessionId = textValue(event.sessionId ?? requestMeta.sessionId) || null;
  const turnId = textValue(event.turnId) || traceId;
  const eventType = textValue(event.eventType ?? event.type ?? event.label) || "event";
  const sourceTerminal = event.terminal === true || TERMINAL_STATUSES.has(normalizeWorkbenchStatus(event.status));
  const sourceStatus = sourceTerminal ? normalizeWorkbenchStatus(event.status) : "running";
  const previousTiming = normalizeTimingProjection(previousCheckpoint?.timing) ?? normalizeTimingProjection(previousCheckpoint);
  const sourceOccurredAt = timestampValue(event.createdAt ?? event.occurredAt ?? event.updatedAt ?? projectedAt);
  const occurredAt = latestTimestamp(previousTiming?.lastEventAt, sourceOccurredAt) ?? sourceOccurredAt;
  if (checkpointIsTerminal(previousCheckpoint)) {
    return {
      written: false,
      suppressedAfterSeal: true,
      traceId,
      sessionId,
      sourceSeq,
      sourceEventId: workbenchSourceEventId(event, { traceId, sourceSeq, occurredAt }),
      projectedSeq: nonNegativeInteger(previousCheckpoint?.projectedSeq) || durableProjectedSeq,
      valuesPrinted: false
    };
  }
  const provisionalLastEventAt = latestTimestamp(previousTiming?.lastEventAt, occurredAt);
  const provisionalFinishedAt = sourceTerminal ? provisionalLastEventAt : null;
  const startedAt = previousTiming?.startedAt ?? occurredAt;
  const lastEventAt = provisionalLastEventAt;
  const finishedAt = sourceTerminal ? latestTimestamp(provisionalFinishedAt, lastEventAt) : null;
  const sourceEventId = workbenchSourceEventId(event, { traceId, sourceSeq, occurredAt });
  const eventId = textValue(event.id) || stableFactId("wte", { traceId, sourceEventId });
  const previousFinalText = finalResponseTextValue(previousCheckpoint?.finalResponse, previousCheckpoint?.assistantText, previousCheckpoint?.finalText);
  const liveAssistantText = isAssistantMessageTraceEvent(event) ? finalResponseTextValue(event.assistantText, event.text, event.reply, event.message) : finalResponseTextValue(event.assistantText, event.reply);
  const eventTerminalFinalResponse = sourceTerminal ? terminalFinalResponse(sourceStatus, event, { evidence: event, finalResponse: event.finalResponse }) : null;
  const messageFinalText = sourceTerminal ? finalResponseTextValue(event.finalResponse, liveAssistantText, previousFinalText, eventTerminalFinalResponse) : null;
  const checkpointAssistantText = messageFinalText ?? liveAssistantText ?? previousFinalText ?? null;
  const checkpointFinalResponse = sourceTerminal && messageFinalText
    ? { ...(eventTerminalFinalResponse ?? {}), text: messageFinalText, status: sourceStatus, traceId, source: "agentrun-kafka-projector", valuesPrinted: false }
    : previousCheckpoint?.finalResponse ?? null;
  const terminalSealBlocked = sourceTerminal && sourceStatus === "completed" && !messageFinalText;
  const effectiveTerminal = sourceTerminal && !terminalSealBlocked;
  const effectiveStatus = effectiveTerminal ? sourceStatus : "running";
  const timing = eventTimingProjection({ startedAt, lastEventAt, finishedAt, terminal: effectiveTerminal });
  const timingAuthorityIssue = terminalTimingAuthorityIssue(timing, { terminal: effectiveTerminal, traceId, source: "kafka", status: effectiveStatus, label: event.label, sourceSeq });
  const projectionDiagnostic = { timingAuthorityIssue, terminalSealBlocked, sourceTerminal, sourceStatus, authority: "agentrun-kafka-projector", valuesRedacted: true };
  const userMessageFact = sessionId && previousCheckpoint?.userMessage ? normalizeMessageFact(previousCheckpoint.userMessage, 0, { traceId, sessionId, turnId, terminal: false, terminalStatus: "sent", finalText: null, timestamp: projectedAt, timing }) : null;
  const messageFact = sessionId ? normalizeMessageFact({
    messageId: eventProjectionAssistantMessageId(traceId, event),
    role: "agent",
    status: effectiveStatus,
    traceId,
    turnId,
    text: messageFinalText ?? liveAssistantText ?? previousFinalText ?? "",
    projectedSeq: durableProjectedSeq,
    sourceSeq,
    sourceEventId,
    terminal: effectiveTerminal,
    sealed: effectiveTerminal,
    timing,
    startedAt: timing.startedAt,
    lastEventAt: timing.lastEventAt,
    finishedAt: timing.finishedAt,
    durationMs: timing.durationMs,
    createdAt: occurredAt,
    updatedAt: projectedAt,
    source: "agentrun-kafka-projector"
  }, userMessageFact ? 1 : 0, { traceId, sessionId, turnId, terminal: effectiveTerminal, terminalStatus: effectiveStatus, finalText: messageFinalText, terminalSealBlocked, timestamp: projectedAt, timing }) : null;
  const facts = {
    sessions: sessionId ? [{ sessionId, status: effectiveStatus, lastTraceId: traceId, projectedSeq: durableProjectedSeq, sourceSeq, sourceEventId, terminal: effectiveTerminal, sealed: effectiveTerminal, timing, startedAt: timing.startedAt, lastEventAt: timing.lastEventAt, finishedAt: timing.finishedAt, durationMs: timing.durationMs, updatedAt: projectedAt }] : [],
    traceEvents: [{ ...event, id: eventId, traceId, sessionId, turnId, messageId: textValue(event.messageId) || null, status: effectiveStatus, sourceSeq, sourceEventId, projectedSeq: durableProjectedSeq, eventType, terminal: effectiveTerminal, sealed: effectiveTerminal, diagnostic: projectionDiagnostic, timing, startedAt: timing.startedAt, lastEventAt: timing.lastEventAt, finishedAt: timing.finishedAt, durationMs: timing.durationMs, createdAt: occurredAt, occurredAt, updatedAt: projectedAt }],
    messages: [userMessageFact, messageFact].filter(Boolean),
    parts: [userMessageFact, messageFact].filter(Boolean).flatMap((message) => messagePartFacts(message, { finalText: message === messageFact ? messageFinalText : null })),
    turns: sessionId ? [{
      turnId,
      sessionId,
      traceId,
      messageId: messageFact?.messageId ?? null,
      status: effectiveStatus,
      projectedSeq: durableProjectedSeq,
      sourceSeq,
      sourceEventId,
      terminal: effectiveTerminal,
      sealed: effectiveTerminal,
      finalResponse: checkpointFinalResponse,
      assistantText: checkpointAssistantText,
      failureKind: textValue(event.failureKind ?? event.errorCode) || null,
      diagnostic: projectionDiagnostic,
      timing,
      startedAt: timing.startedAt,
      lastEventAt: timing.lastEventAt,
      finishedAt: timing.finishedAt,
      durationMs: timing.durationMs,
      createdAt: occurredAt,
      updatedAt: projectedAt
    }] : [],
    checkpoints: [{
      traceId,
      sessionId,
      turnId,
      runId: textValue(event.runId) || null,
      commandId: textValue(event.commandId) || null,
      projectedSeq: durableProjectedSeq,
      sourceSeq,
      sourceEventId,
      projectionStatus: effectiveTerminal ? "caught_up" : "projecting",
      projectionHealth: terminalSealBlocked ? "degraded" : "healthy",
      terminal: effectiveTerminal,
      sealed: effectiveTerminal,
      assistantText: checkpointAssistantText,
      finalResponse: checkpointFinalResponse,
      timing,
      startedAt: timing.startedAt,
      lastEventAt: timing.lastEventAt,
      finishedAt: timing.finishedAt,
      durationMs: timing.durationMs,
      diagnostic: { ...projectionDiagnostic, lastEventLabel: textValue(event.label) || null, lastEventStatus: textValue(event.status) || null },
      updatedAt: projectedAt
    }]
  };
  return { written: true, suppressedAfterSeal: false, traceId, sessionId, sourceSeq, sourceEventId, projectedSeq: durableProjectedSeq, facts, terminal: effectiveTerminal, sourceTerminal, terminalSealBlocked, valuesPrinted: false };
}

function eventProjectionAssistantMessageId(traceId, event = {}) {
  return eventProjectionMessageId(traceId, "agent", event);
}

function eventProjectionUserMessageId(traceId, event = {}) {
  return eventProjectionMessageId(traceId, "user", event);
}

function eventProjectionMessageId(traceId, role, event = {}) {
  const explicit = role === "user"
    ? textValue(event.userMessageId ?? event.userMessage?.messageId)
    : textValue(event.messageId ?? event.assistantMessageId ?? event.assistantMessage?.messageId);
  if (explicit) return explicit;
  const traceSuffix = (safeTraceId(traceId) || String(traceId || "trace"))
    .replace(/^trc_/u, "")
    .replace(/[^A-Za-z0-9_.:-]/gu, "_")
    .slice(0, 48) || "trace";
  return `msg_${traceSuffix}_${role === "user" ? "user" : "agent"}`;
}

function workbenchSessionJsonWithLaunchContext(session = {}, { payload = null, params = null } = {}) {
  const base = plainObjectValue(session) ?? {};
  const launchContext = firstPlainObjectValue(
    base.launchContext,
    base.sessionJson?.launchContext,
    base.session?.launchContext,
    payload?.launchContext,
    payload?.sessionJson?.launchContext,
    payload?.session?.launchContext,
    params?.launchContext,
    params?.sessionJson?.launchContext,
    params?.session?.launchContext
  );
  if (!launchContext) return base;
  const next = { ...base, launchContext: { ...launchContext, valuesRedacted: true }, valuesRedacted: true };
  if (!Object.prototype.hasOwnProperty.call(next, "secretMaterialStored")) next.secretMaterialStored = false;
  return next;
}

function firstPlainObjectValue(...values) {
  for (const value of values) {
    const object = plainObjectValue(value);
    if (object) return object;
  }
  return null;
}

function plainObjectValue(value) {
  return value && typeof value === "object" && !Array.isArray(value) ? value : null;
}

function workbenchProjectionError(code, message) {
  const error = new Error(message);
  error.code = code;
  error.valuesRedacted = true;
  return error;
}

function messagePartFacts(message = {}, { finalText = null } = {}) {
  const messageId = textValue(message.messageId);
  if (!messageId || !message.sessionId) return [];
  const role = textValue(message.role) || "agent";
  const terminal = message.terminal === true;
  const finalResponseText = role !== "user" && terminal ? finalResponseTextValue(finalText, message.finalResponse) : null;
  const text = finalResponseText ?? textValue(message.text);
  if (!text) return [];
  const partType = finalResponseText ? "final_response" : "text";
  return [{
    partId: stableFactId("wpt", { messageId, index: 0, partType }),
    messageId,
    sessionId: message.sessionId,
    turnId: message.turnId,
    traceId: message.traceId,
    partIndex: 0,
    partType,
    status: message.status,
    projectedSeq: message.projectedSeq,
    sourceSeq: message.sourceSeq,
    sourceEventId: message.sourceEventId,
    terminal,
    sealed: finalResponseText ? true : message.sealed,
    text,
    createdAt: message.createdAt,
    updatedAt: message.updatedAt,
    valuesPrinted: false
  }];
}

function normalizeMessageFact(message = {}, index, { traceId, sessionId, conversationId, threadId, terminal, terminalStatus = "completed", finalText = null, terminalSealBlocked = false, timestamp, timing: contextTiming }) {
  const role = textValue(message.role) || "agent";
  const messageId = textValue(message.messageId ?? message.id) || (role === "user" ? eventProjectionUserMessageId(traceId, message) : eventProjectionAssistantMessageId(traceId, message));
  if (!sessionId || !messageId) return null;
  const messageTraceId = safeTraceId(message.traceId ?? message.turnId);
  const appliesToContextTrace = role !== "user" && Boolean(traceId && (!messageTraceId || messageTraceId === traceId));
  const contextTerminalStatus = normalizeWorkbenchStatus(terminalStatus) || "completed";
  const status = appliesToContextTrace && terminalSealBlocked ? "running" : appliesToContextTrace && terminal ? contextTerminalStatus : normalizeWorkbenchStatus(message.status ?? (terminal && role !== "user" ? contextTerminalStatus : role === "user" ? "sent" : "running"));
  const messageTerminal = role !== "user" && !(appliesToContextTrace && terminalSealBlocked) && TERMINAL_STATUSES.has(status);
  const messageTiming = normalizeTimingProjection(message.timing) ?? normalizeTimingProjection(message);
  const contextTimingProjection = appliesToContextTrace ? normalizeTimingProjection(contextTiming) : null;
  const timing = role === "user" ? messageTiming ?? emptyTimingProjection() : contextTimingProjection ?? messageTiming ?? emptyTimingProjection();
  const terminalCompleted = messageTerminal && status === "completed";
  const text = appliesToContextTrace && terminal && finalText ? finalText : textValue(message.text ?? message.content ?? message.message);
  return {
    ...message,
    messageId,
    sessionId,
    conversationId,
    threadId,
    turnId: textValue(message.turnId) || traceId,
    traceId: safeTraceId(message.traceId ?? traceId) ?? traceId,
    role,
    status,
    projectedSeq: nonNegativeInteger(message.projectedSeq ?? message.seq ?? index + 1),
    sourceSeq: nonNegativeInteger(message.sourceSeq ?? message.seq ?? index + 1),
    sourceEventId: textValue(message.sourceEventId) || `${messageId}:0`,
    terminal: messageTerminal,
    sealed: messageTerminal,
    text,
    timing,
    startedAt: timing.startedAt,
    lastEventAt: timing.lastEventAt,
    finishedAt: timing.finishedAt,
    durationMs: timing.durationMs,
    createdAt: timestampValue(message.createdAt ?? timestamp),
    updatedAt: timestampValue(message.updatedAt ?? timestamp),
    ...(terminalCompleted ? { error: null, blocker: null, projection: null, projectionStatus: null, projectionHealth: null, staleMs: null } : {}),
    valuesPrinted: false
  };
}

function finalResponseTextValue(...values) {
  for (const value of values) {
    if (value && typeof value === "object") {
      const nested = finalResponseTextValue(value.text, value.content, value.message, value.summary, value.preview);
      if (nested) return nested;
      continue;
    }
    const text = textValue(value);
    if (text) return text;
  }
  return null;
}

function checkpointIsTerminal(value) {
  const source = value && typeof value === "object" ? value : null;
  if (!source) return false;
  if (source.terminal === true || source.sealed === true) return true;
  const status = normalizeWorkbenchStatus(source.status ?? source.terminalStatus ?? source.traceStatus);
  return TERMINAL_STATUSES.has(status);
}

function eventTimingProjection({ startedAt = null, lastEventAt = null, finishedAt = null, terminal = false, durationMs = null } = {}) {
  const normalizedStartedAt = optionalTimestampValue(startedAt);
  const normalizedLastEventAt = optionalTimestampValue(lastEventAt);
  const normalizedFinishedAt = terminal ? optionalTimestampValue(finishedAt ?? normalizedLastEventAt) : null;
  const normalizedDurationMs = durationValue(durationMs);
  return {
    startedAt: normalizedStartedAt,
    lastEventAt: normalizedLastEventAt,
    finishedAt: normalizedFinishedAt,
    durationMs: terminal ? normalizedDurationMs ?? elapsedBetween(normalizedStartedAt, normalizedFinishedAt ?? normalizedLastEventAt) : null,
    valuesRedacted: true
  };
}

function normalizeTimingProjection(value) {
  const source = value && typeof value === "object" ? value : null;
  if (!source) return null;
  const startedAt = optionalTimestampValue(source.startedAt);
  const lastEventAt = optionalTimestampValue(source.lastEventAt);
  const finishedAt = optionalTimestampValue(source.finishedAt);
  const durationMs = durationValue(source.durationMs);
  if (!startedAt && !lastEventAt && !finishedAt && durationMs === null) return null;
  return { ...source, startedAt, lastEventAt, finishedAt, durationMs, valuesRedacted: source.valuesRedacted !== false };
}

function emptyTimingProjection() {
  return { startedAt: null, lastEventAt: null, finishedAt: null, durationMs: null, valuesRedacted: true };
}

function durationValue(value) {
  if (value === null || value === undefined || value === "") return null;
  const number = Number(value);
  return Number.isFinite(number) && number >= 0 ? Math.trunc(number) : null;
}

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

function terminalTimingAuthorityIssue(timing, { terminal = false, traceId = null, source = null, status = null, label = null, sourceSeq = null } = {}) {
  if (!terminal) return null;
  const startedAt = optionalTimestampValue(timing?.startedAt);
  const finishedAt = optionalTimestampValue(timing?.finishedAt);
  const durationMs = durationValue(timing?.durationMs);
  if (startedAt && finishedAt && durationMs !== null) return null;
  return {
    code: "workbench_terminal_timing_authority_missing",
    layer: "workbench-projection",
    category: "timing-authority",
    traceId,
    source,
    status,
    label: textValue(label) || null,
    sourceSeq: nonNegativeInteger(sourceSeq),
    missingStartedAt: !startedAt,
    missingFinishedAt: !finishedAt,
    missingDurationMs: durationMs === null,
    retryable: true,
    message: "Workbench terminal projection is missing durable timing authority; the UI must surface this diagnostic instead of hiding elapsed time.",
    valuesPrinted: false
  };
}

function latestTimestamp(...values) {
  let latest = null;
  let latestMs = Number.NEGATIVE_INFINITY;
  for (const value of values) {
    const timestamp = optionalTimestampValue(value);
    const ms = Date.parse(String(timestamp ?? ""));
    if (!Number.isFinite(ms) || ms < latestMs) continue;
    latest = timestamp;
    latestMs = ms;
  }
  return latest;
}

function stableFactId(prefix, value) {
  return `${prefix}_${createHash("sha256").update(JSON.stringify(sortJson(value))).digest("hex").slice(0, 32)}`;
}

function workbenchSourceEventId(event = {}, { traceId, sourceSeq, occurredAt } = {}) {
  const explicit = textValue(event.sourceEventId ?? event.id);
  if (explicit) return explicit;
  if (isAssistantMessageTraceEvent(event)) {
    const messageId = eventProjectionAssistantMessageId(traceId, event);
    return `${safeTraceId(traceId) ?? traceId}:assistant-message:${messageId}`;
  }
  const source = textValue(event.source);
  const label = textValue(event.label ?? event.type ?? event.eventType) || "event";
  if (source && sourceSeq > 0) return `${source}:${sourceSeq}:${label}`;
  return stableFactId("wte_source", {
    traceId,
    source: source || null,
    sourceSeq,
    label,
    eventType: textValue(event.eventType ?? event.type) || null,
    status: textValue(event.status) || null,
    itemId: textValue(event.itemId ?? event.messageId) || null,
    runId: textValue(event.runId) || null,
    commandId: textValue(event.commandId) || null,
    occurredAt
  });
}

function isAssistantMessageTraceEvent(event = {}) {
  const type = textValue(event?.eventType ?? event?.type).toLowerCase();
  const label = textValue(event?.label).toLowerCase();
  return type === "assistant" || /assistant:message|assistant_message/u.test(label);
}

function sortJson(value) {
  if (Array.isArray(value)) return value.map(sortJson);
  if (value && typeof value === "object") return Object.fromEntries(Object.keys(value).sort().map((key) => [key, sortJson(value[key])]));
  return value;
}

function nonNegativeInteger(value) {
  const parsed = Number.parseInt(value ?? "", 10);
  return Number.isInteger(parsed) && parsed >= 0 ? parsed : 0;
}

function timestampValue(value) {
  const ms = Date.parse(String(value ?? ""));
  return Number.isFinite(ms) ? new Date(ms).toISOString() : new Date().toISOString();
}

function optionalTimestampValue(value) {
  const ms = Date.parse(String(value ?? ""));
  return Number.isFinite(ms) ? new Date(ms).toISOString() : null;
}

function textValue(value) {
  return String(value ?? "").trim();
}
