import React from "react";
import { fmtDate } from "./time";

type AnyRecord = Record<string, any>;

const h = React.createElement;
const { useEffect, useRef } = React;

export type TraceItemKind = "message" | "system" | "error" | "ran" | "explored" | "edited" | "toolGroup";

export interface TraceEditStage {
  method: string;
  status?: string;
  at?: string;
}

export interface TraceDiffFile {
  path: string;
  status: string;
  additions?: number;
  deletions?: number;
}

export interface TraceDiffLine {
  text: string;
  kind: "meta" | "hunk" | "add" | "del" | "file" | "note" | "context";
  path?: string;
  status?: string;
}

export interface TraceEditObservation {
  status: string;
  summary: string;
  files: TraceDiffFile[];
  stages: TraceEditStage[];
  lines: TraceDiffLine[];
  addedLines: number;
  removedLines: number;
  rawText: string;
}

export interface TraceItem {
  seq: number;
  at?: string;
  durationMs?: number;
  kind: TraceItemKind | string;
  title?: string;
  status?: string;
  commandPreview?: string;
  commandOmittedLines?: number;
  bodyPreview?: string;
  bodyOmittedLines?: number;
  stdoutPreview?: string;
  stdoutOmittedLines?: number;
  stderrPreview?: string;
  stderrOmittedLines?: number;
  rawSeqs?: any[];
  fullPrompt?: string;
  fullPromptLines?: number;
  fullPromptChars?: number;
  foldedReferencePrompt?: boolean;
  items?: TraceItem[];
  counts?: AnyRecord;
  digest?: AnyRecord;
  editObservation?: TraceEditObservation;
}

export interface TracePort<Input = any> {
  source: "codex" | "opencode" | string;
  toTrace(input: Input): TraceItem[];
}

export function traceFromPort<Input>(port: TracePort<Input>, input: Input): TraceItem[] {
  return normalizeTraceItems(port.toTrace(input));
}

function fmtDuration(ms: any): string {
  const value = Number(ms);
  if (!Number.isFinite(value) || value < 0) return "--";
  const totalSeconds = Math.floor(value / 1000);
  const hours = Math.floor(totalSeconds / 3600);
  const minutes = Math.floor((totalSeconds % 3600) / 60);
  const seconds = totalSeconds % 60;
  if (hours > 0) return `${hours}h ${String(minutes).padStart(2, "0")}m`;
  if (minutes > 0) return `${minutes}m ${String(seconds).padStart(2, "0")}s`;
  return `${seconds}s`;
}

function finiteMs(value: any): number | null {
  const number = Number(value);
  return Number.isFinite(number) && number >= 0 ? number : null;
}

function shortText(value: any, max = 180): string {
  const text = String(value || "").replace(/\s+/gu, " ").trim();
  return text.length > max ? `${text.slice(0, max - 1)}…` : text;
}

function promptLineCount(text: string): number {
  if (!text) return 0;
  return text.split(/\r?\n/u).length;
}

function traceKindLabel(kind: string): string {
  const labels: Record<string, string> = {
    ran: "Ran",
    explored: "Explored",
    edited: "Edited",
    toolGroup: "Tool calls",
    plan: "Plan",
    message: "Message",
    system: "System",
    error: "Error",
  };
  return labels[kind] || "Message";
}

function omittedLabel(lines: any): string {
  const count = Number(lines || 0);
  return Number.isFinite(count) && count > 0 ? `… +${Math.floor(count)} lines` : "";
}

export function traceMaxSeq(trace: any[]): number {
  return (Array.isArray(trace) ? trace : []).reduce((max, item) => Math.max(max, Number(item?.seq ?? 0)), 0);
}

export function traceResumeSeq(trace: any[], overlapRows = 8): number {
  const seqs = Array.from(new Set((Array.isArray(trace) ? trace : [])
    .map((item) => Number(item?.seq ?? 0))
    .filter((seq) => Number.isFinite(seq) && seq > 0)))
    .sort((left, right) => left - right);
  if (seqs.length === 0) return 0;
  const anchor = seqs[Math.max(0, seqs.length - overlapRows)] ?? 0;
  return Math.max(0, anchor - 0.001);
}

function isToolTraceItem(item: any): boolean {
  return ["explored", "edited", "ran"].includes(String(item?.kind || ""));
}

function toolTraceCounts(items: any[]): AnyRecord {
  const counts = { read: 0, edit: 0, run: 0 };
  for (const item of items) {
    const kind = String(item?.kind || "");
    if (kind === "explored") counts.read += 1;
    else if (kind === "edited") counts.edit += 1;
    else if (kind === "ran") counts.run += 1;
  }
  return counts;
}

function toolTraceSummary(items: any[]): string {
  const counts = toolTraceCounts(items);
  return `${counts.read} read, ${counts.edit} edit, ${counts.run} run`;
}

function cleanTracePath(value: string): string {
  return value
    .replace(/^['"`([{<]+/u, "")
    .replace(/['"`)\]}>.,;:]+$/u, "")
    .replace(/:\d+(?::\d+)?$/u, "")
    .trim();
}

function extractTracePaths(text: string): string[] {
  const source = String(text || "");
  const matches = source.match(/(?:~|\.{1,2}|\/)?(?:[A-Za-z0-9_.@+-]+\/)+[A-Za-z0-9_.@+-]+|[A-Za-z0-9_.@+-]+\.(?:c|cc|cpp|h|hpp|js|jsx|ts|tsx|json|md|py|sh|toml|ya?ml|txt|log|lock)/gu) || [];
  const paths: string[] = [];
  for (const raw of matches) {
    const path = cleanTracePath(raw);
    if (path.length < 2 || path.includes("...")) continue;
    if (/^(http|https|status|method)$/iu.test(path)) continue;
    if (!paths.includes(path)) paths.push(path);
  }
  return paths;
}

function compactList(items: string[], limit = 4): string {
  if (items.length === 0) return "--";
  const shown = items.slice(0, limit).join(", ");
  return items.length > limit ? `${shown} +${items.length - limit}` : shown;
}

function joinTraceText(parts: string[]): string {
  let text = "";
  for (const part of parts) {
    if (part.length === 0) continue;
    if (text.length > 0 && !text.endsWith("\n") && !part.startsWith("\n")) text += "\n";
    text += part;
  }
  return text;
}

function traceTextLines(text: string): string[] {
  const normalized = String(text || "").replace(/\r\n/gu, "\n").replace(/\r/gu, "\n").trimEnd();
  return normalized.length > 0 ? normalized.split("\n") : [];
}

function fileChangeStageMethod(item: TraceItem): string {
  const status = String(item.status || "").trim();
  if (status.length > 0) return status;
  const text = String(item.bodyPreview || "");
  const match = /^(item\/[A-Za-z]+(?:\/[A-Za-z]+)?):/u.exec(text);
  return match?.[1] || "item/fileChange";
}

function fileChangeStageStatus(item: TraceItem): string | undefined {
  const text = String(item.bodyPreview || "");
  return /file changes status=([A-Za-z0-9_-]+)/u.exec(text)?.[1];
}

function isFileChangeLifecycleText(text: string): boolean {
  return /^item\/(?:started|completed): file changes status=/u.test(String(text || "").trim());
}

function isEditedFileChangeItem(item: TraceItem): boolean {
  if (String(item.kind || "") !== "edited") return false;
  const status = String(item.status || "");
  const title = String(item.title || "");
  const body = String(item.bodyPreview || "");
  const command = String(item.commandPreview || "");
  if (title === "Edited files") return true;
  if (/^item\/fileChange\//u.test(status)) return true;
  if ((status === "item/started" || status === "item/completed") && /file changes status=/u.test(body)) return true;
  if (/^Success\. Updated the following files:/mu.test(body)) return true;
  if (/^diff --git /mu.test(body)) return true;
  return /^([AMDRCU?]{1,2})\s+\S+/mu.test(body) || (command.length > 0 && parseTraceDiffFiles(body).length > 0);
}

function cleanDiffPath(value: string): string {
  return cleanTracePath(String(value || "").replace(/^[ab]\//u, "").trim());
}

function parseStatusDiffFile(line: string): TraceDiffFile | null {
  const match = /^([AMDRCU?]{1,2})\s+(.+)$/u.exec(line);
  if (!match) return null;
  const path = cleanDiffPath(match[2] || "");
  return path.length > 0 ? { status: match[1] || "M", path } : null;
}

function parsePatchDiffFile(line: string): TraceDiffFile | null {
  const match = /^\*\*\*\s+(Add|Update|Delete)\s+File:\s+(.+)$/u.exec(line);
  if (match) {
    const status = match[1] === "Add" ? "A" : match[1] === "Delete" ? "D" : "M";
    const path = cleanDiffPath(match[2] || "");
    return path.length > 0 ? { status, path } : null;
  }
  const moveMatch = /^\*\*\*\s+Move to:\s+(.+)$/u.exec(line);
  if (moveMatch) {
    const path = cleanDiffPath(moveMatch[1] || "");
    return path.length > 0 ? { status: "R", path } : null;
  }
  return null;
}

function parseTraceDiffFiles(text: string): TraceDiffFile[] {
  const files: TraceDiffFile[] = [];
  const addFile = (status: string, path: string) => {
    const cleanPath = cleanDiffPath(path);
    if (cleanPath.length === 0 || cleanPath === "/dev/null") return;
    const existing = files.find((file) => file.path === cleanPath);
    if (existing) {
      if (existing.status === "M" && status !== "M") existing.status = status;
      return;
    }
    files.push({ status, path: cleanPath });
  };
  let lastDiffPath = "";
  for (const line of traceTextLines(text)) {
    const summaryFile = parseStatusDiffFile(line) || parsePatchDiffFile(line);
    if (summaryFile !== null) {
      addFile(summaryFile.status, summaryFile.path);
      lastDiffPath = summaryFile.path;
      continue;
    }
    const diffMatch = /^diff --git a\/(.+?) b\/(.+)$/u.exec(line);
    if (diffMatch) {
      const path = diffMatch[2] || diffMatch[1] || "";
      addFile("M", path);
      lastDiffPath = cleanDiffPath(path);
      continue;
    }
    const plusMatch = /^\+\+\+ b\/(.+)$/u.exec(line);
    if (plusMatch && plusMatch[1] !== "/dev/null") {
      addFile("M", plusMatch[1] || "");
      lastDiffPath = cleanDiffPath(plusMatch[1] || "");
      continue;
    }
    const createMatch = /^new file mode /u.exec(line);
    if (createMatch && lastDiffPath) addFile("A", lastDiffPath);
    const deleteMatch = /^deleted file mode /u.exec(line);
    if (deleteMatch && lastDiffPath) addFile("D", lastDiffPath);
    const renameMatch = /^rename to (.+)$/u.exec(line);
    if (renameMatch) addFile("R", renameMatch[1] || "");
  }
  return files;
}

function diffLineKind(line: string): TraceDiffLine["kind"] {
  if (parseStatusDiffFile(line) !== null || parsePatchDiffFile(line) !== null) return "file";
  if (/^(diff --git |index |--- |\+\+\+ |\*\*\* Begin Patch|\*\*\* End Patch)/u.test(line)) return "meta";
  if (/^@@ /u.test(line)) return "hunk";
  if (/^\+/u.test(line)) return "add";
  if (/^-/u.test(line)) return "del";
  if (/^(Success\.|No changes|Updated\b|Created\b|Deleted\b|Added\s+\d+\s+lines?|Wrote\s+\d+\s+lines?|Read\s+\d+\s+files?|\.\.\.\[patch content truncated)/iu.test(line)) return "note";
  return "context";
}

function traceDiffLines(text: string): TraceDiffLine[] {
  return traceTextLines(text).map((line) => {
    const file = parseStatusDiffFile(line) || parsePatchDiffFile(line);
    if (file !== null) return { text: line, kind: "file", path: file.path, status: file.status };
    return { text: line, kind: diffLineKind(line) };
  });
}

function traceDiffChangeStats(lines: TraceDiffLine[]): { added: number; removed: number } {
  return lines.reduce((stats, line) => {
    if (line.kind === "add") stats.added += 1;
    else if (line.kind === "del") stats.removed += 1;
    return stats;
  }, { added: 0, removed: 0 });
}

function lineCountLabel(count: number, label: string): string {
  return `${label} ${count} line${count === 1 ? "" : "s"}`;
}

function changeStatLabel(added: number, removed: number): string {
  const parts: string[] = [];
  if (added > 0) parts.push(lineCountLabel(added, "Added"));
  if (removed > 0) parts.push(lineCountLabel(removed, "removed"));
  return parts.join(", ");
}

function latestStageStatus(stages: TraceEditStage[]): string {
  for (let index = stages.length - 1; index >= 0; index -= 1) {
    const status = String(stages[index]?.status || "").trim();
    if (status.length > 0) return status;
  }
  const method = String(stages[stages.length - 1]?.method || "").trim();
  if (method === "item/fileChange/outputDelta") return "updated";
  if (method === "item/started") return "started";
  if (method === "item/completed") return "completed";
  return method.replace(/^item\//u, "") || "changed";
}

function fileCountLabel(count: number): string {
  return `${count} file${count === 1 ? "" : "s"}`;
}

function editObservationFromItems(items: TraceItem[]): TraceEditObservation {
  const rows = items.length > 0 ? items : [];
  const allText = joinTraceText(rows.map((item) => String(item.bodyPreview || "")));
  const visibleText = joinTraceText(rows
    .map((item) => String(item.bodyPreview || ""))
    .filter((text) => text.trim().length > 0 && !isFileChangeLifecycleText(text)));
  const diffText = visibleText || allText;
  const files = parseTraceDiffFiles(diffText || allText);
  const stages = rows.map((item) => ({
    method: fileChangeStageMethod(item),
    status: fileChangeStageStatus(item),
    at: item.at,
  }));
  const lines = traceDiffLines(diffText || allText);
  const stats = traceDiffChangeStats(lines);
  const changeSummary = changeStatLabel(stats.added, stats.removed);
  const fileSummary = files.length > 0 ? fileCountLabel(files.length) : "";
  const summary = changeSummary.length > 0
    ? `${changeSummary}${fileSummary ? ` in ${fileSummary}` : ""}`
    : files.length > 0 ? fileSummary : shortText(diffText || allText || "File changes", 72);
  return {
    status: latestStageStatus(stages),
    summary,
    files,
    stages,
    lines,
    addedLines: stats.added,
    removedLines: stats.removed,
    rawText: allText,
  };
}

function mergeEditedFileChangeGroup(group: TraceItem[]): TraceItem {
  const first = group[0];
  const last = group[group.length - 1] || first;
  const observation = editObservationFromItems(group);
  return {
    ...first,
    seq: Number.isFinite(Number(last?.seq)) ? Number(last?.seq) : Number(first?.seq ?? 0),
    at: last?.at || first?.at,
    title: observation.files.length > 0 ? `Edited ${observation.summary}` : "Edited files",
    status: observation.status,
    commandPreview: "",
    commandOmittedLines: undefined,
    bodyPreview: observation.rawText,
    bodyOmittedLines: group.reduce((sum, item) => sum + Number(item.bodyOmittedLines || 0), 0) || undefined,
    rawSeqs: group.flatMap((item: any) => Array.isArray(item?.rawSeqs) ? item.rawSeqs : [item?.seq]).filter((seq) => seq !== undefined),
    editObservation: observation,
  };
}

function coalesceEditedFileChanges(trace: TraceItem[]): TraceItem[] {
  const rows = Array.isArray(trace) ? trace : [];
  const merged: TraceItem[] = [];
  let group: TraceItem[] = [];
  const flush = () => {
    if (group.length === 0) return;
    merged.push(mergeEditedFileChangeGroup(group));
    group = [];
  };
  for (const item of rows) {
    if (isEditedFileChangeItem(item)) {
      if (fileChangeStageMethod(item) === "item/started" && group.length > 0) flush();
      group.push(item);
      if (fileChangeStageMethod(item) === "item/completed") flush();
      continue;
    }
    flush();
    merged.push(item);
  }
  flush();
  return merged;
}

function toolGroupDigest(items: any[]): AnyRecord {
  const readFiles: string[] = [];
  const editedFiles: string[] = [];
  const runCommands: string[] = [];
  const addUnique = (target: string[], values: string[]) => {
    for (const value of values) if (!target.includes(value)) target.push(value);
  };
  for (const item of items) {
    const kind = String(item?.kind || "");
    const text = [item?.commandPreview, item?.bodyPreview, item?.title].map((value) => String(value || "")).join("\n");
    if (kind === "explored") addUnique(readFiles, extractTracePaths(text));
    else if (kind === "edited") addUnique(editedFiles, extractTracePaths(text));
    else if (kind === "ran") {
      const command = String(item?.commandPreview || item?.title || "").trim();
      if (command.length > 0 && !runCommands.includes(command)) runCommands.push(shortText(command, 90));
    }
  }
  const times = items
    .map((item) => Date.parse(String(item?.at || "")))
    .filter((time) => Number.isFinite(time));
  const spanMs = times.length >= 2 ? Math.max(0, Math.max(...times) - Math.min(...times)) : 0;
  const explicitMs = items.reduce((sum, item) => sum + (finiteMs(item?.durationMs) ?? finiteMs(item?.elapsedMs) ?? 0), 0);
  const durationMs = spanMs > 0 ? spanMs : explicitMs;
  return { readFiles, editedFiles, runCommands, durationLabel: fmtDuration(durationMs) };
}

export function collapseToolTraceRuns(trace: TraceItem[], keepRecentToolCalls = 3): TraceItem[] {
  const rows = Array.isArray(trace) ? trace : [];
  const collapsed: TraceItem[] = [];
  let group: TraceItem[] = [];
  let remainingRecentToolCalls = Math.max(0, keepRecentToolCalls);
  const keepOpen = new Set<any>();
  for (let index = rows.length - 1; index >= 0 && remainingRecentToolCalls > 0; index -= 1) {
    const item = rows[index];
    if (!isToolTraceItem(item)) continue;
    keepOpen.add(item);
    remainingRecentToolCalls -= 1;
  }
  const flush = () => {
    if (group.length >= 2) {
      const counts = toolTraceCounts(group);
      collapsed.push({
        seq: Number(group[0]?.seq ?? 0),
        at: group[0]?.at || group.at(-1)?.at,
        kind: "toolGroup",
        title: toolTraceSummary(group),
        status: `${group.length} calls`,
        items: group,
        counts,
        digest: toolGroupDigest(group),
        rawSeqs: group.flatMap((item: any) => Array.isArray(item?.rawSeqs) ? item.rawSeqs : [item?.seq]).filter((seq) => seq !== undefined),
      });
    } else {
      collapsed.push(...group);
    }
    group = [];
  };
  for (const item of rows) {
    if (isToolTraceItem(item) && !keepOpen.has(item)) {
      group.push(item);
      continue;
    }
    flush();
    collapsed.push(item);
  }
  flush();
  return collapsed;
}

export function normalizeTraceItems(items: any[]): TraceItem[] {
  return (Array.isArray(items) ? items : []).map((item: any, index: number) => ({
    ...item,
    seq: Number.isFinite(Number(item?.seq)) ? Number(item.seq) : index + 1,
    kind: String(item?.kind || "message"),
    at: item?.at === undefined ? undefined : String(item.at),
    durationMs: finiteMs(item?.durationMs) ?? undefined,
    title: item?.title === undefined ? undefined : String(item.title),
    status: item?.status === undefined ? undefined : String(item.status),
  }));
}

export function codexTranscriptToTrace(transcript: any[]): TraceItem[] {
  return normalizeTraceItems(transcript);
}

function opencodePartDurationMs(part: any): number | undefined {
  const start = finiteMs(part?.state?.time?.start) ?? finiteMs(part?.time?.start);
  const end = finiteMs(part?.state?.time?.end) ?? finiteMs(part?.time?.end);
  return finiteMs(part?.durationMs)
    ?? finiteMs(part?.elapsedMs)
    ?? finiteMs(part?.timing?.durationMs)
    ?? finiteMs(part?.metadata?.durationMs)
    ?? (start !== null && end !== null && end >= start ? end - start : null)
    ?? undefined;
}

function opencodePartCompletedAt(part: any, fallback: any): string | undefined {
  return part?.createdAt || part?.updatedAt || part?.completedAt || fallback || undefined;
}

function opencodePartRawSeq(part: any, fallback: any): any {
  return part?.id || part?.messageId || fallback;
}

function opencodePreviewValue(value: any, max = 1200): string {
  if (typeof value === "string") return value;
  if (value === undefined || value === null) return "";
  try {
    return shortText(JSON.stringify(value), max);
  } catch {
    return shortText(String(value), max);
  }
}

function opencodeToolOutput(state: any, part: any, event: any): string {
  if (typeof state?.metadata?.diff === "string" && state.metadata.diff.length > 0) return state.metadata.diff;
  if (typeof state?.metadata?.filediff?.patch === "string" && state.metadata.filediff.patch.length > 0) return state.metadata.filediff.patch;
  if (typeof state?.output === "string" && state.output.length > 0) return state.output;
  if (typeof state?.result === "string" && state.result.length > 0) return state.result;
  if (typeof part?.output === "string" && part.output.length > 0) return part.output;
  if (typeof event?.output === "string" && event.output.length > 0) return event.output;
  if (typeof state?.metadata?.output === "string" && state.metadata.output.length > 0) return state.metadata.output;
  return "";
}

function opencodeRecordString(record: any, keys: string[]): string {
  if (!record || typeof record !== "object" || Array.isArray(record)) return "";
  for (const key of keys) {
    const value = record[key];
    if (typeof value === "string" && value.length > 0) return value;
    if (value !== undefined && value !== null && typeof value !== "object") return String(value);
  }
  return "";
}

function opencodeRecordNumber(record: any, keys: string[]): number | null {
  if (!record || typeof record !== "object" || Array.isArray(record)) return null;
  for (const key of keys) {
    const value = Number(record[key]);
    if (Number.isFinite(value)) return value;
  }
  return null;
}

function opencodeToolCommand(part: any, state: any): string {
  const input = state?.input && typeof state.input === "object" && !Array.isArray(state.input)
    ? state.input
    : part?.input && typeof part.input === "object" && !Array.isArray(part.input)
      ? part.input
      : {};
  const inputCommand = opencodeRecordString(input, ["command", "cmd", "script"]);
  if (inputCommand.length > 0) return inputCommand;
  if (typeof part?.command === "string" && part.command.length > 0) return part.command;
  if (typeof state?.command === "string" && state.command.length > 0) return state.command;
  const tool = String(part?.tool || part?.title || "tool");
  const path = opencodeRecordString(input, ["filePath", "filepath", "path"]) || opencodeRecordString(part, ["filePath", "filepath", "path"]);
  const pattern = opencodeRecordString(input, ["pattern", "query"]);
  const offset = opencodeRecordNumber(input, ["offset"]);
  const limit = opencodeRecordNumber(input, ["limit"]);
  const args = [tool];
  if (pattern.length > 0) args.push(pattern);
  if (path.length > 0) args.push(path);
  if (offset !== null) args.push(`offset=${offset}`);
  if (limit !== null) args.push(`limit=${limit}`);
  return args.length > 1 ? args.join(" ") : tool;
}

function opencodeRawEventToTrace(event: any, fallbackSeq: number): TraceItem | null {
  const part = event?.part && typeof event.part === "object" && !Array.isArray(event.part) ? event.part : {};
  const type = String(event?.type || event?.event || event?.name || part?.type || "").toLowerCase();
  const partType = String(part?.type || "").toLowerCase();
  const at = event?.at || event?.timestamp || part?.updatedAt || part?.createdAt;
  const seq = Number.isFinite(Number(event?.seq)) ? Number(event.seq) : fallbackSeq;
  if (type === "step_start" || type === "step-start" || partType === "step-start") return null;
  if (type === "step_finish" || type === "step-finish" || partType === "step-finish") return null;
  if (partType === "tool" || /tool|bash|command/iu.test(`${type} ${partType}`)) {
    const state = part?.state && typeof part.state === "object" && !Array.isArray(part.state) ? part.state : {};
    const command = opencodeToolCommand(part, state);
    const output = opencodeToolOutput(state, part, event);
    const kind = opencodeToolKindFromCommand(command, String(part?.tool || part?.title || ""));
    const observation = kind === "edited" ? {
      status: String(state?.status || part?.status || event?.status || ""),
      summary: shortText(output || command, 72),
      files: parseTraceDiffFiles(output),
      stages: [],
      lines: traceDiffLines(output),
      addedLines: 0,
      removedLines: 0,
      rawText: output,
    } : undefined;
    return {
      seq,
      at: opencodePartCompletedAt(part, at),
      kind,
      title: String(state?.title || part?.title || state?.metadata?.description || part?.tool || "OpenCode tool"),
      status: String(state?.status || part?.status || event?.status || ""),
      commandPreview: command,
      bodyPreview: output,
      durationMs: opencodePartDurationMs(part),
      rawSeqs: [part?.id || part?.callID || event?.sessionID || seq],
      editObservation: observation,
    };
  }
  const text = opencodePreviewValue(part?.text ?? part?.content ?? part?.delta ?? event?.text ?? event?.content ?? event?.delta, 3000).trim();
  if (text.length > 0) {
    return {
      seq,
      at: opencodePartCompletedAt(part, at),
      kind: partType === "reasoning" ? "message" : /error|failed/iu.test(`${type} ${partType}`) ? "error" : "message",
      title: partType === "reasoning" ? "Reasoning" : /error|failed/iu.test(`${type} ${partType}`) ? "OpenCode error" : "Assistant message",
      status: `opencode/${type || partType || "event"}`,
      bodyPreview: text,
      durationMs: opencodePartDurationMs(part),
      rawSeqs: [part?.id || event?.sessionID || seq],
    };
  }
  return null;
}

function opencodeToolKindFromCommand(command: string, tool: string): TraceItemKind {
  const normalized = `${tool} ${command}`.toLowerCase();
  if (/\b(read|grep|glob|list|ls|find|search|view|cat|sed|rg|head|tail|wc|file)\b/iu.test(normalized)) return "explored";
  if (/\b(edit|write|patch|apply|update|create|delete|apply_patch|git apply|cat >|tee .*<<|sed -i|python3? .*write_text|mkdir|rm |touch )\b/iu.test(normalized)) return "edited";
  return "ran";
}

export function opencodeStepsToTrace(steps: any[]): TraceItem[] {
  const rows: TraceItem[] = [];
  let seq = 1;
  for (const step of Array.isArray(steps) ? steps : []) {
    if (step?.kind && step?.title) {
      const status = String(step?.status || "");
      if (status === "opencode/step-start" || status === "opencode/step-finish") continue;
      rows.push({ ...step, seq: Number.isFinite(Number(step?.seq)) ? Number(step.seq) : seq++ });
      continue;
    }
    const at = step?.createdAt || step?.updatedAt || step?.completedAt;
    const role = String(step?.role || "assistant").toLowerCase();
    const parts = Array.isArray(step?.parts) ? step.parts : [];
    if (parts.length === 0) {
      if (step?.part && (step?.sessionID || String(step?.type || "").startsWith("step_") || String(step?.type || "").includes("tool"))) {
        const item = opencodeRawEventToTrace(step, seq);
        if (item !== null) {
          rows.push(item);
          seq = Math.max(seq + 1, Number(item.seq) + 1);
        }
      } else if (step?.textPreview) {
        rows.push({ seq: seq++, at, kind: "message", title: `${role || "assistant"} message`, status: role, bodyPreview: String(step.textPreview), rawSeqs: [step?.messageId || seq] });
      }
      continue;
    }
    for (const part of parts) {
      const type = String(part?.type || "").toLowerCase();
      if (type === "step-start" || type === "step-finish") continue;
      if (type === "text" || type === "reasoning") {
        const text = String(part?.textPreview || step?.textPreview || "").trim();
        if (text.length === 0) continue;
        rows.push({
          seq: seq++,
          at: opencodePartCompletedAt(part, at),
          kind: "message",
          title: type === "reasoning" ? "Reasoning" : role === "user" ? "User message" : role === "system" ? "System message" : "Assistant message",
          status: type === "reasoning" ? "reasoning" : role,
          bodyPreview: text,
          durationMs: opencodePartDurationMs(part),
          rawSeqs: [opencodePartRawSeq(part, seq)],
        });
        continue;
      }
      if (type === "tool") {
        const state = part?.state && typeof part.state === "object" && !Array.isArray(part.state) ? part.state : {};
        const command = opencodeToolCommand(part, state);
        const output = opencodeToolOutput(state, part, {});
        const kind = opencodeToolKindFromCommand(command, String(part?.tool || part?.title || ""));
        const observation = kind === "edited" ? {
          status: String(state?.status || part?.status || ""),
          summary: shortText(output || command, 72),
          files: parseTraceDiffFiles(output),
          stages: [],
          lines: traceDiffLines(output),
          addedLines: 0,
          removedLines: 0,
          rawText: output,
        } : undefined;
        rows.push({
          seq: seq++,
          at: opencodePartCompletedAt(part, at),
          kind,
          title: String(part?.title || part?.tool || "tool"),
          status: String(part?.status || ""),
          commandPreview: command,
          bodyPreview: output,
          durationMs: opencodePartDurationMs(part),
          rawSeqs: [opencodePartRawSeq(part, seq)],
          editObservation: observation,
        });
        continue;
      }
      const text = String(part?.textPreview || part?.title || type || "").trim();
      if (text) rows.push({ seq: seq++, at: opencodePartCompletedAt(part, at), kind: "system", title: type || "part", bodyPreview: text, status: String(part?.status || ""), durationMs: opencodePartDurationMs(part), rawSeqs: [opencodePartRawSeq(part, seq)] });
    }
  }
  return rows;
}

export const codexTracePort: TracePort<any[]> = { source: "codex", toTrace: codexTranscriptToTrace };
export const opencodeTracePort: TracePort<any[]> = { source: "opencode", toTrace: opencodeStepsToTrace };

function safeClassToken(value: string): string {
  return String(value || "unknown").toLowerCase().replace(/[^a-z0-9_-]+/gu, "-") || "unknown";
}

function diffStatusClass(status: string): string {
  const value = String(status || "M").toUpperCase();
  if (value.startsWith("A") || value === "??") return "added";
  if (value.startsWith("D")) return "deleted";
  if (value.startsWith("R")) return "renamed";
  return "modified";
}

function stageLabel(method: string): string {
  if (method === "item/fileChange/outputDelta") return "delta";
  return method.replace(/^item\//u, "");
}

function renderDiffLine(line: TraceDiffLine, index: number): any {
  if (line.kind === "file") {
    const status = String(line.status || "M");
    return h("div", { key: `${index}-${line.text}`, className: `codex-edit-diff-line file ${diffStatusClass(status)}` },
      h("span", { className: `codex-edit-file-status ${diffStatusClass(status)}` }, status),
      h("code", null, line.path || line.text.replace(/^([AMDRCU?]{1,2})\s+/u, "")),
    );
  }
  const sign = line.kind === "add" || line.kind === "del" ? line.text.slice(0, 1) : line.kind === "hunk" ? "@@" : line.kind === "note" ? "ok" : "";
  const text = line.kind === "add" || line.kind === "del" ? line.text.slice(1) : line.text;
  return h("div", { key: `${index}-${line.text}`, className: `codex-edit-diff-line ${line.kind}` },
    h("span", { className: "codex-edit-diff-sign" }, sign),
    h("code", null, text || " "),
  );
}

function renderEditObservation(observation: TraceEditObservation, bodyMore: string): any {
  const lines = observation.lines.length > 0
    ? observation.lines
    : observation.files.map((file) => ({ text: `${file.status} ${file.path}`, kind: "file" as const, path: file.path, status: file.status }));
  const hasChangedContent = Number(observation.addedLines || 0) + Number(observation.removedLines || 0) > 0;
  return h("div", { className: "codex-edit-observation", "data-testid": "codex-edit-observation" },
    h("div", { className: "codex-edit-observation-head" },
      h("span", { className: "codex-edit-window-controls", "aria-hidden": "true" },
        h("i", null),
        h("i", null),
        h("i", null),
      ),
      h("strong", null, hasChangedContent ? "git diff" : "git diff --stat"),
      h("code", null, observation.summary || "File changes"),
    ),
    observation.stages.length > 0 ? h("div", { className: "codex-edit-stage-strip" },
      observation.stages.map((stage, index) => h("span", { key: `${stage.method}-${index}`, className: `codex-edit-stage ${safeClassToken(stage.status || stage.method)}` },
        h("b", null, stageLabel(stage.method)),
        stage.status ? h("em", null, stage.status) : null,
      )),
    ) : null,
    lines.length > 0 ? h("div", { className: "codex-edit-diff", role: "list" }, lines.map(renderDiffLine)) : null,
    bodyMore ? h("div", { className: "codex-edit-omitted" }, `${bodyMore} (查看原始JSON获取完整记录)`) : null,
  );
}

function renderCommandStream(label: "stdout" | "stderr", text: string, omittedLines: any): any {
  const more = omittedLabel(omittedLines);
  return h("div", { className: `codex-transcript-stream ${label}`, "data-testid": `codex-trace-${label}` },
    h("span", { className: "codex-transcript-stream-label" }, label),
    h("pre", { className: "codex-transcript-body" }, text, more ? `\n${more} (查看原始JSON获取完整记录)` : ""),
  );
}

function renderTraceItem(item: TraceItem, nested = false): any {
  const kind = String(item.kind || "message");
  const isCommand = ["ran", "explored", "edited"].includes(kind);
  const commandMore = omittedLabel(item.commandOmittedLines);
  const bodyMore = omittedLabel(item.bodyOmittedLines);
  const commandText = String(item.commandPreview || (isCommand ? item.title || "" : ""));
  const stdoutText = String(item.stdoutPreview || "");
  const stderrText = String(item.stderrPreview || "");
  const hasCommandStreams = stdoutText.length > 0 || stderrText.length > 0;
  const foldedFullPrompt = Boolean(item.foldedReferencePrompt) && String(item.fullPrompt || "").length > 0;
  const editObservation = kind === "edited" && (item.editObservation !== undefined || isEditedFileChangeItem(item))
    ? item.editObservation || editObservationFromItems([item])
    : null;
  return h("article", { key: `${item.seq}-${kind}`, className: `codex-transcript-item ${kind} ${nested ? "nested" : ""}` },
    h("div", { className: "codex-transcript-main" },
      h("div", { className: "codex-transcript-title" },
        h("span", { className: "codex-output-channel" }, traceKindLabel(kind)),
        isCommand && editObservation === null ? null : h("strong", null, editObservation !== null ? "File changes" : String(item.title || traceKindLabel(kind))),
        item.status ? h("code", null, String(editObservation?.status || item.status)) : null,
        h("time", null, fmtDate(item.at)),
      ),
      commandText && editObservation === null ? h("pre", { className: "codex-transcript-command" }, commandText, commandMore ? `\n${commandMore}` : "") : null,
      editObservation !== null
        ? renderEditObservation(editObservation, bodyMore)
        : hasCommandStreams ? h("div", { className: "codex-transcript-streams" },
          stdoutText.length > 0 ? renderCommandStream("stdout", stdoutText, item.stdoutOmittedLines) : null,
          stderrText.length > 0 ? renderCommandStream("stderr", stderrText, item.stderrOmittedLines) : null,
        )
          : item.bodyPreview ? h("pre", { className: "codex-transcript-body" }, String(item.bodyPreview), bodyMore ? `\n${bodyMore} (查看原始JSON获取完整记录)` : "") : null,
      foldedFullPrompt ? h("details", { className: "codex-initial-prompt-full", "data-testid": "codex-initial-prompt-full" },
        h("summary", null,
          h("span", null, "引用注入已折叠，点击查看最终传入 Codex 的完整 prompt"),
          h("code", null, `${item.fullPromptLines || promptLineCount(String(item.fullPrompt || ""))} lines / ${item.fullPromptChars || String(item.fullPrompt || "").length} chars`),
        ),
        h("pre", { className: "codex-transcript-body codex-transcript-full-prompt", "data-testid": "codex-initial-prompt-full-text" }, String(item.fullPrompt || "")),
      ) : null,
    ),
  );
}

function renderToolGroup(item: TraceItem): any {
  const toolItems = Array.isArray(item.items) ? item.items : [];
  const digest = item.digest && typeof item.digest === "object" ? item.digest : toolGroupDigest(toolItems);
  return h("article", { key: `${item.seq}-toolGroup`, className: "codex-transcript-item toolGroup" },
    h("div", { className: "codex-transcript-main" },
      h("details", { className: "codex-tool-group", "data-testid": "codex-tool-group" },
        h("summary", null,
          h("div", { className: "codex-tool-group-head" },
            h("span", { className: "codex-output-channel" }, traceKindLabel("toolGroup")),
            h("strong", null, String(item.title || toolTraceSummary(toolItems))),
            h("code", null, String(item.status || `${toolItems.length} calls`)),
            h("time", null, fmtDate(item.at)),
          ),
        ),
        h("div", { className: "codex-tool-group-digest" },
          h("span", null, `read: ${compactList(Array.isArray(digest.readFiles) ? digest.readFiles : [])}`),
          h("span", null, `edit: ${compactList(Array.isArray(digest.editedFiles) ? digest.editedFiles : [])}`),
          h("span", null, `run: ${compactList(Array.isArray(digest.runCommands) ? digest.runCommands : [], 2)}`),
          h("span", null, `duration: ${digest.durationLabel || "--"}`),
        ),
        h("div", { className: "codex-tool-group-items" }, toolItems.map((toolItem) => renderTraceItem(toolItem, true))),
      ),
    ),
  );
}

function traceSystemItemIsError(item: TraceItem): boolean {
  const text = [
    item.title,
    item.status,
    item.bodyPreview,
    item.commandPreview,
    item.stderrPreview,
    item.stdoutPreview,
  ].map((value) => String(value || "")).join("\n");
  return /\b(error|failed|failure|interrupt|interrupted|cancell?ed|watchdog|timeout|closed|refused|aborted|exception)\b/iu.test(text);
}

function filterTraceSystemEvents(items: TraceItem[], showSystemEvents: boolean): TraceItem[] {
  if (showSystemEvents) return items;
  return items.flatMap((item) => {
    if (String(item.kind || "") === "system" && !traceSystemItemIsError(item)) return [];
    if (String(item.kind || "") !== "toolGroup" || !Array.isArray(item.items)) return [item];
    const visibleItems = filterTraceSystemEvents(item.items, showSystemEvents);
    return [{ ...item, items: visibleItems }];
  });
}

const traceAutoScrollBottomThresholdPx = 16;

function traceIsScrolledToBottom(element: HTMLElement): boolean {
  const distanceToBottom = element.scrollHeight - element.scrollTop - element.clientHeight;
  return distanceToBottom <= traceAutoScrollBottomThresholdPx;
}

export function TraceView({ items, input, port, autoScroll = false, loading = false, hasDetail = true, emptyText = "等待 Trace 输出...", loadingText = "正在加载完整 Trace...", testId = "trace-output", className = "codex-transcript", keepRecentToolCalls = 3, collapseTools = true, showSystemEvents = false }: AnyRecord) {
  const ref = useRef<HTMLDivElement | null>(null);
  const shouldFollowTailRef = useRef(true);
  const rawTrace = filterTraceSystemEvents(coalesceEditedFileChanges(port ? traceFromPort(port, input) : normalizeTraceItems(items)), Boolean(showSystemEvents));
  const trace = collapseTools ? collapseToolTraceRuns(rawTrace, keepRecentToolCalls) : rawTrace;
  const maxSeq = traceMaxSeq(rawTrace);
  useEffect(() => {
    const element = ref.current;
    if (!autoScroll || !element) return;
    if (!shouldFollowTailRef.current && !traceIsScrolledToBottom(element)) return;
    element.scrollTop = element.scrollHeight;
    shouldFollowTailRef.current = true;
  }, [autoScroll, rawTrace.length, maxSeq]);
  const handleScroll = (event: React.UIEvent<HTMLDivElement>) => {
    const element = event.currentTarget;
    shouldFollowTailRef.current = traceIsScrolledToBottom(element);
  };
  const traceProps = { className, ref, onScroll: handleScroll, "data-testid": testId };
  if (loading && !hasDetail) return h("div", traceProps, h("div", { className: "codex-output-empty" }, loadingText));
  return h("div", traceProps,
    trace.length === 0 ? h("div", { className: "codex-output-empty" }, emptyText) : trace.map((item) => String(item.kind || "") === "toolGroup" ? renderToolGroup(item) : renderTraceItem(item)),
  );
}
