import type { AssistantContentPart, Message } from "#cli/core/model";
import type { DimSdkStore } from "#cli/core/store";
import type { DimSdkSessionListItem } from "../../keys";
import { readFileSync, statSync } from "node:fs";
import path from "node:path";
import { mergeSessionUiMetadata, readSessionUiMetadataFromSnapshot } from "#cli/core/session-meta";
import {
  snapshotToUiMessages,
  uiMessagesToRuntimeMessages as uiMessagesToDimSdkMessages,
} from "#cli/core/session-ui-messages";
import {
  deriveSessionLastUserMessageFromMessages,
  deriveSessionSummaryFromMessages,
  deriveSessionTitleFromMessages,
} from "#cli/core/sessions";
import {
  isErrorAssistantMessage,
  isInterruptedAssistantMessage,
} from "#cli/core/store/acp-transform";
import {
  isCompactTranscriptMessageId,
  isCompressionTranscriptMessageId,
} from "#cli/core/supplemental-transcript";
import {
  cloneLastRequestBudget,
  normalizeLastRequestBudget,
} from "../../core/store/request-budget";
import { normalizeStr } from "../user-cache";

const INTERRUPTED_PERSISTED_MESSAGE_LIMIT = 80;
const COMPACT_PERSISTED_MESSAGE_LIMIT = 80;
const COMPRESSION_PERSISTED_MESSAGE_LIMIT = 80;
const ERROR_PERSISTED_MESSAGE_LIMIT = 80;
const MAX_INLINE_PLAN_MARKDOWN_BYTES = 256 * 1024;
const PROPOSED_PLAN_OPEN_TAG = "<proposed_plan>";
const PROPOSED_PLAN_CLOSE_TAG = "</proposed_plan>";

type ErrorRestoreCandidate = Readonly<{
  anchorSig: string | null;
  message: Extract<Message, { role: "assistant" }>;
}>;

type HydrationRestorePhase =
  | Readonly<{
      kind: "anchored";
      name: string;
      candidates: readonly Message[];
    }>
  | Readonly<{
      kind: "error";
      name: string;
      candidates: readonly ErrorRestoreCandidate[];
    }>;

type AssistantLlmMetaPart = Extract<AssistantContentPart, { type: "llm_meta" }>;

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

function snapshotUiMetadata(snapshot: unknown): Record<string, unknown> {
  return asRecord(asRecord(asRecord(snapshot).metadata).ui);
}

function clonePersistedUiMessage(entry: unknown): Message | null {
  if (!entry || typeof entry !== "object") return null;
  try {
    const cloned = JSON.parse(JSON.stringify(entry)) as Message;
    return cloned?.role ? cloned : null;
  } catch {
    return null;
  }
}

function hasNativeAssistantParts(entries: readonly unknown[]): boolean {
  return entries.some((entry) => {
    const record = asRecord(entry);
    return record.role === "assistant" && Array.isArray(record.parts);
  });
}

function clonePersistedUiMessages(entries: readonly unknown[]): Message[] {
  const ui: Message[] = [];
  for (const entry of entries) {
    const cloned = clonePersistedUiMessage(entry);
    if (cloned) ui.push(cloned);
  }
  return ui;
}

function snapshotId(snapshot: unknown): string {
  const record = asRecord(snapshot);
  return String(record.sessionId ?? record.id ?? "").trim();
}

function snapshotUpdatedAt(snapshot: unknown): number {
  const record = asRecord(snapshot);
  const state = asRecord(record.state);
  const updatedAt = Number(record.updatedAt ?? state.updatedAt ?? state.lastActiveAt ?? 0) || 0;
  return updatedAt;
}

function snapshotMessages(snapshot: unknown): unknown[] {
  const record = asRecord(snapshot);
  const context = asRecord(record.context);
  const messages = Array.isArray(record.messages) ? record.messages : context.messages;
  return Array.isArray(messages) ? messages : [];
}

function snapshotMessageCount(snapshot: unknown): number {
  const record = asRecord(snapshot);
  const context = asRecord(record.context);
  const hasRuntimeMessages = Array.isArray(record.messages);
  const n = Number(hasRuntimeMessages ? record.messages.length : (context.messageCount ?? 0));
  if (Number.isFinite(n) && n >= 0) return n;
  return snapshotMessages(snapshot).length;
}

function snapshotUsage(snapshot: unknown): Record<string, unknown> | null {
  const record = asRecord(snapshot);
  const stats = asRecord(record.stats);
  const metadata = asRecord(record.metadata);
  const ui = asRecord(metadata.ui);
  const usage = record.usage ?? stats.usage ?? ui.usage;
  return usage && typeof usage === "object" ? asRecord(usage) : null;
}

function persistedLastRequestBudgetFromSnapshot(snapshot: unknown) {
  const metadata = asRecord(asRecord(snapshot).metadata);
  const ui = asRecord(metadata.ui);
  return normalizeLastRequestBudget(ui.lastRequestBudget);
}

function snapshotTitle(snapshot: unknown): string | null {
  const record = asRecord(snapshot);
  const metadata = asRecord(record.metadata);
  const runtimeMeta = asRecord(metadata.dimAgentRuntime);
  const state = asRecord(record.state);
  const runtimeTitle = normalizeStr(runtimeMeta.title);
  if (runtimeTitle) return runtimeTitle;
  const title = normalizeStr(metadata.title ?? state.title ?? record.title);
  return title ?? null;
}

function normalizeInlinePlanMarkdownPath(value: unknown): string | null {
  const candidate = normalizeStr(value);
  if (!candidate || candidate.includes("\n") || candidate.includes("\0")) return null;
  if (!path.isAbsolute(candidate)) return null;
  if (path.extname(candidate).toLowerCase() !== ".md") return null;
  return path.normalize(candidate);
}

function readInlinePlanMarkdownSync(filePath: string): string | null {
  const normalizedPath = normalizeInlinePlanMarkdownPath(filePath);
  if (!normalizedPath) return null;

  try {
    const fileInfo = statSync(normalizedPath);
    if (
      !fileInfo.isFile() ||
      fileInfo.size <= 0 ||
      fileInfo.size > MAX_INLINE_PLAN_MARKDOWN_BYTES
    ) {
      return null;
    }
    const markdown = readFileSync(normalizedPath, "utf-8");
    return markdown.trim() ? markdown : null;
  } catch {
    return null;
  }
}

function wrapInlinePlanMarkdown(markdown: string): string | null {
  const body = String(markdown ?? "")
    .replace(/\r/g, "")
    .trim();
  if (!body) return null;
  return `${PROPOSED_PLAN_OPEN_TAG}\n${body}\n${PROPOSED_PLAN_CLOSE_TAG}`;
}

function normalizeToolName(value: unknown): string {
  return String(value ?? "")
    .trim()
    .toLowerCase();
}

function isInlinePlanMarkdownToolName(toolName: string): boolean {
  return toolName === "todoplan" || toolName === "plan_mode_todoplan";
}

function assistantHasMarkdownContent(message: Extract<Message, { role: "assistant" }>): boolean {
  return message.parts.some((part) => {
    if (part.type !== "markdown") return false;
    const markdown = normalizeStr((part as { markdown?: string }).markdown);
    return Boolean(markdown);
  });
}

function extractInlinePlanMarkdownPathFromAssistant(
  message: Extract<Message, { role: "assistant" }>,
): string | null {
  if (!Array.isArray(message.parts) || assistantHasMarkdownContent(message)) return null;

  const toolNameByCallId = new Map<string, string>();
  for (const part of message.parts) {
    if (part.type !== "tool_call") continue;
    const callId = String(part.call?.id ?? "").trim();
    if (!callId) continue;
    toolNameByCallId.set(callId, normalizeToolName(part.call?.name));
  }

  for (let index = message.parts.length - 1; index >= 0; index--) {
    const part = message.parts[index];
    if (part?.type !== "tool_result") continue;

    const callId = String(part.result?.toolCallId ?? "").trim();
    const toolName = toolNameByCallId.get(callId) ?? "";
    if (!isInlinePlanMarkdownToolName(toolName)) continue;

    const structured =
      part.result?.structuredContent && typeof part.result.structuredContent === "object"
        ? (part.result.structuredContent as Record<string, unknown>)
        : {};
    const candidates = [
      structured.markdownPath,
      structured.markdown_path,
      structured.filePath,
      structured.file_path,
      structured.path,
      part.result?.output,
    ];

    for (const candidate of candidates) {
      const normalized = normalizeInlinePlanMarkdownPath(candidate);
      if (normalized) return normalized;
    }
  }

  return null;
}

function hydrateInlinePlanMarkdownPreviews(store: DimSdkStore): void {
  const messages = store.state.messages as Message[];
  let changed = false;
  const nextMessages = messages.map((message) => {
    if (message?.role !== "assistant") return message;

    const previewPath = extractInlinePlanMarkdownPathFromAssistant(message);
    if (!previewPath) return message;

    const markdown = readInlinePlanMarkdownSync(previewPath);
    if (!markdown) return message;
    const proposedPlanMarkdown = wrapInlinePlanMarkdown(markdown);
    if (!proposedPlanMarkdown) return message;

    changed = true;
    return {
      ...message,
      parts: [...message.parts, { type: "markdown", markdown: proposedPlanMarkdown } as const],
    } satisfies Message;
  });

  if (changed) store.replaceMessages(nextMessages);
}

/**
 * Convert a backend snapshot into the minimal session list shape used by the UI.
 *
 * Keep this transformation pure so it can be reused by both the UI and tests.
 */
export function snapshotToListItem(snapshot: unknown): DimSdkSessionListItem | null {
  const id = snapshotId(snapshot);
  if (!id) return null;

  const updatedAt = snapshotUpdatedAt(snapshot);
  const record = asRecord(snapshot);
  const metadata = asRecord(record.metadata);
  const cwd = normalizeStr(record.cwd ?? metadata.cwd);
  const uiMeta = readSessionUiMetadataFromSnapshot(snapshot);
  const messages = snapshotMessages(snapshot);
  const title = snapshotTitle(snapshot) ?? deriveSessionTitleFromMessages(messages);
  const summary = deriveSessionSummaryFromMessages(messages);
  const lastMessage = deriveSessionLastUserMessageFromMessages(messages);
  const messageCount = snapshotMessageCount(snapshot);
  return {
    id,
    updatedAt,
    cwd: cwd ?? undefined,
    title,
    summary,
    messageCount,
    lastMessage,
    status: uiMeta.status,
    queueLength: uiMeta.queueLength,
    queue: uiMeta.queue,
    lastUpdatedAt: uiMeta.lastUpdatedAt,
  };
}

function collectRestorableErrorAssistants(
  messages: readonly Message[],
): Extract<Message, { role: "assistant" }>[] {
  return messages.filter((m): m is Extract<Message, { role: "assistant" }> => {
    if (!m || m.role !== "assistant") return false;
    return isErrorAssistantMessage(m);
  });
}

function assistantThinkingSignature(msg: Extract<Message, { role: "assistant" }>): string {
  return msg.parts
    .filter((p): p is Extract<typeof p, { type: "thinking" }> => p.type === "thinking")
    .map((p) => String(p.content ?? "").trim())
    .join("\n")
    .trim();
}

function assistantToolCallFlowSignature(msg: Extract<Message, { role: "assistant" }>): string {
  return msg.parts
    .filter((p): p is Extract<typeof p, { type: "tool_call" }> => p.type === "tool_call")
    .map((p) => {
      const id = String(p.call.id ?? "").trim();
      const name = String(p.call.name ?? "").trim();
      return `${name}:${id}`;
    })
    .filter(Boolean)
    .join("|");
}

function assistantStatusSignature(msg: Extract<Message, { role: "assistant" }>): string {
  const status = msg.parts.find(
    (p): p is Extract<typeof p, { type: "status" }> => p.type === "status",
  );
  return String(status?.text ?? "").trim();
}

function stripErrorBlockFromMarkdown(markdown: string): string {
  const normalized = String(markdown ?? "")
    .replace(/\r/g, "")
    .trimEnd();
  if (!normalized) return "";
  const match = normalized.match(/(?:^|\n+)# Error\b/i);
  if (!match || typeof match.index !== "number") return normalized.trim();
  if (match.index <= 0) return "";
  return normalized.slice(0, match.index).trimEnd();
}

function assistantErrorMergeSignature(msg: Extract<Message, { role: "assistant" }>): string {
  const thinking = assistantThinkingSignature(msg);
  const markdown = stripErrorBlockFromMarkdown(assistantMarkdownOnlySignature(msg));
  const toolCalls = assistantToolCallFlowSignature(msg);
  return [thinking, markdown, toolCalls].some(Boolean)
    ? `th:${thinking}\nmd:${markdown}\ntc:${toolCalls}`
    : "";
}

function assistantErrorExactSignature(msg: Extract<Message, { role: "assistant" }>): string {
  const status = assistantStatusSignature(msg);
  const thinking = assistantThinkingSignature(msg);
  const markdown = assistantMarkdownOnlySignature(msg);
  const toolCalls = assistantToolCallFlowSignature(msg);
  return `st:${status}\nth:${thinking}\nmd:${markdown}\ntc:${toolCalls}`;
}

function collectInterruptedRoundMessages(messages: readonly Message[]): Message[] {
  const out: Message[] = [];
  let lastUser: Extract<Message, { role: "user" }> | null = null;
  for (let i = 0; i < messages.length; i++) {
    const current = messages[i];
    if (current?.role === "user") {
      lastUser = current;
      continue;
    }
    if (!current || current.role !== "assistant") continue;
    if (!isInterruptedAssistantMessage(current)) continue;
    if (lastUser) out.push(lastUser);
    out.push(current);
  }
  return out;
}

function collectLocalCompactMessages(messages: readonly Message[]): Message[] {
  return messages.filter((m) => {
    if (!m || m.role !== "assistant") return false;
    if (!isCompactTranscriptMessageId(m.id)) return false;
    return m.parts.some((p) => {
      if (p.type === "markdown") return String(p.markdown ?? "").trim().length > 0;
      if (p.type === "info") return String(p.text ?? "").trim().length > 0;
      return false;
    });
  });
}

function collectLocalCompressionMessages(messages: readonly Message[]): Message[] {
  return messages.filter((m) => {
    if (!m || m.role !== "assistant") return false;
    if (!isCompressionTranscriptMessageId(m.id)) return false;
    return m.parts.some((p) => {
      if (p.type === "markdown") return String(p.markdown ?? "").trim().length > 0;
      if (p.type === "info") return String(p.text ?? "").trim().length > 0;
      return false;
    });
  });
}

function isSupplementalTranscriptAssistantMessage(msg: Message): boolean {
  if (!msg || msg.role !== "assistant") return false;
  if (isCompactTranscriptMessageId(msg.id)) return true;
  if (isCompressionTranscriptMessageId(msg.id)) return true;
  if (isInterruptedAssistantMessage(msg)) return true;
  return isErrorAssistantMessage(msg);
}

export function stripSupplementalTranscriptMessages(messages: readonly Message[]): Message[] {
  return messages.filter((m) => {
    if (!m || m.role !== "assistant") return true;
    return !isSupplementalTranscriptAssistantMessage(m);
  });
}

function dedupeAnchoredAssistantRoundMessages(
  candidates: readonly Message[],
  opts: Readonly<{
    include: (message: Extract<Message, { role: "assistant" }>) => boolean;
    limit: number;
  }>,
): Message[] {
  const rounds = new Map<
    string,
    { anchor?: Message; assistant: Extract<Message, { role: "assistant" }> }
  >();
  let lastAnchor: Message | undefined;

  for (const candidate of candidates) {
    if (!candidate) continue;
    if (candidate.role === "user") {
      lastAnchor = candidate;
      continue;
    }
    if (candidate.role !== "assistant") continue;
    if (!opts.include(candidate)) continue;

    const assistantSig = assistantTranscriptSignature(candidate);
    if (!assistantSig) continue;
    const anchorSig = lastAnchor ? messageTranscriptSignature(lastAnchor) : "";
    const key = `${anchorSig}\n---\n${assistantSig}`;
    rounds.delete(key);
    rounds.set(key, { anchor: lastAnchor, assistant: candidate });
  }

  return [...rounds.values()]
    .slice(-opts.limit)
    .flatMap(({ anchor, assistant }) => (anchor ? [anchor, assistant] : [assistant]));
}

function dedupeAssistantMessagesBySignature(
  messages: readonly Message[],
  opts: Readonly<{
    include: (message: Extract<Message, { role: "assistant" }>) => boolean;
    limit: number;
  }>,
): Message[] {
  const unique = new Map<string, Extract<Message, { role: "assistant" }>>();
  for (const message of messages) {
    if (!message || message.role !== "assistant") continue;
    if (!opts.include(message)) continue;
    const sig = messageTranscriptSignature(message);
    if (!sig) continue;
    unique.delete(sig);
    unique.set(sig, message);
  }
  return [...unique.values()].slice(-opts.limit);
}

function dedupeErrorRestoreCandidates(
  candidates: readonly ErrorRestoreCandidate[],
): ErrorRestoreCandidate[] {
  const unique = new Map<string, ErrorRestoreCandidate>();
  for (const candidate of candidates) {
    const exactSig = assistantErrorExactSignature(candidate.message);
    if (!exactSig) continue;
    const key = `${candidate.anchorSig ?? ""}\n---\n${exactSig}`;
    unique.delete(key);
    unique.set(key, candidate);
  }
  return [...unique.values()].slice(-ERROR_PERSISTED_MESSAGE_LIMIT);
}

function collectContextErrorRestoreCandidates(
  messages: readonly Message[],
): ErrorRestoreCandidate[] {
  const out: ErrorRestoreCandidate[] = [];
  let pendingAnchorSig: string | null = null;

  for (const message of messages) {
    if (!message) continue;
    if (message.role === "user") {
      pendingAnchorSig = messageTranscriptSignature(message);
      continue;
    }
    if (message.role !== "assistant") continue;
    if (!isErrorAssistantMessage(message)) continue;
    out.push({
      message,
      anchorSig: pendingAnchorSig,
    });
  }

  return dedupeErrorRestoreCandidates(out);
}

function splitSnapshotContextMessages(messages: readonly Message[]): Readonly<{
  canonicalMessages: Message[];
  interruptedMessages: Message[];
  compactMessages: Message[];
  compressionMessages: Message[];
  errorMessages: ErrorRestoreCandidate[];
}> {
  return {
    canonicalMessages: stripSupplementalTranscriptMessages(messages),
    interruptedMessages: dedupeAnchoredAssistantRoundMessages(
      collectInterruptedRoundMessages(messages),
      {
        include: () => true,
        limit: INTERRUPTED_PERSISTED_MESSAGE_LIMIT,
      },
    ),
    compactMessages: dedupeAssistantMessagesBySignature(collectLocalCompactMessages(messages), {
      include: isCompactTranscriptMessage,
      limit: COMPACT_PERSISTED_MESSAGE_LIMIT,
    }),
    compressionMessages: dedupeAssistantMessagesBySignature(
      collectLocalCompressionMessages(messages),
      {
        include: isCompressionTranscriptMessage,
        limit: COMPRESSION_PERSISTED_MESSAGE_LIMIT,
      },
    ),
    errorMessages: collectContextErrorRestoreCandidates(messages),
  };
}

function persistedInterruptedRoundMessagesFromSnapshot(snapshot: unknown): Message[] {
  const raw = snapshotUiMetadata(snapshot).interruptedMessages;
  if (!Array.isArray(raw) || raw.length === 0) return [];

  // Native UI format (current): assistant entries have a `parts` array.
  // Legacy SDK format (older snapshots): assistant entries have `content` + `tool_calls`.
  // Native format preserves parts order (e.g. tool_calls before interrupted markdown),
  // while the SDK round-trip through snapshotToUiMessages reorders them.
  const hasNativeParts = hasNativeAssistantParts(raw);

  const ui = hasNativeParts
    ? clonePersistedUiMessages(raw)
    : snapshotToUiMessages({ context: { messages: raw } });

  return dedupeAnchoredAssistantRoundMessages(ui, {
    include: () => true,
    limit: INTERRUPTED_PERSISTED_MESSAGE_LIMIT,
  });
}

function persistedCompactMessagesFromSnapshot(snapshot: unknown): Message[] {
  const raw = snapshotUiMetadata(snapshot).compactMessages;
  if (!Array.isArray(raw) || raw.length === 0) return [];
  const ui = snapshotToUiMessages({ context: { messages: raw } }).map((m, idx) => {
    if (!m || m.role !== "assistant") return m;
    const oldId = String(m.id ?? "").trim() || String(idx);
    return { ...m, id: `restored-compact-${oldId}` } as Message;
  });
  return dedupeAssistantMessagesBySignature(ui, {
    include: isCompactTranscriptMessage,
    limit: COMPACT_PERSISTED_MESSAGE_LIMIT,
  });
}

function persistedCompressionMessagesFromSnapshot(snapshot: unknown): Message[] {
  const raw = snapshotUiMetadata(snapshot).compressionMessages;
  if (!Array.isArray(raw) || raw.length === 0) return [];
  const ui = snapshotToUiMessages({ context: { messages: raw } }).map((m, idx) => {
    if (!m || m.role !== "assistant") return m;
    const oldId = String(m.id ?? "").trim() || String(idx);
    return { ...m, id: `restored-compression-${oldId}` } as Message;
  });
  return dedupeAssistantMessagesBySignature(ui, {
    include: isCompressionTranscriptMessage,
    limit: COMPRESSION_PERSISTED_MESSAGE_LIMIT,
  });
}

function persistedErrorMessagesFromSnapshot(snapshot: unknown): ErrorRestoreCandidate[] {
  const raw = snapshotUiMetadata(snapshot).errorMessages;
  if (!Array.isArray(raw) || raw.length === 0) return [];

  const hasNativeParts = hasNativeAssistantParts(raw);

  const ui = hasNativeParts
    ? clonePersistedUiMessages(raw)
    : snapshotToUiMessages({ context: { messages: raw } });

  const out: ErrorRestoreCandidate[] = [];
  let pendingAnchorSig: string | null = null;
  for (const entry of ui) {
    if (!entry) continue;
    if (entry.role === "user") {
      pendingAnchorSig = messageTranscriptSignature(entry);
      continue;
    }
    if (entry.role !== "assistant") continue;
    out.push({
      message: entry,
      anchorSig: pendingAnchorSig,
    });
    pendingAnchorSig = null;
  }
  return dedupeErrorRestoreCandidates(out);
}

function isCompactTranscriptMessage(msg: Message): boolean {
  if (!msg || msg.role !== "assistant") return false;
  return isCompactTranscriptMessageId(msg.id);
}

function isCompressionTranscriptMessage(msg: Message): boolean {
  if (!msg || msg.role !== "assistant") return false;
  return isCompressionTranscriptMessageId(msg.id);
}

function assistantMarkdownOnlySignature(msg: Extract<Message, { role: "assistant" }>): string {
  const markdown = msg.parts
    .filter((p): p is Extract<typeof p, { type: "markdown" }> => p.type === "markdown")
    .map((p) => String(p.markdown ?? ""))
    .join("\n")
    .trim();
  return markdown;
}

function assistantTranscriptSignature(msg: Extract<Message, { role: "assistant" }>): string {
  if (isCompactTranscriptMessage(msg)) {
    const markdownOnly = assistantMarkdownOnlySignature(msg);
    return markdownOnly ? `compact:${markdownOnly}` : "";
  }
  if (isCompressionTranscriptMessage(msg)) {
    const markdownOnly = assistantMarkdownOnlySignature(msg);
    return markdownOnly ? `compression:${markdownOnly}` : "";
  }
  if (isInterruptedAssistantMessage(msg)) {
    const markdownOnly = assistantMarkdownOnlySignature(msg);
    const status = msg.parts.find(
      (p): p is Extract<typeof p, { type: "status" }> => p.type === "status",
    );
    const statusText = String(status?.text ?? "").trim();
    const normalized = markdownOnly || statusText;
    return normalized;
  }
  const status = msg.parts.find(
    (p): p is Extract<typeof p, { type: "status" }> => p.type === "status",
  );
  const statusText = String(status?.text ?? "").trim();
  const markdown = assistantMarkdownOnlySignature(msg);
  return [statusText, markdown].filter(Boolean).join("\n").trim();
}

function assistantLlmMetaPreserveSignature(msg: Extract<Message, { role: "assistant" }>): string {
  const markdown = assistantMarkdownOnlySignature(msg);
  const toolCalls = msg.parts
    .filter((p): p is Extract<typeof p, { type: "tool_call" }> => p.type === "tool_call")
    .map((p) => {
      const id = String(p.call.id ?? "").trim();
      const name = String(p.call.name ?? "").trim();
      return `${name}:${id}`;
    })
    .filter(Boolean)
    .join("|");
  return `md:${markdown}\ntc:${toolCalls}`;
}

function assistantDisplaySignature(msg: Extract<Message, { role: "assistant" }>): string {
  const thinking = msg.parts
    .filter((p): p is Extract<typeof p, { type: "thinking" }> => p.type === "thinking")
    .map((p) => String(p.content ?? "").trim())
    .join("\n")
    .trim();
  const markdown = assistantMarkdownOnlySignature(msg);
  const toolCalls = msg.parts
    .filter((p): p is Extract<typeof p, { type: "tool_call" }> => p.type === "tool_call")
    .map((p) => {
      const id = String(p.call.id ?? "").trim();
      const name = String(p.call.name ?? "").trim();
      return `${name}:${id}`;
    })
    .filter(Boolean)
    .join("|");
  return `th:${thinking}\nmd:${markdown}\ntc:${toolCalls}`;
}

function userTranscriptSignature(msg: Extract<Message, { role: "user" }>): string {
  const content = String(msg.content ?? "").trim();
  const focusFiles = (msg.focusFiles ?? [])
    .map((file) => String(file ?? "").trim())
    .filter(Boolean)
    .join("\n");
  return [content, focusFiles].filter(Boolean).join("\n").trim();
}

function messageTranscriptSignature(msg: Message): string {
  if (msg.role === "assistant") {
    const sig = assistantTranscriptSignature(msg);
    return sig ? `assistant:${sig}` : "";
  }
  if (msg.role === "user") {
    const sig = userTranscriptSignature(msg);
    return sig ? `user:${sig}` : "";
  }
  return "";
}

function assistantThinkingCollapsePreserveKey(
  msg: Extract<Message, { role: "assistant" }>,
): string {
  const messageId = String(msg.id ?? "").trim();
  if (messageId) return `id:${messageId}`;

  const sig = assistantDisplaySignature(msg);
  return sig ? `sig:${sig}` : "";
}

function collectLocalThinkingCollapsedState(
  messages: readonly Message[],
): Map<string, boolean[][]> {
  const byKey = new Map<string, boolean[][]>();

  for (const msg of messages) {
    if (!msg || msg.role !== "assistant") continue;

    const collapsedByOrdinal = msg.parts
      .filter((p): p is Extract<typeof p, { type: "thinking" }> => p.type === "thinking")
      .map((p) => Boolean(p.collapsed));
    if (collapsedByOrdinal.length === 0) continue;

    const key = assistantThinkingCollapsePreserveKey(msg);
    if (!key) continue;

    const queue = byKey.get(key) ?? [];
    queue.push([...collapsedByOrdinal]);
    byKey.set(key, queue);
  }

  return byKey;
}

function restoreLocalThinkingCollapsedState(
  store: DimSdkStore,
  localThinkingCollapsedState: Map<string, boolean[][]>,
): void {
  if (localThinkingCollapsedState.size === 0) return;

  let changed = false;
  const next = store.state.messages.map((msg) => {
    if (!msg || msg.role !== "assistant") return msg;

    const key = assistantThinkingCollapsePreserveKey(msg);
    if (!key) return msg;
    const queue = localThinkingCollapsedState.get(key);
    if (!queue || queue.length === 0) return msg;

    const collapsedByOrdinal = queue.shift() ?? [];
    if (queue.length === 0) localThinkingCollapsedState.delete(key);
    if (collapsedByOrdinal.length === 0) return msg;

    let thinkingOrdinal = 0;
    let messageChanged = false;
    const parts = msg.parts.map((part) => {
      if (part.type !== "thinking") return part;
      const collapsed = collapsedByOrdinal[thinkingOrdinal];
      thinkingOrdinal += 1;
      if (typeof collapsed !== "boolean" || part.collapsed === collapsed) return part;
      messageChanged = true;
      return { ...part, collapsed };
    });
    if (!messageChanged) return msg;
    changed = true;
    return { ...msg, parts } as Message;
  });

  if (changed) store.replaceMessages(next);
}

function cloneLlmMetaPart(part: AssistantLlmMetaPart): AssistantLlmMetaPart {
  return {
    ...part,
    ...(part.changedFiles ? { changedFiles: [...part.changedFiles] } : {}),
    ...(part.changedFileDetails
      ? {
          changedFileDetails: part.changedFileDetails.map((detail) => ({
            ...detail,
          })),
        }
      : {}),
  };
}

function collectLocalAssistantLlmMetaBySignature(
  messages: readonly Message[],
): Map<string, AssistantLlmMetaPart[][]> {
  const bySignature = new Map<string, AssistantLlmMetaPart[][]>();
  for (const msg of messages) {
    if (!msg || msg.role !== "assistant") continue;
    if (!msg.parts.some((p) => p.type === "llm_meta")) continue;
    const signature = assistantLlmMetaPreserveSignature(msg);
    if (!signature) continue;
    const llmMetaParts = msg.parts
      .filter((p): p is AssistantLlmMetaPart => p.type === "llm_meta")
      .map((p) => cloneLlmMetaPart(p));
    if (llmMetaParts.length === 0) continue;
    const queue = bySignature.get(signature) ?? [];
    queue.push(llmMetaParts);
    bySignature.set(signature, queue);
  }
  return bySignature;
}

function mergeLocalAssistantLlmMetaBySignature(
  uiMessages: Message[],
  localLlmMetaBySignature: Map<string, AssistantLlmMetaPart[][]>,
): void {
  if (localLlmMetaBySignature.size === 0) return;
  for (let i = 0; i < uiMessages.length; i++) {
    const msg = uiMessages[i];
    if (!msg || msg.role !== "assistant") continue;
    if (msg.parts.some((p) => p.type === "llm_meta")) continue;
    const signature = assistantLlmMetaPreserveSignature(msg);
    if (!signature) continue;
    const queue = localLlmMetaBySignature.get(signature);
    if (!queue || queue.length === 0) continue;
    const llmMetaParts = queue.shift() ?? [];
    if (queue.length === 0) localLlmMetaBySignature.delete(signature);
    if (llmMetaParts.length === 0) continue;
    uiMessages[i] = {
      ...msg,
      parts: [...msg.parts, ...llmMetaParts],
    } as Message;
  }
}

type MessageHeaderMeta = Readonly<{ createdAtMs?: number; modelId?: string }>;
type LocalAssistantLlmMetaRecord = Readonly<{
  llmMetaParts: AssistantLlmMetaPart[];
  toolCallIds: string[];
}>;

function collectLocalMessageHeaderMetaBySignature(
  messages: readonly Message[],
): Map<string, MessageHeaderMeta[]> {
  const bySignature = new Map<string, MessageHeaderMeta[]>();
  for (const msg of messages) {
    if (!msg || (msg.role !== "user" && msg.role !== "assistant")) continue;
    const createdAtMs = Number(msg.createdAtMs);
    const modelId = String(msg.modelId ?? "").trim() || undefined;
    if (!Number.isFinite(createdAtMs) && !modelId) continue;
    const signature =
      msg.role === "user" ? userTranscriptSignature(msg) : assistantDisplaySignature(msg);
    if (!signature) continue;
    const key = `${msg.role}:${signature}`;
    const queue = bySignature.get(key) ?? [];
    queue.push({
      ...(Number.isFinite(createdAtMs) ? { createdAtMs: Math.floor(createdAtMs) } : {}),
      ...(modelId ? { modelId } : {}),
    });
    bySignature.set(key, queue);
  }
  return bySignature;
}

function mergeLocalMessageHeaderMetaBySignature(
  uiMessages: Message[],
  localHeaderMetaBySignature: Map<string, MessageHeaderMeta[]>,
): void {
  if (localHeaderMetaBySignature.size === 0) return;
  for (let i = 0; i < uiMessages.length; i++) {
    const msg = uiMessages[i];
    if (!msg || (msg.role !== "user" && msg.role !== "assistant")) continue;
    const hasCreatedAt = Number.isFinite(Number(msg.createdAtMs));
    const hasModelId = Boolean(String(msg.modelId ?? "").trim());
    if (hasCreatedAt && hasModelId) continue;
    const signature =
      msg.role === "user" ? userTranscriptSignature(msg) : assistantDisplaySignature(msg);
    if (!signature) continue;
    const key = `${msg.role}:${signature}`;
    const queue = localHeaderMetaBySignature.get(key);
    if (!queue || queue.length === 0) continue;
    const headerMeta = queue.shift() ?? {};
    if (queue.length === 0) localHeaderMetaBySignature.delete(key);
    const nextCreatedAtMs = hasCreatedAt ? Number(msg.createdAtMs) : Number(headerMeta.createdAtMs);
    const nextModelId = hasModelId
      ? String(msg.modelId ?? "").trim()
      : String(headerMeta.modelId ?? "").trim();
    const next = {
      ...msg,
      ...(Number.isFinite(nextCreatedAtMs) ? { createdAtMs: Math.floor(nextCreatedAtMs) } : {}),
      ...(nextModelId ? { modelId: nextModelId } : {}),
    } as Message;
    uiMessages[i] = next;
  }
}

function collectAssistantToolCallIds(msg: Extract<Message, { role: "assistant" }>): string[] {
  const out: string[] = [];
  const seen = new Set<string>();
  for (const part of msg.parts) {
    if (part.type !== "tool_call") continue;
    const id = String(part.call.id ?? "").trim();
    if (!id || seen.has(id)) continue;
    seen.add(id);
    out.push(id);
  }
  return out;
}

function hasAssistantUserFacingContent(msg: Extract<Message, { role: "assistant" }>): boolean {
  return msg.parts.some((part) => {
    if (part.type === "markdown") return Boolean(String(part.markdown ?? "").trim());
    if (part.type === "info") return Boolean(String(part.text ?? "").trim());
    return part.type === "todo" || part.type === "plan" || part.type === "approve";
  });
}

function llmMetaPartsSignature(parts: readonly AssistantLlmMetaPart[]): string {
  return parts
    .map((part) => {
      const durationMs = Math.floor(Number(part.durationMs ?? 0));
      const modelId = String(part.modelId ?? "").trim();
      const toolCalls = Math.floor(Number(part.toolCalls ?? 0));
      const tokensUsed = Math.floor(Number(part.tokensUsed ?? 0));
      const changedFiles = (part.changedFiles ?? [])
        .map((file) => String(file ?? "").trim())
        .filter(Boolean)
        .join("|");
      const changedFileDetails = (part.changedFileDetails ?? [])
        .map((item) => {
          const path = String(item.path ?? "").trim();
          const added = Math.floor(Number(item.added ?? -1));
          const removed = Math.floor(Number(item.removed ?? -1));
          return `${path}:${added}:${removed}`;
        })
        .filter(Boolean)
        .join("|");
      return [durationMs, modelId, toolCalls, tokensUsed, changedFiles, changedFileDetails].join(
        ":",
      );
    })
    .filter(Boolean)
    .join("||");
}

function collectLocalAssistantLlmMetaRecords(
  messages: readonly Message[],
): LocalAssistantLlmMetaRecord[] {
  const out: LocalAssistantLlmMetaRecord[] = [];
  for (const msg of messages) {
    if (!msg || msg.role !== "assistant") continue;
    const llmMetaParts = msg.parts
      .filter((p): p is AssistantLlmMetaPart => p.type === "llm_meta")
      .map((p) => cloneLlmMetaPart(p));
    if (llmMetaParts.length === 0) continue;
    out.push({
      llmMetaParts,
      toolCallIds: collectAssistantToolCallIds(msg),
    });
  }
  return out;
}

function mergeLocalAssistantLlmMetaByToolCallFlow(
  uiMessages: Message[],
  localRecords: readonly LocalAssistantLlmMetaRecord[],
): void {
  if (localRecords.length === 0) return;

  const existingLlmMetaSignatures = new Set<string>();
  for (const msg of uiMessages) {
    if (!msg || msg.role !== "assistant") continue;
    const llmMetaParts = msg.parts.filter((p): p is AssistantLlmMetaPart => p.type === "llm_meta");
    if (llmMetaParts.length === 0) continue;
    existingLlmMetaSignatures.add(llmMetaPartsSignature(llmMetaParts));
  }

  let searchStart = 0;
  for (const record of localRecords) {
    const sig = llmMetaPartsSignature(record.llmMetaParts);
    if (!sig || existingLlmMetaSignatures.has(sig)) continue;

    let anchorIdx = -1;
    if (record.toolCallIds.length > 0) {
      for (let i = searchStart; i < uiMessages.length; i++) {
        const msg = uiMessages[i];
        if (!msg || msg.role !== "assistant") continue;
        const callIds = collectAssistantToolCallIds(msg);
        if (callIds.some((id) => record.toolCallIds.includes(id))) {
          anchorIdx = i;
          break;
        }
      }
    }
    if (anchorIdx < 0) {
      for (let i = searchStart; i < uiMessages.length; i++) {
        const msg = uiMessages[i];
        if (msg?.role === "assistant") {
          anchorIdx = i;
          break;
        }
      }
    }
    if (anchorIdx < 0) continue;

    let endIdx = anchorIdx;
    for (let i = anchorIdx + 1; i < uiMessages.length; i++) {
      const msg = uiMessages[i];
      if (msg?.role === "user") break;
      if (msg?.role === "assistant") endIdx = i;
    }
    let targetIdx = endIdx;
    for (let i = endIdx; i >= anchorIdx; i--) {
      const msg = uiMessages[i];
      if (!msg || msg.role !== "assistant") continue;
      if (hasAssistantUserFacingContent(msg)) {
        targetIdx = i;
        break;
      }
    }

    const target = uiMessages[targetIdx];
    if (!target || target.role !== "assistant") {
      searchStart = endIdx + 1;
      continue;
    }
    if (target.parts.some((p) => p.type === "llm_meta")) {
      searchStart = endIdx + 1;
      continue;
    }
    uiMessages[targetIdx] = {
      ...target,
      parts: [...target.parts, ...record.llmMetaParts],
    } as Message;
    existingLlmMetaSignatures.add(sig);
    searchStart = endIdx + 1;
  }
}

function buildSignatureCounts(signatures: readonly string[]): Map<string, number> {
  const counts = new Map<string, number>();
  for (const sig of signatures) {
    if (!sig) continue;
    counts.set(sig, (counts.get(sig) ?? 0) + 1);
  }
  return counts;
}

function consumeSignatureCount(counts: Map<string, number>, signature: string): boolean {
  const remaining = counts.get(signature) ?? 0;
  if (remaining <= 0) return false;
  if (remaining === 1) counts.delete(signature);
  else counts.set(signature, remaining - 1);
  return true;
}

function buildAnchoredInsertionPlan(
  existingMessages: readonly Message[],
  candidates: readonly Message[],
): Array<{ message: Message; anchorSig: string | null }> {
  const existingCounts = buildSignatureCounts(existingMessages.map(messageTranscriptSignature));

  // Walk candidates in order, tracking the last-seen existing message as the anchor.
  // When a missing message is encountered, record it together with its anchor signature
  // so we can find the correct insertion point later.
  let pendingAnchorSig: string | null = null;
  const toInsert: Array<{ message: Message; anchorSig: string | null }> = [];
  for (const m of candidates) {
    const sig = messageTranscriptSignature(m);
    if (!sig) {
      pendingAnchorSig = null;
      continue;
    }
    if (consumeSignatureCount(existingCounts, sig)) {
      // Already in store; use as anchor for the next missing candidate.
      pendingAnchorSig = sig;
      continue;
    }
    toInsert.push({ message: m, anchorSig: pendingAnchorSig });
    // Missing messages also act as anchors for subsequent candidates
    // (e.g. when user_prev itself is also missing and is paired with interrupted_assistant).
    pendingAnchorSig = sig;
  }

  return toInsert;
}

function findAnchoredInsertIndex(messages: readonly Message[], anchorSig: string | null): number {
  if (!anchorSig) return -1;

  // Search from the end so duplicate-content user messages resolve to the latest
  // matching turn, which aligns with how persisted supplemental transcript is appended.
  let anchorIdx = -1;
  for (let i = messages.length - 1; i >= 0; i--) {
    if (messageTranscriptSignature(messages[i]!) === anchorSig) {
      anchorIdx = i;
      break;
    }
  }
  if (anchorIdx < 0) return -1;

  let insertAt = anchorIdx + 1;
  // Keep a completed turn together: reinsert supplemental assistant content after the
  // anchor user and any non-user messages already associated with that turn.
  while (insertAt < messages.length && messages[insertAt]?.role !== "user") {
    insertAt++;
  }
  return insertAt;
}

function findChronologicalInsertIndex(
  messages: readonly Message[],
  candidate: Pick<Message, "createdAtMs">,
): number {
  const ts = candidate.createdAtMs ?? 0;
  if (ts <= 0) return messages.length;

  let insertAt = 0;
  for (let i = 0; i < messages.length; i++) {
    if ((messages[i]!.createdAtMs ?? 0) <= ts) insertAt = i + 1;
  }
  return insertAt;
}

function restoreAnchoredMessages(store: DimSdkStore, candidates: readonly Message[]): void {
  const toInsert = buildAnchoredInsertionPlan(store.state.messages, candidates);
  if (toInsert.length === 0) return;

  const next = store.state.messages.slice();
  for (const { message, anchorSig } of toInsert) {
    const insertAt = (() => {
      const anchored = findAnchoredInsertIndex(next, anchorSig);
      if (anchored >= 0) return anchored;
      return findChronologicalInsertIndex(next, message);
    })();
    next.splice(insertAt, 0, message);
  }

  store.replaceMessages(next);
}

function restoreErrorAssistantMessages(
  store: DimSdkStore,
  candidates: readonly ErrorRestoreCandidate[],
): void {
  if (candidates.length === 0) return;

  const next = store.state.messages.slice();
  const usedReplacementIndexes = new Set<number>();
  let changed = false;

  for (const { message: candidate, anchorSig } of candidates) {
    const exactSig = assistantErrorExactSignature(candidate);
    const existingExactIndex = next.findIndex((m) => {
      if (!m || m.role !== "assistant") return false;
      return assistantErrorExactSignature(m) === exactSig;
    });
    if (existingExactIndex >= 0) continue;

    const mergeSig = assistantErrorMergeSignature(candidate);
    if (mergeSig) {
      const replaceIndex = next.findIndex((m, index) => {
        if (usedReplacementIndexes.has(index) || !m || m.role !== "assistant") return false;
        return assistantErrorMergeSignature(m) === mergeSig;
      });
      if (replaceIndex >= 0) {
        next[replaceIndex] = candidate;
        usedReplacementIndexes.add(replaceIndex);
        changed = true;
        continue;
      }
    }

    const insertAt = (() => {
      const anchored = findAnchoredInsertIndex(next, anchorSig);
      if (anchored >= 0) return anchored;
      return findChronologicalInsertIndex(next, candidate);
    })();
    next.splice(insertAt, 0, candidate);
    changed = true;
  }

  if (changed) store.replaceMessages(next);
}

function applyHydrationRestorePhases(
  store: DimSdkStore,
  phases: readonly HydrationRestorePhase[],
): void {
  for (const phase of phases) {
    if (phase.kind === "anchored") {
      restoreAnchoredMessages(store, phase.candidates);
      continue;
    }
    restoreErrorAssistantMessages(store, phase.candidates);
  }
}

/**
 * Hydrate the UI store from a backend snapshot.
 *
 * Note: this intentionally resets transient UI state (selected tool call, pending approvals, etc).
 */
export function hydrateStoreFromSnapshot(
  store: DimSdkStore,
  snapshot: unknown,
  opts?: Readonly<{
    preserveLocalSyntheticErrors?: boolean;
    preserveLocalInterruptedMessages?: boolean;
    preserveLocalCompactMessages?: boolean;
    preserveLocalCompressionMessages?: boolean;
  }>,
): void {
  const previousSessionId = String(store.state.sessionId ?? "").trim();
  const hadLocalMessagesBeforeHydrate = store.state.messages.length > 0;
  const preserveLocalSyntheticErrors = Boolean(opts?.preserveLocalSyntheticErrors);
  const preserveLocalInterruptedMessages = Boolean(opts?.preserveLocalInterruptedMessages);
  const preserveLocalCompactMessages = Boolean(opts?.preserveLocalCompactMessages);
  const preserveLocalCompressionMessages = Boolean(opts?.preserveLocalCompressionMessages);
  const localErrorAssistants = preserveLocalSyntheticErrors
    ? collectRestorableErrorAssistants(store.state.messages)
    : [];
  const localInterruptedRoundMessages = preserveLocalInterruptedMessages
    ? collectInterruptedRoundMessages(store.state.messages)
    : [];

  const id = snapshotId(snapshot);
  const sameSession = Boolean(id) && id === previousSessionId;
  const localSessionMeta =
    sameSession && store.state.sessionMeta
      ? mergeSessionUiMetadata(store.state.sessionMeta, {})
      : null;
  const preserveLocalSessionHistoryDirty = sameSession && Boolean(store.state.sessionHistoryDirty);
  const localAssistantLlmMetaBySignature = sameSession
    ? collectLocalAssistantLlmMetaBySignature(store.state.messages)
    : new Map<string, unknown[][]>();
  const localAssistantLlmMetaRecords = sameSession
    ? collectLocalAssistantLlmMetaRecords(store.state.messages)
    : [];
  const localMessageHeaderMetaBySignature = sameSession
    ? collectLocalMessageHeaderMetaBySignature(store.state.messages)
    : new Map<string, MessageHeaderMeta[]>();
  const localThinkingCollapsedState = sameSession
    ? collectLocalThinkingCollapsedState(store.state.messages)
    : new Map<string, boolean[][]>();
  const localLastRequestBudget = sameSession
    ? normalizeLastRequestBudget(store.state.lastRequestBudget)
    : null;
  const persistedLastRequestBudget = persistedLastRequestBudgetFromSnapshot(snapshot);
  // Compact success transcript is an ephemeral UI notice. Do not keep replaying it on
  // same-session re-hydrates once the local transcript already exists, otherwise it
  // jumps back to the bottom of the chat after every round.
  const shouldRestorePersistedCompactMessages = !sameSession || !hadLocalMessagesBeforeHydrate;
  const persistedCompactMessages = shouldRestorePersistedCompactMessages
    ? persistedCompactMessagesFromSnapshot(snapshot)
    : [];
  const localCompactMessages =
    preserveLocalCompactMessages && sameSession && !hadLocalMessagesBeforeHydrate
      ? dedupeAssistantMessagesBySignature(collectLocalCompactMessages(store.state.messages), {
          include: isCompactTranscriptMessage,
          limit: COMPACT_PERSISTED_MESSAGE_LIMIT,
        })
      : [];
  const localCompressionMessages =
    preserveLocalCompressionMessages && sameSession
      ? dedupeAssistantMessagesBySignature(collectLocalCompressionMessages(store.state.messages), {
          include: isCompressionTranscriptMessage,
          limit: COMPRESSION_PERSISTED_MESSAGE_LIMIT,
        })
      : [];
  if (id) store.state.sessionId = id;

  const rawUiMessages = snapshotToUiMessages(snapshot);
  const contextTranscript = splitSnapshotContextMessages(rawUiMessages);
  const persistedInterruptedRoundMessages = dedupeAnchoredAssistantRoundMessages(
    [
      ...contextTranscript.interruptedMessages,
      ...persistedInterruptedRoundMessagesFromSnapshot(snapshot),
    ],
    {
      include: () => true,
      limit: INTERRUPTED_PERSISTED_MESSAGE_LIMIT,
    },
  );
  const persistedCompressionMessages = dedupeAssistantMessagesBySignature(
    [
      ...contextTranscript.compressionMessages,
      ...persistedCompressionMessagesFromSnapshot(snapshot),
    ],
    {
      include: isCompressionTranscriptMessage,
      limit: COMPRESSION_PERSISTED_MESSAGE_LIMIT,
    },
  );
  const persistedErrorMessages = dedupeErrorRestoreCandidates([
    ...contextTranscript.errorMessages,
    ...persistedErrorMessagesFromSnapshot(snapshot),
  ]);
  const uiMessages = contextTranscript.canonicalMessages.slice();
  const persistedCompactRestoreCandidates = shouldRestorePersistedCompactMessages
    ? dedupeAssistantMessagesBySignature(
        [...contextTranscript.compactMessages, ...persistedCompactMessages],
        {
          include: isCompactTranscriptMessage,
          limit: COMPACT_PERSISTED_MESSAGE_LIMIT,
        },
      )
    : [];
  if (sameSession) {
    mergeLocalMessageHeaderMetaBySignature(uiMessages, localMessageHeaderMetaBySignature);
    mergeLocalAssistantLlmMetaBySignature(uiMessages, localAssistantLlmMetaBySignature);
    mergeLocalAssistantLlmMetaByToolCallFlow(uiMessages, localAssistantLlmMetaRecords);
  }
  const restorePhases: HydrationRestorePhase[] = [
    // Persisted transcript supplements restore first so snapshot-backed state is the base.
    {
      name: "persisted_interrupted",
      kind: "anchored",
      candidates: persistedInterruptedRoundMessages,
    },
    {
      name: "persisted_compression",
      kind: "anchored",
      candidates: persistedCompressionMessages,
    },
    {
      name: "persisted_compact",
      kind: "anchored",
      candidates: persistedCompactRestoreCandidates,
    },
    {
      name: "persisted_error",
      kind: "error",
      candidates: persistedErrorMessages,
    },
    // Same-session local supplements restore after persisted ones so in-memory UI-only
    // transcript survives polling/hydration races without overriding canonical snapshot data.
    {
      name: "local_interrupted",
      kind: "anchored",
      candidates: dedupeAnchoredAssistantRoundMessages(localInterruptedRoundMessages, {
        include: () => true,
        limit: INTERRUPTED_PERSISTED_MESSAGE_LIMIT,
      }),
    },
    {
      name: "local_compression",
      kind: "anchored",
      candidates: localCompressionMessages,
    },
    {
      name: "local_compact",
      kind: "anchored",
      candidates: localCompactMessages,
    },
    {
      name: "local_error",
      kind: "error",
      candidates: dedupeErrorRestoreCandidates(
        localErrorAssistants.map((message) => ({ message, anchorSig: null })),
      ),
    },
  ];

  store.replaceMessages(uiMessages);
  applyHydrationRestorePhases(store, restorePhases);
  if (sameSession) restoreLocalThinkingCollapsedState(store, localThinkingCollapsedState);
  if (preserveLocalSessionHistoryDirty) store.state.sessionHistoryDirty = true;
  store.clearRevertedMessages();
  store.state.selectedToolCallId = null;
  store.state.runningToolCallId = null;
  store.state.expandedBashOutputs = new Set();
  store.state.expandedToolOutputLineCounts = new Map();
  store.state.unifiedEditDiffToolCallIds = new Set();
  store.state.layoutDirtyMessageId = null;
  store.state.layoutDirtySeq = 0;
  store.state.paused = null;
  store.state.approval.active = null;
  store.state.approval.pendingAutoRun = null;
  store.state.approval.queue = [];
  store.state.approval.decisions = {};

  const meta = readSessionUiMetadataFromSnapshot(snapshot);
  const resolvedMeta = (() => {
    if (!sameSession || !localSessionMeta) return meta;
    const incomingUpdatedAt = Number(meta.lastUpdatedAt ?? 0);
    const localUpdatedAt = Number(localSessionMeta.lastUpdatedAt ?? 0);
    return localUpdatedAt > incomingUpdatedAt ? localSessionMeta : meta;
  })();
  if (store.state.sessionMeta) {
    const target = store.state.sessionMeta;
    target.queue = [...resolvedMeta.queue];
    target.queueLength = resolvedMeta.queueLength;
    target.status = resolvedMeta.status;
    target.lastUpdatedAt = resolvedMeta.lastUpdatedAt;
    target.lastErrorCode = resolvedMeta.lastErrorCode ?? undefined;
    target.pinnedMessageIds = [...resolvedMeta.pinnedMessageIds];
    target.titleSuggestedAt = resolvedMeta.titleSuggestedAt ?? undefined;
    target.reasoningDiagnosticsByTurn = {
      ...resolvedMeta.reasoningDiagnosticsByTurn,
    };
  }

  const resolvedLastRequestBudget = (() => {
    if (
      sameSession &&
      localLastRequestBudget &&
      (!persistedLastRequestBudget ||
        localLastRequestBudget.updatedAtMs > persistedLastRequestBudget.updatedAtMs)
    ) {
      return localLastRequestBudget;
    }
    return persistedLastRequestBudget;
  })();
  store.state.lastRequestBudget = cloneLastRequestBudget(resolvedLastRequestBudget);

  hydrateInlinePlanMarkdownPreviews(store);

  // Recompute syntax highlighting for restored tool results (e.g. Read tool outputs).
  for (const m of store.state.messages) {
    if (!m || m.role !== "assistant" || !Array.isArray(m.parts)) continue;
    for (const p of m.parts) {
      if (
        p?.type === "tool_result" &&
        p.result &&
        typeof store.scheduleToolResultHighlight === "function"
      )
        store.scheduleToolResultHighlight(String(m.id ?? ""), p.result);
    }
  }

  const u = snapshotUsage(snapshot);
  if (u) {
    const prompt = Number(u.promptTokens ?? u.prompt_tokens);
    const completion = Number(u.completionTokens ?? u.completion_tokens);
    const total = Number(u.totalTokens ?? u.total_tokens);
    if (Number.isFinite(prompt)) store.state.usage.prompt_tokens = prompt;
    if (Number.isFinite(completion)) store.state.usage.completion_tokens = completion;

    // Session snapshots store prompt/completion as the most recent request usage,
    // while totalTokens is cumulative for the full session.
    if (Number.isFinite(total)) {
      store.state.usage.total_tokens = Math.max(0, total);
    } else if (Number.isFinite(prompt) && Number.isFinite(completion)) {
      store.state.usage.total_tokens = Math.max(0, prompt + completion);
    }
  }
}

/**
 * Convert UI messages into backend snapshot messages.
 *
 * This is used for forking/seeding sessions.
 */
export function uiMessagesToSnapshotMessages(
  messages: readonly Message[],
): Record<string, unknown>[] {
  const seed = Array.isArray(messages) ? messages : [];
  return uiMessagesToDimSdkMessages(seed);
}
