<script setup lang="ts">
import { computed, ref, watch } from "vue";
import type { RunnerTrace } from "@/types";
import LoadingState from "@/components/common/LoadingState.vue";
import MessageMarkdown from "@/components/workbench/MessageMarkdown.vue";
import { useBottomFollowScroll } from "@/composables/useBottomFollowScroll";
import { formatDisplayClock } from "@/config/runtime";
import { traceDisplayRows, type TraceEventRow } from "../../../../../tools/src/hwlab-cli/trace-renderer.ts";

const props = defineProps<{ trace?: RunnerTrace | null; defaultExpanded?: boolean; storageKey?: string; autoExpanded?: boolean | null; sealedFinalText?: string }>();

const listRef = ref<HTMLOListElement | null>(null);
const expanded = ref(false);
const { following, keepBottomAfterUpdate, onScroll, scrollToBottom } = useBottomFollowScroll(listRef, { threshold: 24 });

const events = computed(() => Array.isArray(props.trace?.events) ? props.trace.events : []);
const eventCount = computed(() => props.trace?.eventCount ?? events.value.length);
const rawReadableRows = computed(() => traceDisplayRows(traceRecord(props.trace), events.value as Record<string, unknown>[], { formatClock: formatDisplayClock }));
const readableRows = computed(() => filterReadableRows(rawReadableRows.value, props.sealedFinalText));
const projectionDiagnosticLabel = computed(() => {
  const projection = props.trace?.projection ?? null;
  const health = projection?.projectionHealth ?? props.trace?.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);
});
const emptyTraceLabel = computed(() => {
  if (projectionDiagnosticLabel.value) return projectionDiagnosticLabel.value;
  if (isTerminalTraceStatus(props.trace?.status)) return terminalTraceEmptyLabel(props.trace?.status);
  if (events.value.length > 0 || (props.trace?.fullTraceLoaded === true && eventCount.value > 0)) return "暂无可读 Trace";
  return props.trace?.fullTraceLoaded === true ? "思考中..." : "加载中...";
});
const toolPreviewLimit = 140;
let programmaticToggleTarget: boolean | null = null;

watch(() => [eventCount.value, props.trace?.status, readableRows.value.length, expanded.value], () => {
  if (!expanded.value) return;
  void keepBottomAfterUpdate();
});

watch(() => [props.trace?.traceId, props.storageKey, props.defaultExpanded, props.autoExpanded] as const, () => {
  setExpandedProgrammatically(readStoredExpanded() ?? props.autoExpanded ?? props.defaultExpanded ?? false);
}, { immediate: true });

function rowKey(row: TraceEventRow, index: number): string {
  return `${row.rowId}-${row.seq ?? "no-seq"}-${index}`;
}

function onDisclosureToggle(event: Event): void {
  const target = event.currentTarget;
  if (!(target instanceof HTMLDetailsElement)) return;
  expanded.value = target.open;
  if (target.open) scrollToBottom();
  if (programmaticToggleTarget === target.open) {
    programmaticToggleTarget = null;
    return;
  }
  programmaticToggleTarget = null;
  writeStoredExpanded(target.open);
}

function setExpandedProgrammatically(value: boolean): void {
  if (expanded.value === value) return;
  programmaticToggleTarget = value;
  expanded.value = value;
}

function readStoredExpanded(): boolean | null {
  if (!props.storageKey) return null;
  try {
    const value = window.localStorage.getItem(props.storageKey);
    if (value === "1") return true;
    if (value === "0") return false;
  } catch { /* ignore */ }
  return null;
}

function writeStoredExpanded(value: boolean): void {
  if (!props.storageKey) return;
  try { window.localStorage.setItem(props.storageKey, value ? "1" : "0"); } catch { /* ignore */ }
}

function rowIsTool(row: TraceEventRow): boolean {
  return row.rowId.startsWith("tool:") || /commandExecution/u.test(row.header);
}

function filterReadableRows(rows: TraceEventRow[], sealedFinalText: string | undefined): TraceEventRow[] {
  const finalText = normalizeComparableText(sealedFinalText);
  if (!finalText || finalText.length < 24) return rows;
  return rows.filter((row) => !assistantRowDuplicatesFinal(row, finalText));
}

function assistantRowDuplicatesFinal(row: TraceEventRow, finalText: string): boolean {
  if (rowIsTool(row)) return false;
  const rowTextRaw = [row.header, row.preview, row.body].filter(Boolean).join("\n");
  if (!/(?:助手消息|assistant\s+message|assistant)/iu.test(rowTextRaw)) return false;
  const rowText = normalizeComparableText(rowTextRaw);
  if (!rowText || rowText.length < 24) return true;
  return finalText.includes(rowText) || rowText.includes(finalText) || longestSharedSubstringLength(finalText, rowText) >= Math.min(80, Math.floor(Math.min(finalText.length, rowText.length) * 0.7));
}

function normalizeComparableText(value: unknown): string {
  return String(value ?? "").replace(/\s+/gu, " ").trim();
}

function longestSharedSubstringLength(a: string, b: string): number {
  const left = a.length <= b.length ? a : b;
  const right = a.length <= b.length ? b : a;
  const max = Math.min(left.length, 320);
  let best = 0;
  for (let start = 0; start < max; start += 1) {
    for (let end = Math.min(max, start + 180); end > start + best; end -= 1) {
      if (right.includes(left.slice(start, end))) {
        best = end - start;
        break;
      }
    }
  }
  return best;
}

function toolCommandPreview(row: TraceEventRow): string | null {
  if (!rowIsTool(row)) return null;
  const preview = singleLinePreview(row.preview);
  if (!preview) return null;
  return preview.length > toolPreviewLimit ? `${preview.slice(0, toolPreviewLimit - 3).trimEnd()}...` : preview;
}

function singleLinePreview(value: unknown): string | null {
  const preview = String(value ?? "").replace(/\u0000/gu, "").replace(/\r\n|\r|\n/gu, " ").replace(/\s+/gu, " ").trim();
  return preview || null;
}

function traceRecord(trace: RunnerTrace | null | undefined): Record<string, unknown> {
  return trace && typeof trace === "object" ? trace as Record<string, unknown> : {};
}

function isTerminalTraceStatus(status: unknown): boolean {
  return ["completed", "failed", "blocked", "timeout", "canceled", "cancelled"].includes(String(status ?? "").toLowerCase());
}

function terminalTraceEmptyLabel(status: unknown): string {
  switch (String(status ?? "").toLowerCase()) {
    case "completed": return "运行已完成，暂无可读 Trace";
    case "failed":
    case "blocked": return "运行失败，暂无可读 Trace";
    case "timeout": return "运行超时，暂无可读 Trace";
    case "canceled":
    case "cancelled": return "运行已取消，暂无可读 Trace";
    default: return "运行已结束，暂无可读 Trace";
  }
}

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

</script>

<template>
  <section v-if="trace" class="trace-timeline" :data-status="trace.status" :data-projection-health="trace.projection?.projectionHealth || trace.projectionHealth || undefined" :data-following="following ? 'true' : 'false'">
    <details class="trace-disclosure" :open="expanded" @toggle="onDisclosureToggle">
      <summary class="trace-disclosure-summary" aria-label="折叠运行记录">
        <span class="trace-disclosure-caret" aria-hidden="true" />
        <span class="trace-disclosure-label">运行记录</span>
      </summary>
      <div class="trace-disclosure-body">
        <ol ref="listRef" @scroll="onScroll">
          <li v-for="(row, index) in readableRows" :key="rowKey(row, index)" class="trace-render-row" :data-event-status="row.tone" :data-terminal="row.terminal ? 'true' : 'false'" :data-row-kind="rowIsTool(row) ? 'tool' : 'message'" :data-row-id="row.rowId">
            <details v-if="rowIsTool(row)" class="trace-tool-details">
              <summary>
                <code>{{ row.header }}</code>
                <span v-if="toolCommandPreview(row)" class="trace-tool-command-preview" :title="row.preview ?? undefined">{{ toolCommandPreview(row) }}</span>
              </summary>
              <MessageMarkdown v-if="row.body && row.bodyFormat === 'markdown'" class="trace-row-body trace-row-markdown" :source="row.body" />
              <pre v-else-if="row.body" class="trace-row-body">{{ row.body }}</pre>
              <span v-else class="trace-row-body muted">已记录</span>
            </details>
            <template v-else>
              <code>{{ row.header }}</code>
              <MessageMarkdown v-if="row.body && row.bodyFormat === 'markdown'" class="trace-row-body trace-row-markdown" :source="row.body" />
              <pre v-else-if="row.body" class="trace-row-body">{{ row.body }}</pre>
              <span v-else class="trace-row-body muted">已记录</span>
            </template>
          </li>
        </ol>
        <LoadingState v-if="readableRows.length === 0" class="trace-empty" :label="emptyTraceLabel" compact />
      </div>
    </details>
  </section>
</template>
