// SPEC: PJ2026-0106050514 Workbench实时运行面 draft-2026-06-30-p0-1297-spec-first; PJ2026-0104010803 Workbench唯一投影 draft-2026-06-28-p0-d518-session-timeline-consistency.
// Responsibility: Stable Workbench timeline row identities derived from authoritative message projections.
// Mechanical source references:
// - OpenCode pages/session/timeline/rows.ts:8-30 TimelineRowMap.
// - OpenCode pages/session/timeline/rows.ts:32-105 tagged row/key/equality.
// - OpenCode pages/session/timeline/rows.ts:107-242 constructMessageRows.
// - OpenCode pages/session/timeline/rows.ts:284-329 unwrapErrorMessage.

import type { ChatMessage } from "@/types";
import { firstScopePart, workbenchTimelineRowScopeKey } from "@/utils/workbench-key";
import { messageOwnsPrimaryFailurePresentation } from "@/utils/workbench-error-runtime";

export type WorkbenchTimelineRowType = "TurnGap" | "CommentStrip" | "UserMessage" | "TurnDivider" | "AssistantPart" | "Thinking" | "Retry" | "DiffSummary" | "Error";

export type WorkbenchTimelineOpenCodeParityStatus = "preserved" | "adapted" | "not-applicable";

export interface WorkbenchTimelineOpenCodeParityRow {
  openCodeRows: string;
  openCodeSource: string;
  hwlabRows: WorkbenchTimelineRowType | "MessageTraceDebugPanel" | "none";
  status: WorkbenchTimelineOpenCodeParityStatus;
  note: string;
}

export const WORKBENCH_TIMELINE_OPENCODE_PARITY: readonly WorkbenchTimelineOpenCodeParityRow[] = Object.freeze([
  { openCodeRows: "TurnGap", openCodeSource: "pages/session/timeline/rows.ts:107-242", hwlabRows: "TurnGap", status: "preserved", note: "User turn boundaries are rebuilt from authoritative ChatMessage order." },
  { openCodeRows: "UserMessage", openCodeSource: "pages/session/timeline/rows.ts:107-242", hwlabRows: "UserMessage", status: "preserved", note: "User text remains a first-class row keyed by message identity." },
  { openCodeRows: "AssistantPart", openCodeSource: "pages/session/timeline/rows.ts:107-242", hwlabRows: "AssistantPart", status: "adapted", note: "HWLAB uses read-model ChatMessage projection instead of OpenCode message parts." },
  { openCodeRows: "Thinking", openCodeSource: "pages/session/timeline/rows.ts:107-242", hwlabRows: "Thinking", status: "adapted", note: "Running agent rows with no visible text keep the thinking placeholder and reasoning heading." },
  { openCodeRows: "Retry", openCodeSource: "pages/session/timeline/rows.ts:107-242", hwlabRows: "Retry", status: "preserved", note: "Retry status produces an explicit retry row." },
  { openCodeRows: "Error", openCodeSource: "pages/session/timeline/rows.ts:284-329", hwlabRows: "Error", status: "preserved", note: "Provider/config JSON errors are unwrapped to readable error rows." },
  { openCodeRows: "DiffSummary", openCodeSource: "pages/session/timeline/rows.ts:107-242", hwlabRows: "DiffSummary", status: "adapted", note: "HWLAB projects summary.diffs from read model and deduplicates by file." },
  { openCodeRows: "Tool", openCodeSource: "pages/session/timeline/rows.ts:107-242", hwlabRows: "MessageTraceDebugPanel", status: "adapted", note: "Tool call details live in the trace timeline/debug panel; collapsed command previews are guarded separately." },
  { openCodeRows: "Permission", openCodeSource: "pages/session/timeline/rows.ts:107-242", hwlabRows: "none", status: "not-applicable", note: "HWLAB Workbench does not expose an interactive OpenCode permission prompt row in the current read model." },
  { openCodeRows: "Question", openCodeSource: "pages/session/timeline/rows.ts:107-242", hwlabRows: "none", status: "not-applicable", note: "HWLAB Workbench has no OpenCode question row authority; user prompts remain UserMessage rows." }
]);

interface WorkbenchTimelineRowBase {
  type: WorkbenchTimelineRowType;
  key: string;
  identity: string;
  sessionId: string | null;
  traceId: string | null;
  userMessageId: string | null;
  message: ChatMessage | null;
  role?: ChatMessage["role"];
}

export interface WorkbenchMessageTimelineRow extends WorkbenchTimelineRowBase {
  type: "UserMessage" | "AssistantPart";
  message: ChatMessage;
  role: ChatMessage["role"];
  anchor?: boolean;
  previousAssistantPart?: boolean;
}

export interface WorkbenchSyntheticTimelineRow extends WorkbenchTimelineRowBase {
  type: "TurnGap" | "CommentStrip" | "TurnDivider" | "Thinking" | "Retry" | "DiffSummary" | "Error";
  message: null;
  label?: "compaction" | "interrupted";
  text?: string | null;
  reasoningHeading?: string | null;
  diffs?: Array<Record<string, unknown>>;
}

export type WorkbenchTimelineRow = WorkbenchMessageTimelineRow | WorkbenchSyntheticTimelineRow;

export function buildWorkbenchTimelineRows(messages: ChatMessage[]): WorkbenchTimelineRow[] {
  const rows: WorkbenchTimelineRow[] = [];
  let currentUserMessageId: string | null = null;
  let assistantPartCount = 0;
  const deduped = dedupeTimelineMessages(messages);
  deduped.forEach((message, index) => {
    const identity = workbenchMessageIdentity(message, index);
    const sessionId = firstScopePart(message.sessionId);
    const traceId = firstScopePart(message.traceId, message.runnerTrace?.traceId);
    if (message.role === "user") {
      if (currentUserMessageId) rows.push(syntheticRow("TurnGap", currentUserMessageId, sessionId, traceId));
      currentUserMessageId = identity;
      assistantPartCount = 0;
      rows.push(messageRow("UserMessage", message, identity, sessionId, traceId, { anchor: true }));
      if (messageHasCommentStrip(message)) rows.push(syntheticRow("CommentStrip", identity, sessionId, traceId));
      return;
    }

    const userMessageId = currentUserMessageId ?? previousUserMessageIdentity(deduped, index) ?? identity;
    rows.push(messageRow("AssistantPart", message, identity, sessionId, traceId, { userMessageId, previousAssistantPart: assistantPartCount > 0 }));
    assistantPartCount += 1;

    if (messageIsThinking(message)) rows.push(syntheticRow("Thinking", userMessageId, sessionId, traceId, { reasoningHeading: reasoningHeading(message) }));
    if (messageIsRetry(message)) rows.push(syntheticRow("Retry", userMessageId, sessionId, traceId));
    const errorText = messageErrorText(message);
    if (errorText && !messageOwnsPrimaryFailurePresentation(message)) rows.push(syntheticRow("Error", userMessageId, sessionId, traceId, { text: errorText }));
    const diffs = summaryDiffs(message);
    if (diffs.length > 0 && !messageIsActive(message)) rows.push(syntheticRow("DiffSummary", userMessageId, sessionId, traceId, { diffs }));
  });
  return rows;
}

export function normalizeWorkbenchTimelineMessages(messages: ChatMessage[]): ChatMessage[] {
  return buildWorkbenchTimelineRows(messages).flatMap((row) => row.message ? [row.message] : []);
}

export function workbenchMessageIdentity(message: ChatMessage, fallbackIndex: number | null = null): string {
  const authorityId = firstScopePart(message.messageId, message.id);
  if (authorityId) return authorityId;
  const turnId = firstScopePart(message.turnId);
  if (turnId) return `turn:${turnId}`;
  const traceId = firstScopePart(message.traceId, message.runnerTrace?.traceId);
  if (traceId && message.role === "agent") return `agent-trace:${traceId}`;
  return `message-index:${fallbackIndex ?? message.createdAt}`;
}

export function workbenchTimelineSignature(rows: WorkbenchTimelineRow[]): string {
  return rows.map((row) => {
    const state = row.message ? row.message.status : row.text ?? row.label ?? row.reasoningHeading ?? "";
    const length = row.message ? String(row.message.text ?? "").length : String(state ?? "").length;
    return [row.type, row.key, state, length].join(":");
  }).join("|");
}

export function timelineRowMessage(row: WorkbenchTimelineRow): ChatMessage | null {
  return row.message;
}

function dedupeTimelineMessages(messages: ChatMessage[]): ChatMessage[] {
  const rows: ChatMessage[] = [];
  const indexByIdentity = new Map<string, number>();
  messages.forEach((message, index) => {
    const identity = workbenchMessageIdentity(message, index);
    const existing = indexByIdentity.get(identity);
    if (existing === undefined) {
      indexByIdentity.set(identity, rows.length);
      rows.push(message);
    } else {
      rows[existing] = message;
    }
  });
  return rows;
}

function messageRow(type: "UserMessage" | "AssistantPart", message: ChatMessage, identity: string, sessionId: string | null, traceId: string | null, extra: { userMessageId?: string | null; anchor?: boolean; previousAssistantPart?: boolean } = {}): WorkbenchMessageTimelineRow {
  const userMessageId = extra.userMessageId ?? (type === "UserMessage" ? identity : null);
  return {
    type,
    key: rowKey(type, sessionId, traceId, userMessageId, message.role, identity),
    identity,
    message,
    role: message.role,
    sessionId,
    traceId,
    userMessageId,
    anchor: extra.anchor,
    previousAssistantPart: extra.previousAssistantPart
  };
}

function syntheticRow(type: WorkbenchSyntheticTimelineRow["type"], userMessageId: string | null, sessionId: string | null, traceId: string | null, extra: Partial<WorkbenchSyntheticTimelineRow> = {}): WorkbenchSyntheticTimelineRow {
  const identity = `${type}:${userMessageId ?? "pending"}:${extra.label ?? extra.text ?? extra.reasoningHeading ?? ""}`;
  return {
    type,
    key: rowKey(type, sessionId, traceId, userMessageId, null, identity),
    identity,
    sessionId,
    traceId,
    userMessageId,
    message: null,
    label: extra.label,
    text: extra.text ?? null,
    reasoningHeading: extra.reasoningHeading ?? null,
    diffs: extra.diffs
  };
}

function rowKey(type: WorkbenchTimelineRowType, sessionId: string | null, traceId: string | null, userMessageId: string | null, role: ChatMessage["role"] | null, rowId: string): string {
  return workbenchTimelineRowScopeKey({ sessionId, traceId, type, role, rowId: userMessageId ? `${userMessageId}:${rowId}` : rowId });
}

function previousUserMessageIdentity(messages: ChatMessage[], beforeIndex: number): string | null {
  for (let index = beforeIndex - 1; index >= 0; index -= 1) {
    const message = messages[index];
    if (message?.role === "user") return workbenchMessageIdentity(message, index);
  }
  return null;
}

function messageHasCommentStrip(message: ChatMessage): boolean {
  return Array.isArray((message as Record<string, unknown>).comments) && ((message as Record<string, unknown>).comments as unknown[]).length > 0;
}

function messageIsThinking(message: ChatMessage): boolean {
  const events = Array.isArray(message.runnerTrace?.events) ? message.runnerTrace.events : [];
  const latest = events.at(-1);
  return message.role === "agent"
    && Boolean(message.runnerTrace)
    && typeof latest?.failureDomain !== "string"
    && messageIsActive(message)
    && !String(message.text ?? "").trim();
}

function messageIsRetry(message: ChatMessage): boolean {
  return normalizeStatus(message.status) === "retry";
}

function messageIsActive(message: ChatMessage): boolean {
  const status = normalizeStatus(message.status);
  return status === "pending" || status === "running" || status === "busy" || status === "retry";
}

function messageErrorText(message: ChatMessage): string | null {
  const source = recordValue(message.error) ?? recordValue(message.projection?.blocker) ?? recordValue(message.runnerTrace?.blocker);
  const text = firstNonEmptyString(source?.userMessage, source?.message, source?.summary, source?.code ? String(source.code) : null);
  return text ? unwrapErrorMessage(text) : null;
}

function summaryDiffs(message: ChatMessage): Array<Record<string, unknown>> {
  const summary = recordValue((message as Record<string, unknown>).summary);
  const diffs = Array.isArray(summary?.diffs) ? summary.diffs : [];
  const seen = new Set<string>();
  return diffs.reduceRight<Array<Record<string, unknown>>>((result, item) => {
    const diff = recordValue(item);
    const file = firstNonEmptyString(diff?.file);
    if (!diff || !file || seen.has(file)) return result;
    seen.add(file);
    result.push(diff);
    return result;
  }, []).reverse();
}

function reasoningHeading(message: ChatMessage): string | null {
  const traceEvents = Array.isArray(message.runnerTrace?.events) ? message.runnerTrace.events : [];
  const latest = traceEvents.at(-1);
  if (String(latest?.label ?? "") === "agentrun:backend:request-admitted") return "启动中...";
  const reasoning = traceEvents.map((event) => firstNonEmptyString(event.text, event.message, event.outputSummary)).find(Boolean) ?? null;
  return reasoning ? extractHeading(reasoning) : null;
}

function extractHeading(text: string): string | null {
  const markdown = text.replace(/\r\n?/g, "\n");
  const html = markdown.match(/<h[1-6][^>]*>([\s\S]*?)<\/h[1-6]>/i);
  if (html?.[1]) return cleanHeading(html[1].replace(/<[^>]+>/g, " "));
  const atx = markdown.match(/^\s{0,3}#{1,6}[ \t]+(.+?)(?:[ \t]+#+[ \t]*)?$/m);
  if (atx?.[1]) return cleanHeading(atx[1]);
  const setext = markdown.match(/^([^\n]+)\n(?:=+|-)\s*$/m);
  if (setext?.[1]) return cleanHeading(setext[1]);
  const strong = markdown.match(/^\s*(?:\*\*|__)(.+?)(?:\*\*|__)\s*$/m);
  if (strong?.[1]) return cleanHeading(strong[1]);
  return null;
}

function cleanHeading(value: string): string | null {
  const text = value.replace(/`([^`]+)`/g, "$1").replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/[*_~]+/g, "").trim();
  return text || null;
}

function unwrapErrorMessage(message: string): string {
  const text = message.replace(/^Error:\s*/, "").trim();
  const parse = (value: string): unknown => {
    try { return JSON.parse(value) as unknown; } catch { return undefined; }
  };
  const read = (value: string): unknown => {
    const first = parse(value);
    return typeof first === "string" ? parse(first.trim()) : first;
  };
  let json = read(text);
  if (json === undefined) {
    const start = text.indexOf("{");
    const end = text.lastIndexOf("}");
    if (start !== -1 && end > start) json = read(text.slice(start, end + 1));
  }
  const source = recordValue(json);
  if (!source) return message;
  const error = recordValue(source.error);
  if (error) {
    const type = firstNonEmptyString(error.type);
    const msg = firstNonEmptyString(error.message);
    if (type && msg) return `${type}: ${msg}`;
    if (msg) return msg;
    if (type) return type;
    const code = firstNonEmptyString(error.code);
    if (code) return code;
  }
  return firstNonEmptyString(source.message, source.error) ?? message;
}

function normalizeStatus(status: unknown): string {
  return String(status ?? "").trim().toLowerCase().replace(/_/gu, "-");
}

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" && typeof value !== "number") continue;
    const text = String(value).trim();
    if (text) return text;
  }
  return null;
}
