<script setup lang="ts">
// SPEC: PJ2026-01060505 Workbench Performance draft-2026-06-17-p0; PJ2026-0104010803 唯一投影 draft-2026-06-19-p2-terminal-sealed-final-response; PJ2026-010401 Web工作台 draft-2026-06-20-p1-view-local-timing-ticker.
// Confirms Workbench message visibility after Vue render and keeps sealed final responses separate from diagnostics.

import { computed, nextTick, onMounted, ref, watch } from "vue";
import type { ApiError, ChatMessage, ErrorDiagnostic } from "@/types";
import ApiErrorDiagnostic from "@/components/common/ApiErrorDiagnostic.vue";
import LoadingState from "@/components/common/LoadingState.vue";
import StatusBadge from "@/components/common/StatusBadge.vue";
import TraceTimeline from "@/components/agent/TraceTimeline.vue";
import CodeAgentStatusSummary from "@/components/workbench/CodeAgentStatusSummary.vue";
import MessageActions from "@/components/workbench/MessageActions.vue";
import MessageMarkdown from "@/components/workbench/MessageMarkdown.vue";
import MessageTraceDebugPanel from "@/components/workbench/MessageTraceDebugPanel.vue";
import { workbenchTraceTimelinePolicy } from "@/config/runtime";
import { useBottomFollowScroll } from "@/composables/useBottomFollowScroll";
import { useClipboard } from "@/composables/useClipboard";
import { useWorkbenchNowTicker } from "@/composables/useWorkbenchNowTicker";
import { useWorkbenchStore } from "@/stores/workbench";
import { acknowledgeWorkbenchVisibleAfterPaint } from "@/utils/workbench-performance";
import { traceIdentityText } from "./message-rendering";
import { traceLifecycleExpanded } from "./trace-lifecycle";

const workbench = useWorkbenchStore();
const clipboard = useClipboard();
const panelRef = ref<HTMLElement | null>(null);
const { following, keepBottomAfterUpdate, onScroll: onPanelScroll, scrollToBottom } = useBottomFollowScroll(panelRef, { threshold: 56 });
const detailMessageId = ref<string | null>(null);
const displayedDurationFloorMs = new Map<string, number>();
const sealedDurationMs = new Map<string, number>();
const visibleMessages = computed(() => workbench.sessionDetailLoading ? [] : workbench.activeMessages);
const hasRunningAgentMessages = computed(() => visibleMessages.value.some(isRunningMessage));
const { nowMs } = useWorkbenchNowTicker(hasRunningAgentMessages);
const detailMessage = computed(() => workbench.activeMessages.find((message) => message.id === detailMessageId.value) ?? null);
const traceTimelinePolicy = computed(() => workbenchTraceTimelinePolicy());
const scrollSignature = computed(() => workbench.activeMessages.map((message) => [
  message.id,
  message.status,
  visibleMessageText(message).length,
  message.updatedAt ?? "",
  message.runnerTrace?.eventCount ?? message.runnerTrace?.events?.length ?? 0,
  message.runnerTrace?.status ?? ""
].join(":")));

watch(scrollSignature, async () => {
  await keepBottomAfterUpdate();
  acknowledgeVisibleMessages();
});

onMounted(async () => {
  await nextTick();
  scrollToBottom();
  acknowledgeVisibleMessages();
});

watch(() => workbench.sessionDetailLoading, async () => {
  await nextTick();
  acknowledgeVisibleMessages();
});

watch(visibleMessages, (messages) => {
  const visibleKeys = new Set(messages.map(messageDurationFloorKey));
  for (const key of Array.from(displayedDurationFloorMs.keys())) {
    if (!visibleKeys.has(key)) displayedDurationFloorMs.delete(key);
  }
  for (const key of Array.from(sealedDurationMs.keys())) {
    if (!visibleKeys.has(key)) sealedDurationMs.delete(key);
  }
}, { immediate: true });

function openDetails(message: ChatMessage): void {
  detailMessageId.value = message.id;
}

function isRunningMessage(message: ChatMessage): boolean {
  return message.role === "agent" && ["pending", "running"].includes(String(message.status ?? "").toLowerCase());
}

function isTerminalAgentMessage(message: ChatMessage): boolean {
  if (message.role !== "agent") return false;
  return ["completed", "failed", "blocked", "timeout", "canceled", "cancelled", "stale", "thread-resume-failed"].includes(String(message.status ?? "").trim().toLowerCase().replace(/_/gu, "-"));
}

function isCompletedAgentMessage(message: ChatMessage): boolean {
  if (message.role !== "agent") return false;
  return [message.status, message.runnerTrace?.status].some((value) => String(value ?? "").trim().toLowerCase().replace(/_/gu, "-") === "completed");
}

function hasSealedCompletedText(message: ChatMessage): boolean {
  return isCompletedAgentMessage(message) && Boolean(visibleMessageText(message));
}

function visibleMessageText(message: ChatMessage): string {
  const text = String(message.text ?? "").trim();
  if (message.role === "agent" && !isTerminalAgentMessage(message)) return "";
  return text;
}

function showStatusBadge(message: ChatMessage): boolean {
  return message.role !== "agent" || !isRunningMessage(message);
}

function isAwaitingAgentBody(message: ChatMessage): boolean {
  return isRunningMessage(message) && !message.runnerTrace && !visibleMessageText(message);
}

function showMessageText(message: ChatMessage): boolean {
  return Boolean(visibleMessageText(message));
}

function messageDiagnosticText(message: ChatMessage): string | null {
  if (message.role === "agent") return projectionDiagnosticText(message);
  if (message.role !== "user" || String(message.text ?? "").trim()) return null;
  return "消息内容缺失：Workbench read model 未返回 user text。";
}

function messageHasDiagnostic(message: ChatMessage): boolean {
  if (hasSealedCompletedText(message)) return false;
  return Boolean(messageDiagnosticText(message) || messageApiError(message) || messageErrorDiagnostic(message));
}

function projectionDiagnosticText(message: ChatMessage): string | null {
  if (hasSealedCompletedText(message)) return null;
  const projection = messageProjection(message);
  const health = projection?.projectionHealth ?? message.projectionHealth ?? message.runnerTrace?.projectionHealth;
  if (!projection || health === "caught-up") return null;
  return firstNonEmptyString(projection.blocker?.userMessage, projection.blocker?.message, projection.blocker?.summary, projection.blocker?.code ? `状态更新异常：${projection.blocker.code}` : null);
}

function messageProjection(message: ChatMessage): ChatMessage["projection"] {
  return message.projection ?? message.runnerTrace?.projection ?? null;
}

function messageApiError(message: ChatMessage): ApiError | null {
  if (hasSealedCompletedText(message)) return null;
  const projection = messageProjection(message);
  const blocker = recordValue(projection?.blocker ?? message.blocker ?? message.runnerTrace?.blocker);
  const error = recordValue(message.error);
  const apiError = recordValue(projection?.apiError ?? error?.apiError);
  const diagnostic = messageErrorDiagnostic(message);
  if (!projectionDiagnosticText(message) && !blocker && !error && !apiError && !diagnostic) return null;
  const source = apiError ?? error ?? blocker ?? {};
  const messageText = firstNonEmptyString(source.userMessage, source.message, blocker?.userMessage, blocker?.message, blocker?.summary, diagnostic?.code ? String(diagnostic.code) : null, messageDiagnosticText(message));
  return {
    ...source,
    message: messageText ?? "Workbench 诊断",
    userMessage: firstNonEmptyString(source.userMessage, blocker?.userMessage, messageText),
    code: firstValue(source.code, blocker?.code, diagnostic?.code),
    retryable: firstBooleanValue(source.retryable, blocker?.retryable, diagnostic?.retryable),
    layer: firstNonEmptyString(source.layer, blocker?.layer, diagnostic?.layer),
    category: firstNonEmptyString(source.category, blocker?.category, diagnostic?.category),
    route: firstNonEmptyString(source.route, blocker?.route, diagnostic?.route),
    traceId: firstNonEmptyString(source.traceId, blocker?.traceId, diagnostic?.traceId, message.traceId, message.runnerTrace?.traceId),
    requestId: firstNonEmptyString(source.requestId, blocker?.requestId, diagnostic?.requestId),
    source: firstNonEmptyString(source.source, blocker?.source, diagnostic?.source),
    diagnostic,
    valuesPrinted: source.valuesPrinted === true || blocker?.valuesPrinted === true
  } as ApiError;
}

function messageErrorDiagnostic(message: ChatMessage): ErrorDiagnostic | null {
  if (hasSealedCompletedText(message)) return null;
  const projection = messageProjection(message);
  const blocker = recordValue(projection?.blocker ?? message.blocker ?? message.runnerTrace?.blocker);
  const error = recordValue(message.error);
  const apiError = recordValue(projection?.apiError ?? error?.apiError);
  if (!projectionDiagnosticText(message) && !blocker && !error && !apiError && !projection?.diagnostic) return null;
  const nested = normalizeErrorDiagnostic(projection?.diagnostic, blocker?.diagnostic, error?.diagnostic, apiError?.diagnostic);
  if (nested) return nested;
  const code = firstValue(error?.code, apiError?.code, blocker?.code);
  const traceId = firstNonEmptyString(error?.traceId, apiError?.traceId, blocker?.traceId, message.traceId, message.runnerTrace?.traceId);
  const requestId = firstNonEmptyString(error?.requestId, apiError?.requestId, blocker?.requestId);
  const route = firstNonEmptyString(error?.route, apiError?.route, blocker?.route);
  const layer = firstNonEmptyString(error?.layer, apiError?.layer, blocker?.layer);
  const category = firstNonEmptyString(error?.category, apiError?.category, blocker?.category);
  const source = firstNonEmptyString(error?.source, apiError?.source, blocker?.source);
  const httpStatus = firstNumber(error?.httpStatus, apiError?.httpStatus, blocker?.httpStatus, error?.providerStatus);
  if (!code && !traceId && !requestId && !route && !layer && !category && !source && httpStatus === null) return null;
  return { contractVersion: "hwlab-error-diagnostic-v1", traceId, requestId, route, layer, category, code, httpStatus, source, retryable: firstBooleanValue(error?.retryable, apiError?.retryable, blocker?.retryable), valuesPrinted: false };
}

function normalizeErrorDiagnostic(...values: unknown[]): ErrorDiagnostic | null {
  for (const value of values) {
    const record = recordValue(value);
    if (!record) continue;
    return {
      ...record,
      contractVersion: firstNonEmptyString(record.contractVersion, record.contract_version, "hwlab-error-diagnostic-v1"),
      traceId: firstNonEmptyString(record.traceId, record.trace_id, record.otelTraceId, record.otel_trace_id),
      requestId: firstNonEmptyString(record.requestId, record.request_id),
      serviceId: firstNonEmptyString(record.serviceId, record.service_id),
      route: firstNonEmptyString(record.route),
      layer: firstNonEmptyString(record.layer),
      category: firstNonEmptyString(record.category),
      code: firstValue(record.code),
      httpStatus: firstNumber(record.httpStatus, record.http_status),
      source: firstNonEmptyString(record.source),
      observedAt: firstNonEmptyString(record.observedAt, record.observed_at),
      retryable: firstBooleanValue(record.retryable),
      valuesPrinted: record.valuesPrinted === true
    };
  }
  return null;
}

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

function firstNonEmptyString(...values: unknown[]): string | null {
  for (const value of values) {
    if (typeof value !== "string") continue;
    const text = value.trim();
    if (text) return text;
  }
  return null;
}

function firstValue(...values: unknown[]): string | number | null {
  for (const value of values) {
    if (typeof value === "number" && Number.isFinite(value)) return value;
    if (typeof value === "string" && value.trim()) return value.trim();
  }
  return null;
}

function firstNumber(...values: unknown[]): number | null {
  for (const value of values) {
    if (typeof value === "number" && Number.isFinite(value)) return value;
    if (typeof value === "string" && value.trim() && Number.isFinite(Number(value))) return Number(value);
  }
  return null;
}

function firstBooleanValue(...values: unknown[]): boolean | null {
  for (const value of values) if (typeof value === "boolean") return value;
  return null;
}

function acknowledgeVisibleMessages(): void {
  acknowledgeWorkbenchVisibleAfterPaint({ messages: workbench.activeMessages, activeSessionId: workbench.activeSessionId, detailLoading: workbench.sessionDetailLoading });
}

function traceStorageKey(message: ChatMessage): string {
  return `hwlab.workbench.trace-open.${message.traceId ?? message.runnerTrace?.traceId ?? message.id}`;
}

function traceAutoExpanded(message: ChatMessage): boolean | null {
  return traceLifecycleExpanded(message, traceTimelinePolicy.value);
}

function traceForDisplay(message: ChatMessage): ChatMessage["runnerTrace"] {
  if (!message.runnerTrace) return message.runnerTrace;
  const status = firstNonEmptyString(message.status, message.runnerTrace.status);
  if (!status || status === message.runnerTrace.status) return message.runnerTrace;
  return { ...message.runnerTrace, status };
}

function messageDurationMeta(message: ChatMessage): { text: string; label: string } | null {
  if (message.role !== "agent") return null;
  const timing = messageTimingForDisplay(message);
  const rawDurationMs = isRunningMessage(message) ? durationSince(timing?.startedAt) : terminalMessageDurationMs(message, timing);
  const durationMs = isRunningMessage(message) ? monotonicDisplayDurationMs(message, rawDurationMs) : sealedDisplayDurationMs(message, rawDurationMs);
  if (durationMs === null) return null;
  const value = formatDuration(durationMs);
  return { text: `耗时 ${value}`, label: `总耗时：${value}` };
}

function monotonicDisplayDurationMs(message: ChatMessage, durationMs: number | null): number | null {
  if (durationMs === null) return null;
  const key = messageDurationFloorKey(message);
  const previous = displayedDurationFloorMs.get(key);
  const next = previous === undefined ? durationMs : Math.max(previous, durationMs);
  if (next !== previous) displayedDurationFloorMs.set(key, next);
  return next;
}

function sealedDisplayDurationMs(message: ChatMessage, durationMs: number | null): number | null {
  const key = messageDurationFloorKey(message);
  displayedDurationFloorMs.delete(key);
  const previous = sealedDurationMs.get(key);
  if (durationMs === null) return previous ?? null;
  const next = previous === undefined ? durationMs : Math.max(previous, durationMs);
  if (next !== previous) sealedDurationMs.set(key, next);
  return next;
}

function messageDurationFloorKey(message: ChatMessage): string {
  return firstNonEmptyString(message.id, message.runnerTrace?.traceId, message.traceId) ?? "unknown-agent-message";
}

function messageActivityMeta(message: ChatMessage): { text: string; label: string } | null {
  if (!isRunningMessage(message)) return null;
  const timing = messageTimingForDisplay(message);
  const ageMs = durationSince(timing?.lastEventAt);
  if (ageMs === null) return null;
  const value = formatDuration(ageMs) + "前";
  return { text: `最近 ${value}`, label: `最近事件：${value}` };
}

function messageTimingForDisplay(message: ChatMessage): ChatMessage["timing"] {
  // The main message card consumes only the durable message timing projection.
  // Trace timing belongs to TraceTimeline/details; letting trace hydration drive the
  // card timer makes running turns jump when a later trace snapshot carries an
  // older startedAt/lastEventAt window.
  return message.timing ?? null;
}

function terminalMessageDurationMs(message: ChatMessage, timing: ChatMessage["timing"]): number | null {
  const elapsed = elapsedTerminalDurationMs(message, timing);
  if (elapsed !== null && elapsed > 0) return elapsed;
  const recorded = sealedTerminalRecordedDurationMs(message, timing);
  if (recorded !== null) return recorded;
  return null;
}

function sealedTerminalRecordedDurationMs(message: ChatMessage, timing: ChatMessage["timing"]): number | null {
  const messageRecord = message as Record<string, unknown>;
  const trace = message.runnerTrace as Record<string, unknown> | null | undefined;
  const traceTiming = trace?.timing as Record<string, unknown> | null | undefined;
  const messageHasTerminalTimestamp = timestampMs(timing?.finishedAt ?? message.finishedAt ?? messageRecord.completedAt) !== null;
  const traceHasTerminalTimestamp = timestampMs(traceTiming?.finishedAt ?? trace?.finishedAt ?? traceTiming?.completedAt ?? trace?.completedAt) !== null;
  return maxPositiveDurationMs(
    messageHasTerminalTimestamp ? message.durationMs : null,
    messageHasTerminalTimestamp ? timing?.durationMs : null,
    traceHasTerminalTimestamp ? message.runnerTrace?.durationMs : null,
    traceHasTerminalTimestamp ? message.runnerTrace?.timing?.durationMs : null
  );
}

function elapsedTerminalDurationMs(message: ChatMessage, timing: ChatMessage["timing"]): number | null {
  const messageRecord = message as Record<string, unknown>;
  const trace = message.runnerTrace as Record<string, unknown> | null | undefined;
  const traceTiming = trace?.timing as Record<string, unknown> | null | undefined;
  const startedAt = timestampMs(timing?.startedAt ?? message.startedAt ?? traceTiming?.startedAt ?? trace?.startedAt);
  const endedAt = timestampMs(timing?.finishedAt ?? message.finishedAt ?? messageRecord.completedAt ?? traceTiming?.finishedAt ?? trace?.finishedAt ?? traceTiming?.completedAt ?? trace?.completedAt);
  if (startedAt === null || endedAt === null || endedAt < startedAt) return null;
  return Math.trunc(endedAt - startedAt);
}

function durationSince(timestamp: unknown): number | null {
  const startedAtMs = timestampMs(timestamp);
  return startedAtMs === null ? null : Math.max(0, nowMs.value - startedAtMs);
}

function timestampMs(value: unknown): number | null {
  if (typeof value !== "string") return null;
  const trimmed = value.trim();
  if (!trimmed) return null;
  const parsed = Date.parse(trimmed);
  return Number.isFinite(parsed) ? parsed : null;
}

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

function maxPositiveDurationMs(...values: unknown[]): number | null {
  let maximum: number | null = null;
  for (const value of values) {
    const duration = finiteDurationMs(value);
    if (duration !== null && duration > 0) maximum = maximum === null ? duration : Math.max(maximum, duration);
  }
  return maximum;
}

function formatDuration(ms: number): string {
  const seconds = ms > 0 && ms < 1000 ? 1 : Math.max(0, Math.floor(ms / 1000));
  if (seconds < 60) return `${seconds} 秒`;
  const minutes = Math.floor(seconds / 60);
  if (minutes < 60) return `${minutes} 分 ${seconds % 60} 秒`;
  const hours = Math.floor(minutes / 60);
  if (hours < 24) return `${hours} 小时 ${minutes % 60} 分`;
  const days = Math.floor(hours / 24);
  return `${days} 天 ${hours % 24} 小时`;
}
</script>

<template>
  <section id="conversation-list" ref="panelRef" class="conversation-panel" :data-following="following ? 'true' : 'false'" @scroll="onPanelScroll">
    <LoadingState v-if="workbench.sessionDetailLoading" class="conversation-detail-loading" label="加载中" />
        <article
          v-for="message in visibleMessages"
          :key="message.id"
          class="message-card"
          :data-role="message.role"
          :data-status="message.status"
          :data-session-id="message.sessionId || undefined"
          :data-message-id="message.id || undefined"
          :data-trace-id="message.traceId || message.runnerTrace?.traceId || undefined"
          :data-turn-id="message.turnId || message.traceId || message.runnerTrace?.traceId || undefined"
        >
      <header v-if="message.role !== 'user'">
        <div class="message-header-main">
          <strong>{{ message.title }}</strong>
          <span v-if="messageDurationMeta(message)" class="message-timing-meta message-duration-meta" :aria-label="messageDurationMeta(message)?.label" :title="messageDurationMeta(message)?.label">{{ messageDurationMeta(message)?.text }}</span>
        </div>
        <div class="message-header-actions">
          <span v-if="messageActivityMeta(message)" class="message-activity-meta" :aria-label="messageActivityMeta(message)?.label" :title="messageActivityMeta(message)?.label">{{ messageActivityMeta(message)?.text }}</span>
          <span v-if="isRunningMessage(message)" class="agent-running-indicator" aria-label="运行中" role="status"><span aria-hidden="true" /></span>
          <StatusBadge v-if="showStatusBadge(message)" :status="message.status" />
          <button v-if="message.role === 'agent'" class="message-detail-button" type="button" aria-label="运行详情" title="运行详情" @click="openDetails(message)">!</button>
        </div>
      </header>
      <TraceTimeline v-if="message.role === 'agent' && message.runnerTrace" :trace="traceForDisplay(message)" :auto-expanded="traceAutoExpanded(message)" :storage-key="traceStorageKey(message)" :sealed-final-text="hasSealedCompletedText(message) ? visibleMessageText(message) : ''" />
      <LoadingState v-if="isAwaitingAgentBody(message)" class="message-loading" label="思考中..." compact />
      <MessageMarkdown v-if="showMessageText(message)" class="message-text" :source="visibleMessageText(message)" />
      <ApiErrorDiagnostic v-if="messageHasDiagnostic(message)" class="message-diagnostic projection-diagnostic" :error="messageDiagnosticText(message)" :api-error="messageApiError(message)" :diagnostic="messageErrorDiagnostic(message)" compact />
    </article>
    <div v-if="!workbench.sessionDetailLoading && workbench.activeMessages.length === 0 && workbench.error" class="conversation-empty-hint conversation-error-hint">加载失败：{{ workbench.error }}</div>
    <div v-else-if="!workbench.sessionDetailLoading && workbench.activeMessages.length === 0" class="conversation-empty-hint">发起对话，或从左侧选择 session。</div>
    <div v-if="detailMessage" class="workbench-dialog-backdrop" role="presentation" @click.self="detailMessageId = null">
      <section id="message-detail-dialog" class="workbench-dialog wide" role="dialog" aria-modal="true" aria-labelledby="message-detail-title">
        <header>
          <div class="dialog-title-stack">
            <h2 id="message-detail-title">消息详情</h2>
            <p>{{ detailMessage.traceId || detailMessage.runnerTrace?.traceId || 'trace 未观测' }}</p>
          </div>
          <button type="button" class="dialog-close" aria-label="关闭" @click="detailMessageId = null">x</button>
        </header>
        <CodeAgentStatusSummary :message="detailMessage" />
        <ApiErrorDiagnostic v-if="messageHasDiagnostic(detailMessage)" class="message-detail-diagnostic" title="诊断" :error="messageDiagnosticText(detailMessage)" :api-error="messageApiError(detailMessage)" :diagnostic="messageErrorDiagnostic(detailMessage)" compact />
        <MessageTraceDebugPanel :message="detailMessage" />
        <MessageActions
          :message="detailMessage"
          @cancel="workbench.cancelAgentMessage"
          @retry="workbench.retryAgentMessage"
          @copy-trace="(target: ChatMessage) => clipboard.copy(traceIdentityText(target))"
        />
      </section>
    </div>
  </section>
</template>
