/*
 * SPEC: PJ2026-0104010803 Workbench唯一投影 draft-2026-06-19-p0-projector-resume; draft-2026-06-20-p1-zero-split-durable-realtime; PJ2026-010403 API契约 draft-2026-06-18-r1.
 * 职责: 生成唯一 Workbench turn projection；trace event 状态只作为输入证据，不直接成为 turn lifecycle。
 */

export const TERMINAL_STATUSES = new Set(["completed", "failed", "blocked", "timeout", "cancelled", "canceled", "idle"]);
export const RUNNING_STATUSES = new Set(["running", "retrying", "pending", "queued", "accepted", "dispatching", "streaming", "active", "processing", "busy", "creating"]);

export function createWorkbenchTurnProjection({ turnId = null, traceId = null, result = null, session = null, trace = null } = {}) {
  const projectionTraceId = textValue(traceId ?? trace?.traceId ?? result?.traceId ?? session?.lastTraceId) || null;
  const projectionTurnId = textValue(turnId) || projectionTraceId;
  const traceTerminal = traceTerminalEvidence(trace);
  const terminalEvidence = terminalTurnEvidence({ result, traceTerminal });
  const activeEvidence = activeTurnEvidence({ result, session, trace });
  const status = terminalEvidence?.status ?? activeEvidence?.status ?? "unknown";
  const running = RUNNING_STATUSES.has(status);
  const terminal = Boolean(terminalEvidence && TERMINAL_STATUSES.has(status) && !running);
  const finalText = terminal ? projectionText(result?.finalResponse, result?.assistantText, result?.reply, result?.text, result?.summary, trace?.finalResponse, trace?.terminalEvidence?.finalResponse, terminalEvidence?.finalResponse) : null;
  const agentRun = objectValue(result?.agentRun ?? trace?.agentRun);
  const lastEvent = traceLastEvent(trace);
  const timing = createWorkbenchTurnTimingProjection({ result, session, trace, status, terminal });
  return {
    turnId: projectionTurnId,
    traceId: projectionTraceId,
    status,
    running,
    terminal,
    source: terminalEvidence?.source ?? activeEvidence?.source ?? null,
    terminalEvidence,
    finalResponse: finalText ? { text: finalText, status, traceId: projectionTraceId, valuesPrinted: false } : null,
    assistantText: finalText,
    lastProjectedSeq: traceLastSeq(trace),
    sourceRunId: agentRun?.runId ?? lastEvent?.runId ?? lastEvent?.payload?.runId ?? null,
    sourceCommandId: agentRun?.commandId ?? lastEvent?.commandId ?? lastEvent?.payload?.commandId ?? null,
    eventCount: normalizedEventCount(trace),
    updatedAt: trace?.updatedAt ?? result?.updatedAt ?? session?.updatedAt ?? null,
    timing,
    startedAt: timing.startedAt,
    lastEventAt: timing.lastEventAt,
    finishedAt: timing.finishedAt,
    durationMs: timing.durationMs,
    valuesRedacted: true
  };
}

export function createWorkbenchTurnTimingProjection({ result = null, session = null, trace = null, status = null, terminal = null } = {}) {
  const events = Array.isArray(trace?.events) ? trace.events : [];
  const firstEvent = events[0] ?? null;
  const lastEvent = traceLastEvent(trace);
  const terminalEvent = terminalTraceEventForTiming(events);
  // Session lifecycle timing is not turn timing authority. Mixing session
  // startedAt/createdAt with turn terminal duration makes the running timer
  // jump backwards when the turn completes.
  const directTiming = objectValue(result?.timing) ?? objectValue(trace?.timing) ?? null;
  const agentRun = objectValue(result?.agentRun ?? trace?.agentRun);
  const traceSummary = objectValue(result?.traceSummary ?? trace?.traceSummary);
  const sessionSnapshot = objectValue(session?.session);
  const normalizedStatus = normalizeWorkbenchStatus(status ?? result?.status ?? trace?.status ?? session?.status);
  const isTerminal = typeof terminal === "boolean" ? terminal : TERMINAL_STATUSES.has(normalizedStatus) && !RUNNING_STATUSES.has(normalizedStatus);
  const startedAt = firstTimestamp(
    result?.startedAt,
    directTiming?.startedAt,
    result?.traceStartedAt,
    trace?.startedAt,
    traceSummary?.startedAt,
    firstEvent?.startedAt,
    firstEvent?.createdAt,
    firstEvent?.occurredAt
  );
  const lastEventAt = firstTimestamp(
    result?.lastEventAt,
    directTiming?.lastEventAt,
    trace?.lastEventAt,
    traceSummary?.lastEventAt,
    lastEvent?.lastEventAt,
    lastEvent?.updatedAt,
    lastEvent?.createdAt,
    lastEvent?.occurredAt,
    trace?.updatedAt,
    result?.updatedAt
  );
  const finishedAt = isTerminal ? firstTimestamp(
    result?.finishedAt,
    directTiming?.finishedAt,
    trace?.finishedAt,
    traceSummary?.finishedAt,
    trace?.terminalEvidence?.updatedAt,
    terminalEvent?.updatedAt,
    terminalEvent?.createdAt,
    terminalEvent?.occurredAt,
    result?.updatedAt,
    trace?.updatedAt,
    lastEventAt
  ) : null;
  const durationMs = durationValue(
    result?.durationMs,
    directTiming?.durationMs,
    trace?.durationMs,
    traceSummary?.durationMs,
    trace?.elapsedMs,
    result?.elapsedMs,
    elapsedBetween(startedAt, finishedAt ?? lastEventAt)
  );
  return {
    startedAt,
    lastEventAt,
    finishedAt,
    durationMs,
    valuesRedacted: true
  };
}

export function projectionDiagnostics({ traceId = null, projection = null, result = null, trace = null, refreshError = null } = {}) {
  const turn = projection ?? createWorkbenchTurnProjection({ traceId, result, trace });
  const sealedCompleted = turn.terminal === true && turn.status === "completed" && Boolean(turn.finalResponse?.text);
  const source = sealedCompleted ? null : projectionDiagnosticSource(trace);
  const rawBlocker = sealedCompleted ? null : refreshError ?? source?.blocker ?? trace?.blocker ?? result?.blocker ?? result?.error ?? null;
  const retryingProviderInterruption = !sealedCompleted && turn.running === true ? retryableProviderInterruptionEvidence(rawBlocker) : null;
  const blocker = retryingProviderInterruption ? null : rawBlocker;
  const hasProjectionInput = hasTraceProjection(trace) || Boolean(result || result?.agentRun);
  const sourceStatus = sealedCompleted ? null : normalizeProjectionStatus(source?.projectionStatus ?? trace?.projectionStatus);
  const effectiveSourceStatus = retryingProviderInterruption && sourceStatus === "blocked" ? "projecting" : sourceStatus;
  const status = sealedCompleted ? "caught-up" : effectiveSourceStatus ?? (blocker ? "blocked" : turn.terminal ? "caught-up" : hasProjectionInput ? "projecting" : "unknown");
  const diagnostic = blocker ? diagnosticBlocker(blocker) : null;
  const sourceHealth = sealedCompleted ? null : normalizeProjectionHealth(source?.projectionHealth ?? trace?.projectionHealth);
  const effectiveSourceHealth = retryingProviderInterruption && (sourceHealth === "degraded" || sourceHealth === "unavailable" || sourceHealth === "stalled") ? "projecting" : sourceHealth;
  const projectionHealth = sealedCompleted ? "healthy" : effectiveSourceHealth
    ?? projectionHealthFor({ status, turn, hasProjectionInput, blocker: diagnostic });
  const staleMs = sealedCompleted ? null : projectionStaleMs(source?.staleMs ?? trace?.staleMs, turn.updatedAt ?? trace?.updatedAt ?? result?.updatedAt);
  return {
    projectionStatus: status,
    projectionHealth,
    lastProjectedSeq: turn.lastProjectedSeq ?? null,
    sourceRunId: turn.sourceRunId ?? null,
    sourceCommandId: turn.sourceCommandId ?? null,
    staleMs,
    blocker: diagnostic,
    retryingProviderInterruption,
    updatedAt: turn.updatedAt ?? null,
    valuesRedacted: true
  };
}

export function durableTraceStatus(events = []) {
  const terminal = terminalTraceEventEvidence(events);
  if (terminal) return terminal.status;
  const activeStatus = activeTraceEventStatus(events);
  return activeStatus ?? (events.length > 0 ? "running" : "missing");
}

export function traceTerminalEvidence(trace = null) {
  const direct = objectValue(trace?.terminalEvidence);
  if (direct) {
    const status = terminalStatusFromValue(direct.status ?? direct.terminalStatus ?? trace?.status) ?? "completed";
    if (status !== "completed" && retryableProviderInterruptionEvidence(direct, trace)) return null;
    return { source: "trace-terminal-evidence", status, evidence: direct, valuesRedacted: true };
  }
  const finalResponse = objectValue(trace?.finalResponse);
  if (finalResponse) {
    const status = terminalStatusFromValue(finalResponse.status ?? trace?.status) ?? "completed";
    return { source: "trace-final-response", status, evidence: { textPresent: Boolean(projectionText(finalResponse)), valuesRedacted: true }, valuesRedacted: true };
  }
  const events = Array.isArray(trace?.events) ? trace.events : [];
  return terminalTraceEventEvidence(events);
}

export function normalizeWorkbenchStatus(value) {
  const text = String(value ?? "").trim().toLowerCase().replace(/_/gu, "-");
  if (text === "cancelled") return "canceled";
  return text || "unknown";
}

function terminalTurnEvidence({ result = null, traceTerminal = null } = {}) {
  const resultStatus = terminalStatusFromValue(
    result?.terminalStatus
    ?? result?.agentRun?.terminalStatus
    ?? result?.agentRun?.commandState
    ?? result?.agentRun?.status
    ?? result?.agentRun?.runStatus
  );
  if (resultStatus) {
    if (resultStatus !== "completed" && retryableProviderInterruptionEvidence(result, result?.agentRun, result?.providerTrace, traceTerminal?.evidence)) return null;
    return { source: "result", status: resultStatus, finalResponse: traceTerminal?.finalResponse ?? null, valuesRedacted: true };
  }
  const statusOnly = terminalStatusFromValue(result?.status);
  if (statusOnly && resultHasTerminalAuthority(result, traceTerminal)) {
    if (statusOnly !== "completed" && retryableProviderInterruptionEvidence(result, result?.agentRun, result?.providerTrace, traceTerminal?.evidence)) return null;
    return { source: "result", status: statusOnly, finalResponse: traceTerminal?.finalResponse ?? null, valuesRedacted: true };
  }
  if (traceTerminal && retryableProviderInterruptionEvidence(traceTerminal.evidence)) return null;
  return traceTerminal;
}

function resultHasTerminalAuthority(result = null, traceTerminal = null) {
  if (!result || typeof result !== "object") return false;
  if (traceTerminal) return true;
  if (result.terminal === true || result.sealed === true) return true;
  if (result.error || result.blocker) return true;
  if (firstTimestamp(result.finishedAt, result.completedAt, result.endedAt)) return true;
  const finalText = projectionText(result.finalResponse, result.assistantText, result.reply, result.text, result.summary);
  return Boolean(finalText && terminalStatusFromValue(result.status));
}

function activeTurnEvidence({ result = null, session = null, trace = null } = {}) {
  const retryEvidence = retryableProviderInterruptionEvidence(result, result?.agentRun, result?.providerTrace, trace);
  if (retryEvidence) return { source: "provider-retry", status: "retrying", evidence: retryEvidence, valuesRedacted: true };
  const resultStatus = firstStatus([
    result?.status,
    result?.agentRun?.status,
    result?.agentRun?.commandState,
    result?.agentRun?.runStatus
  ], RUNNING_STATUSES);
  if (resultStatus) return { source: "result", status: normalizeActiveStatus(resultStatus), valuesRedacted: true };
  const traceStatus = firstStatus([trace?.status, trace?.traceStatus], RUNNING_STATUSES);
  if (traceStatus) return { source: "trace", status: normalizeActiveStatus(traceStatus), valuesRedacted: true };
  if (hasTraceProjection(trace)) return { source: "trace-events", status: "running", valuesRedacted: true };
  const sessionStatus = firstStatus([session?.status, session?.session?.sessionStatus], RUNNING_STATUSES);
  if (sessionStatus) return { source: "session", status: normalizeActiveStatus(sessionStatus), valuesRedacted: true };
  return null;
}

function firstStatus(values, accepted) {
  for (const value of values) {
    const status = normalizeWorkbenchStatus(value);
    if (accepted.has(status)) return status;
  }
  return null;
}

function terminalStatusFromValue(value) {
  const status = normalizeWorkbenchStatus(value);
  return TERMINAL_STATUSES.has(status) && !RUNNING_STATUSES.has(status) ? status : null;
}

function normalizeActiveStatus(status) {
  return status === "active" || status === "busy" || status === "processing" || status === "creating" ? "running" : status;
}

function terminalTraceEventEvidence(events = []) {
  const finalResponse = terminalAssistantEventFinalResponse(events);
  for (let index = events.length - 1; index >= 0; index -= 1) {
    const event = events[index];
    if (!event || typeof event !== "object") continue;
    if (retryableProviderInterruptionEvidence(event)) continue;
    const terminal = event.terminal === true || event.final === true || event.replyAuthority === true;
    if (!terminal) continue;
    const status = terminalStatusFromValue(event.status ?? event.terminalStatus) ?? (event.error || event.errorCode ? "failed" : "completed");
    return {
      source: "trace-terminal-event",
      status,
      seq: eventSeq(event, index),
      eventType: textValue(event.type ?? event.label) || null,
      finalResponse,
      evidence: finalResponse ? { textPresent: true, source: "trace-terminal-assistant-event", valuesRedacted: true } : null,
      valuesRedacted: true
    };
  }
  return null;
}

function terminalAssistantEventFinalResponse(events = []) {
  for (let index = events.length - 1; index >= 0; index -= 1) {
    const event = events[index];
    if (!event || typeof event !== "object") continue;
    if (!isAssistantTraceEvent(event)) continue;
    if (!(event.terminal === true || event.final === true || event.replyAuthority === true)) continue;
    const text = projectionText(event.finalResponse, event.text, event.content, event.message, event.summary, event.payload?.text, event.payload?.content, event.payload?.message);
    if (!text) continue;
    const status = terminalStatusFromValue(event.status ?? event.terminalStatus ?? event.payload?.terminalStatus) ?? "completed";
    return {
      text,
      status,
      traceId: textValue(event.traceId) || null,
      seq: eventSeq(event, index),
      eventType: textValue(event.type ?? event.label) || null,
      valuesPrinted: false
    };
  }
  return null;
}

function isAssistantTraceEvent(event = {}) {
  const type = String(event.type ?? event.eventType ?? "").trim().toLowerCase();
  if (type === "assistant" || type === "assistant_message") return true;
  return /assistant:message|assistant_message/u.test(String(event.label ?? "").toLowerCase());
}

function activeTraceEventStatus(events = []) {
  for (const event of [...events].reverse()) {
    if (retryableProviderInterruptionEvidence(event)) return "retrying";
    const status = normalizeWorkbenchStatus(event?.status ?? event?.type);
    if (RUNNING_STATUSES.has(status)) return normalizeActiveStatus(status);
  }
  return null;
}

function retryableProviderInterruptionEvidence(...values) {
  for (const value of values) {
    const direct = retryableProviderInterruptionRecord(value);
    if (direct) return direct;
  }
  return null;
}

const RETRYABLE_AGENTRUN_TRANSPORT_FAILURE_KINDS = new Set([
  "agentrun-connect-failed",
  "agentrun-proxy-exec-failed",
  "agentrun-manager-fetch-failed",
  "agentrun-timeout"
]);

function isRetryableAgentRunTransportFailureKind(kind) {
  return RETRYABLE_AGENTRUN_TRANSPORT_FAILURE_KINDS.has(normalizedProjectionFailureKind(kind));
}

function retryableProviderInterruptionRecord(value) {
  const record = objectValue(value);
  if (!record) return null;
  const kind = normalizedProjectionFailureKind(record.failureKind ?? record.errorCode ?? record.code ?? record.name);
  if ((kind === "provider-stream-disconnected" || kind === "provider-unavailable" || isRetryableAgentRunTransportFailureKind(kind)) && record.willRetry === true) {
    return {
      failureKind: kind,
      willRetry: true,
      retryAttempt: numberOrNull(record.retryAttempt),
      retryMax: numberOrNull(record.retryMax),
      retryDelayMs: numberOrNull(record.retryDelayMs),
      nextRetryAt: textValue(record.nextRetryAt) || null,
      runId: textValue(record.runId) || null,
      commandId: textValue(record.commandId) || null,
      valuesRedacted: true
    };
  }
  for (const nested of [record.payload, record.providerTrace, record.agentRun, record.error, record.blocker, record.terminalEvidence]) {
    const found = retryableProviderInterruptionRecord(nested);
    if (found) return found;
  }
  return null;
}

function normalizedProjectionFailureKind(value) {
  const kind = normalizeWorkbenchStatus(value);
  if (kind === "agentrun-unreachable") return "agentrun-connect-failed";
  return kind;
}

function hasTraceProjection(trace) {
  return Boolean(trace && trace.status !== "missing" && (normalizedEventCount(trace) > 0 || traceLastEvent(trace)));
}

function normalizedEventCount(trace) {
  const events = Array.isArray(trace?.events) ? trace.events : [];
  const count = Number(trace?.eventCount ?? events.length);
  return Number.isFinite(count) && count >= 0 ? Math.trunc(count) : events.length;
}

function traceLastSeq(trace) {
  const events = Array.isArray(trace?.events) ? trace.events : [];
  const lastEvent = traceLastEvent(trace);
  const indexedMax = events.reduce((max, event, index) => Math.max(max, eventSeq(event, index)), 0);
  return Math.max(indexedMax, lastEvent ? eventSeq(lastEvent, Math.max(0, events.length - 1)) : 0) || null;
}

function traceLastEvent(trace) {
  const events = Array.isArray(trace?.events) ? trace.events : [];
  return trace?.lastEvent ?? events.at(-1) ?? null;
}

function terminalTraceEventForTiming(events = []) {
  for (let index = events.length - 1; index >= 0; index -= 1) {
    const event = events[index];
    if (!event || typeof event !== "object") continue;
    if (event.terminal === true || event.final === true || event.replyAuthority === true || terminalStatusFromValue(event.status ?? event.terminalStatus)) return event;
  }
  return null;
}

function firstTimestamp(...values) {
  for (const value of values) {
    const ms = Date.parse(String(value ?? ""));
    if (Number.isFinite(ms)) return new Date(ms).toISOString();
  }
  return null;
}

function durationValue(...values) {
  let max = null;
  for (const value of values) {
    const number = Number(value);
    if (!Number.isFinite(number) || number < 0) continue;
    const duration = Math.trunc(number);
    max = max === null ? duration : Math.max(max, duration);
  }
  return max;
}

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

function projectionDiagnosticSource(trace) {
  const direct = objectValue(trace?.projection);
  if (direct) return direct;
  if (trace?.projectionHealth || trace?.projectionStatus || trace?.blocker) return trace;
  return null;
}

function normalizeProjectionStatus(value) {
  const text = String(value ?? "").trim().toLowerCase().replace(/_/gu, "-");
  return ["caught-up", "projecting", "blocked", "stalled", "unknown"].includes(text) ? text : null;
}

function normalizeProjectionHealth(value) {
  const text = String(value ?? "").trim().toLowerCase().replace(/_/gu, "-");
  return ["caught-up", "projecting", "degraded", "stalled", "unavailable", "unknown"].includes(text) ? text : null;
}

function projectionHealthFor({ status, turn, hasProjectionInput, blocker }) {
  if (blocker?.code === "projection_store_unavailable") return "unavailable";
  if (blocker) return status === "stalled" ? "stalled" : "degraded";
  if (turn.terminal || status === "caught-up") return "caught-up";
  if (status === "stalled") return "stalled";
  if (status === "projecting" || hasProjectionInput) return "projecting";
  return "unknown";
}

function projectionStaleMs(value, updatedAt) {
  const direct = Number(value);
  if (Number.isFinite(direct) && direct >= 0) return Math.trunc(direct);
  const updatedAtMs = Date.parse(String(updatedAt ?? ""));
  if (!Number.isFinite(updatedAtMs)) return null;
  return Math.max(0, Date.now() - updatedAtMs);
}

function eventSeq(event, index) {
  const seq = Number(event?.seq);
  return Number.isFinite(seq) && seq > 0 ? Math.trunc(seq) : index + 1;
}

function projectionText(...values) {
  for (const value of values) {
    if (value && typeof value === "object") {
      const nested = messageAuthorityTextValue(value.text ?? value.content ?? value.message ?? value.summary ?? value.preview ?? value.title);
      if (nested) return nested;
      continue;
    }
    const text = messageAuthorityTextValue(value);
    if (text) return text;
  }
  return null;
}

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

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

function numberOrNull(value) {
  const number = Number(value);
  return Number.isFinite(number) ? number : null;
}

function messageAuthorityTextValue(value) {
  const text = String(value ?? "").replace(/\r\n?/gu, "\n");
  if (!text.trim() || text.trim() === "[object Object]") return "";
  return text;
}

function diagnosticBlocker(value = {}) {
  if (!value || typeof value !== "object") return { code: "projection_blocked", summary: String(value), valuesRedacted: true };
  return {
    code: String(value.code ?? value.errorCode ?? "projection_blocked"),
    layer: textValue(value.layer ?? value.source) || null,
    category: textValue(value.category ?? value.type) || null,
    summary: String(value.summary ?? value.userMessage ?? value.message ?? value.code ?? "Projection is blocked."),
    message: textValue(value.message ?? value.summary ?? value.userMessage) || null,
    userMessage: textValue(value.userMessage ?? value.zh) || null,
    retryable: typeof value.retryable === "boolean" ? value.retryable : null,
    runId: textValue(value.runId) || null,
    commandId: textValue(value.commandId) || null,
    timeoutMs: Number.isFinite(Number(value.timeoutMs)) ? Math.trunc(Number(value.timeoutMs)) : null,
    valuesRedacted: true
  };
}
