import type {
  AssistantContentPart,
  ToolCall,
  ToolResult,
  Message as UiMessage,
} from '#cli/core/model'
import type { PersistedSubagentChildSummary } from '#cli/compat/runtime-host'
import { renderFocusFilesMarkdownBlock } from '#cli/core/focus-files'
import { formatHookEvaluationInfoText } from '#cli/core/hook-evaluation-info'
import { unwrapStrictRespondText } from '#cli/core/strict-respond'
import { createLocalThinkingPartId } from '#cli/core/transcript-part-id'
import { canonicalToolName, isReadLikeToolName } from '#cli/core/tool-metadata'
import {
  isErrorAssistantMessage,
  isInterruptedAssistantMessage,
} from '#cli/core/store/acp-transform'
import { truncateReadToolOutput } from '#cli/core/store/read-output'
import {
  isCompactTranscriptMessageId,
  isCompressionTranscriptMessageId,
} from '#cli/core/supplemental-transcript'
import {
  isDelegationRecordedFinalMessage,
  normalizePersistedSubagentChildSummary,
  readPersistedSubagentChildSummaries,
} from '#cli/compat/runtime-host'

function id(prefix: string): string {
  return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`
}

function isPlainObject(v: unknown): v is Record<string, unknown> {
  return v != null && typeof v === 'object' && !Array.isArray(v)
}

function asRecord(value: unknown): Record<string, unknown> {
  return isPlainObject(value) ? value : {}
}

function isAbsPath(p: string): boolean {
  return p.startsWith('/') || /^[A-Z]:[\\/]/i.test(p)
}

function basenameFromPath(path: string): string {
  return path.split(/[/\\]/).filter(Boolean).pop() ?? path
}

function escapeMarkdownLinkName(name: string): string {
  return name.replace(/\[/g, '\\[').replace(/\]/g, '\\]')
}

function escapeMarkdownLinkPath(path: string): string {
  return path.replace(/\)/g, '\\)')
}

function parseLeadingFocusFilesBlock(text: string): {
  content: string
  focusFiles?: readonly string[]
} {
  const normalized = String(text ?? '').replace(/\r/g, '')
  if (!normalized)
    return { content: '' }

  const lines = normalized.split('\n')
  const header = String(lines[0] ?? '').trim()
  if (header !== '[focus_files]' && header !== '[user_focus_files]') {
    const parseInlinePrefix = (
      raw: string,
    ): { content: string, focusFiles?: readonly string[] } | null => {
      const s = String(raw ?? '')
      if (!s)
        return null

      const focusFiles: string[] = []
      let idx = 0

      while (idx < s.length) {
        while (
          idx < s.length
          && (s[idx] === ' ' || s[idx] === '\t' || s[idx] === '\n')
        ) {
          idx++
        }

        const m = s.slice(idx).match(/^\[([^\]]*)\]\(((?:\\\)|[^)])+)\)/)
        if (!m)
          break

        let path = String(m[2] ?? '').trim()
        if (path.startsWith('<') && path.endsWith('>'))
          path = path.slice(1, -1)
        path = path.replace(/\\\)/g, ')').trim()

        const isAbs = path.startsWith('/') || /^[A-Z]:[\\/]/i.test(path)
        if (!isAbs)
          break

        if (path)
          focusFiles.push(path)
        idx += m[0].length
      }

      if (!focusFiles.length)
        return null

      let rest = s.slice(idx)
      rest = rest.replace(/^[ \t]+/g, '')
      if (rest.startsWith('\n\n'))
        rest = rest.slice(2)
      else if (rest.startsWith('\n'))
        rest = rest.slice(1)
      else if (rest.startsWith(' '))
        rest = rest.slice(1)

      return { content: rest, focusFiles }
    }

    const inlineParsed = parseInlinePrefix(normalized)
    if (inlineParsed)
      return inlineParsed

    // Also support the ACP "focus files prefix" format:
    // one markdown link per line, with absolute paths, followed by a blank line (or EOF).
    //
    // Example:
    // [foo.ts](/abs/foo.ts)
    //
    // hello
    const focusFiles: string[] = []
    let i = 0
    for (; i < lines.length; i++) {
      const line = String(lines[i] ?? '')
      if (!line.trim())
        break

      const m = line.match(/^\s*\[([^\]]*)\]\((.+)\)\s*$/)
      if (!m)
        break

      let path = String(m[2] ?? '').trim()
      if (path.startsWith('<') && path.endsWith('>'))
        path = path.slice(1, -1)
      path = path.replace(/\\\)/g, ')').trim()

      const isAbs = path.startsWith('/') || /^[A-Z]:[\\/]/i.test(path)
      if (!isAbs)
        break

      if (path)
        focusFiles.push(path)
    }

    if (!focusFiles.length)
      return { content: normalized }

    while (i < lines.length && !String(lines[i] ?? '').trim()) i++

    const content = lines.slice(i).join('\n')
    return { content, focusFiles }
  }

  const focusFiles: string[] = []
  let i = 1
  for (; i < lines.length; i++) {
    const line = String(lines[i] ?? '')
    if (!line.trim())
      break

    const m = line.match(/^\s*-\s*\[[^\]]*\]\((.+)\)\s*$/)
    if (!m)
      break

    let path = String(m[1] ?? '').trim()
    if (path.startsWith('<') && path.endsWith('>'))
      path = path.slice(1, -1)
    path = path.replace(/\\\)/g, ')').trim()
    if (path)
      focusFiles.push(path)
  }

  if (!focusFiles.length)
    return { content: normalized }

  while (i < lines.length && !String(lines[i] ?? '').trim()) i++

  const content = lines.slice(i).join('\n')
  return { content, focusFiles }
}

type UserContentPart
  = | { kind: 'text', text: string }
    | { kind: 'image' }
    | { kind: 'file', uri?: string }

function normalizeUserContentParts(content: unknown): UserContentPart[] {
  const out: UserContentPart[] = []

  const push = (part: unknown): void => {
    if (part == null)
      return
    if (Array.isArray(part)) {
      for (const p of part) push(p)
      return
    }
    if (typeof part === 'string') {
      out.push({ kind: 'text', text: part })
      return
    }
    if (isPlainObject(part)) {
      const record = asRecord(part)
      const type = String(record.type ?? '').trim()
      if (type === 'text' || type === 'input_text' || type === 'output_text') {
        out.push({ kind: 'text', text: String(record.text ?? '') })
        return
      }
      if (type === 'image' || type === 'input_image' || type === 'image_url') {
        out.push({ kind: 'image' })
        return
      }
      if (type === 'resource') {
        const uri = String(asRecord(record.resource).uri ?? '').trim()
        out.push({ kind: 'file', uri: uri || undefined })
        return
      }
      if (type === 'input_file') {
        const uri = String(
          record.file_url ?? record.filename ?? '',
        ).trim()
        out.push({ kind: 'file', uri: uri || undefined })
        return
      }
      if (typeof record.text === 'string') {
        out.push({ kind: 'text', text: String(record.text ?? '') })
        return
      }
      return
    }
    out.push({ kind: 'text', text: String(part) })
  }

  push(content)
  return out
}

function stripLeadingFocusSeparator(text: string): string {
  let rest = String(text ?? '')
  rest = rest.replace(/^[ \t]+/g, '')
  if (rest.startsWith('\n\n'))
    rest = rest.slice(2)
  else if (rest.startsWith('\n'))
    rest = rest.slice(1)
  else if (rest.startsWith(' '))
    rest = rest.slice(1)
  return rest
}

function renderFileLabel(index: number, uri?: string): string {
  const raw = String(uri ?? '').trim()
  if (raw && isAbsPath(raw)) {
    const label = basenameFromPath(raw) || `File #${index}`
    const escapedName = escapeMarkdownLinkName(label)
    const escapedPath = escapeMarkdownLinkPath(raw)
    return `[${escapedName}](${escapedPath})`
  }
  return `[File #${index}]`
}

function extractTextFromJsonishString(raw: string): string | null {
  const s = String(raw ?? '').trim()
  if (!s)
    return null
  if (!(s.startsWith('[') || s.startsWith('{')))
    return null
  try {
    const parsed = JSON.parse(s)
    const items = Array.isArray(parsed) ? parsed : [parsed]
    const texts: string[] = []
    for (const item of items) {
      const record = asRecord(item)
      if (record.type !== 'text')
        continue
      const text = String(record.text ?? '').replace(/\r/g, '')
      if (text)
        texts.push(text)
    }
    if (texts.length === 0)
      return null
    return texts.join('\n')
  }
  catch {
    return null
  }
}

function tryParseJsonObject(raw: string): Record<string, unknown> | null {
  const s = String(raw ?? '').trim()
  if (!s)
    return null
  if (!s.startsWith('{'))
    return null
  try {
    const parsed = JSON.parse(s)
    return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
      ? (parsed as Record<string, unknown>)
      : null
  }
  catch {
    return null
  }
}

function extractHookReasonText(result: unknown): string {
  if (typeof result === 'string') {
    const text = result.trim()
    if (!text)
      return ''
    const parsed = tryParseJsonObject(text)
    const reason
      = parsed && typeof parsed.reason === 'string' ? parsed.reason.trim() : ''
    return reason || text
  }

  if (result && typeof result === 'object') {
    const obj = result as Record<string, unknown>
    const reason = typeof obj.reason === 'string' ? obj.reason.trim() : ''
    if (reason)
      return reason
    const message = typeof obj.message === 'string' ? obj.message.trim() : ''
    if (message)
      return message
    try {
      return JSON.stringify(obj)
    }
    catch {
      return String(obj)
    }
  }

  return String(result ?? '').trim()
}

type HookEvaluationRecord = Readonly<{
  type: string
  evaluationId: string
  timestamp: number
  toolCallId: string
  text: string
}>

function extractHookEvaluationsFromMetadata(
  metadata: unknown,
  seenEvaluationIds: Set<string>,
): HookEvaluationRecord[] {
  if (!metadata || typeof metadata !== 'object')
    return []
  const rawByHook = (metadata as Record<string, unknown>)._hookEvaluations
  if (!rawByHook || typeof rawByHook !== 'object')
    return []

  const out: HookEvaluationRecord[] = []
  for (const [hookName, value] of Object.entries(
    rawByHook as Record<string, unknown>,
  )) {
    if (!Array.isArray(value))
      continue
    for (const rawEntry of value) {
      if (!rawEntry || typeof rawEntry !== 'object')
        continue
      const entry = rawEntry as Record<string, unknown>
      const evaluationId = String(entry.evaluationId ?? '').trim()
      if (!evaluationId || seenEvaluationIds.has(evaluationId))
        continue

      const rawTs = Number(entry.timestamp)
      const timestamp = Number.isFinite(rawTs)
        ? Math.max(0, Math.floor(rawTs))
        : 0
      const type = String(entry.type ?? hookName).trim() || hookName
      const toolCallId = String(entry.toolCallId ?? '').trim()
      // Keep transcript ordering stable by only restoring tool-bound hook messages.
      // Non-tool hook evaluations are still available in metadata for diagnostics.
      if (!toolCallId)
        continue
      const text = extractHookReasonText(entry.result)
      if (!text)
        continue
      seenEvaluationIds.add(evaluationId)
      out.push({
        type,
        evaluationId,
        timestamp,
        toolCallId,
        text,
      })
    }
  }

  out.sort((a, b) => {
    if (a.timestamp !== b.timestamp)
      return a.timestamp - b.timestamp
    return a.evaluationId.localeCompare(b.evaluationId)
  })
  return out
}

function insertHookEvaluationMessages(
  messages: UiMessage[],
  records: readonly HookEvaluationRecord[],
): UiMessage[] {
  if (records.length === 0)
    return messages

  // Group by toolCallId
  const recordsByToolCallId = new Map<string, HookEvaluationRecord[]>()
  for (const record of records) {
    if (!record.toolCallId)
      continue
    const existing = recordsByToolCallId.get(record.toolCallId) ?? []
    existing.push(record)
    recordsByToolCallId.set(record.toolCallId, existing)
  }

  // Sort within each group by timestamp
  for (const records of recordsByToolCallId.values()) {
    records.sort((a, b) => {
      if (a.timestamp !== b.timestamp)
        return a.timestamp - b.timestamp
      return a.evaluationId.localeCompare(b.evaluationId)
    })
  }

  // Build toolCallId -> assistantIndex mapping
  const toolCallToAssistantIndex = new Map<string, number>()
  for (let i = 0; i < messages.length; i++) {
    const msg = messages[i]
    if (msg?.role !== 'assistant')
      continue
    for (const part of msg.parts) {
      if (part.type === 'tool_call' && part.call?.id)
        toolCallToAssistantIndex.set(part.call.id, i)
    }
  }

  // Add hook evaluations as info parts to corresponding assistants
  const out = messages.slice()
  for (const [toolCallId, hookRecords] of recordsByToolCallId) {
    const assistantIndex = toolCallToAssistantIndex.get(toolCallId)
    if (assistantIndex === undefined)
      continue

    const assistant = out[assistantIndex]
    if (!assistant || assistant.role !== 'assistant')
      continue
    const hasMarkdownOutput = assistant.parts.some(
      p => p.type === 'markdown' && Boolean(String(p.markdown ?? '').trim()),
    )
    if (hasMarkdownOutput)
      continue

    // Create info parts
    const infoParts: Array<{ type: 'info', text: string }> = hookRecords.map(
      record => ({
        type: 'info',
        text: formatHookEvaluationInfoText(record.text),
      }),
    )

    // Insert position: after the corresponding tool_call part
    const parts = assistant.parts.slice()
    const toolCallIndex = parts.findIndex(
      p => p.type === 'tool_call' && p.call?.id === toolCallId,
    )

    if (toolCallIndex >= 0) {
      parts.splice(toolCallIndex + 1, 0, ...infoParts)
      out[assistantIndex] = { ...assistant, parts }
    }
  }

  return out
}

export function extractTextContent(content: unknown): string {
  if (content == null)
    return ''
  if (typeof content === 'string')
    return content
  if (Array.isArray(content))
    return content.map(extractTextContent).join('')
  if (isPlainObject(content)) {
    const record = asRecord(content)
    if (typeof record.text === 'string')
      return String(record.text)
    if (String(record.type ?? '') === 'image')
      return '[image]'
    try {
      return JSON.stringify(content)
    }
    catch {
      return String(content)
    }
  }
  return String(content)
}

function extractUserTextContent(content: unknown): string {
  if (content == null)
    return ''
  if (typeof content === 'string')
    return content

  const parts = normalizeUserContentParts(content)
  if (parts.length === 0)
    return extractTextContent(content)

  let imageIndex = 0
  let fileIndex = 0
  const prefixLabels: string[] = []
  const restSegments: string[] = []
  let inPrefix = true
  let pendingWhitespace = ''

  for (const part of parts) {
    if (part.kind === 'text') {
      const text = String(part.text ?? '')
      if (!text)
        continue
      if (inPrefix) {
        if (!text.trim()) {
          if (prefixLabels.length > 0) {
            pendingWhitespace += text
          }
          else {
            inPrefix = false
            restSegments.push(text)
          }
          continue
        }

        inPrefix = false
        const combined
          = prefixLabels.length > 0
            ? stripLeadingFocusSeparator(pendingWhitespace + text)
            : pendingWhitespace + text
        pendingWhitespace = ''
        if (combined)
          restSegments.push(combined)
        continue
      }
      restSegments.push(text)
      continue
    }

    const label
      = part.kind === 'image'
        ? `[Image #${++imageIndex}]`
        : renderFileLabel(++fileIndex, part.uri)

    if (inPrefix)
      prefixLabels.push(label)
    else restSegments.push(label)
  }

  const rest = restSegments.join('')
  if (inPrefix) {
    if (prefixLabels.length === 0)
      return pendingWhitespace
    return prefixLabels.join(' ')
  }
  if (!prefixLabels.length)
    return rest
  if (!rest)
    return prefixLabels.join(' ')
  const sep = rest.startsWith('\n') ? '' : ' '
  return `${prefixLabels.join(' ')}${sep}${rest}`
}

function normalizeToolArguments(raw: unknown): Record<string, unknown> {
  if (isPlainObject(raw))
    return raw
  if (typeof raw === 'string') {
    const s = raw.trim()
    if (!s)
      return {}
    try {
      const parsed = JSON.parse(s)
      return isPlainObject(parsed) ? parsed : { _raw: raw }
    }
    catch {
      return { _raw: raw }
    }
  }
  return {}
}

function snapshotTranscriptMetadata(raw: unknown): {
  statusText?: string
  transcriptKind?: string
} {
  if (!raw || typeof raw !== 'object')
    return {}
  const obj = raw as Record<string, unknown>
  const statusText
    = typeof obj.uiStatusText === 'string' && obj.uiStatusText.trim()
      ? obj.uiStatusText.trim()
      : undefined
  const transcriptKind
    = typeof obj.uiTranscriptKind === 'string' && obj.uiTranscriptKind.trim()
      ? obj.uiTranscriptKind.trim()
      : undefined
  return { statusText, transcriptKind }
}

function restoredTranscriptMessageId(
  transcriptKind: string | undefined,
  storedId: string,
  fallbackId: string,
): string {
  const base = storedId || fallbackId
  if (transcriptKind === 'compact')
    return `restored-compact-${base}`
  if (transcriptKind === 'compression')
    return `restored-compression-${base}`
  return base
}

function assistantTranscriptKind(
  msg: Extract<UiMessage, { role: 'assistant' }>,
): string | undefined {
  const messageId = String(msg.id ?? '').trim()
  if (isCompactTranscriptMessageId(messageId))
    return 'compact'
  if (isCompressionTranscriptMessageId(messageId))
    return 'compression'
  if (isInterruptedAssistantMessage(msg))
    return 'interrupted'
  if (isErrorAssistantMessage(msg))
    return 'error'
  return undefined
}

function buildAssistantSnapshotMetadata(
  statusText: string | undefined,
  transcriptKind: string | undefined,
): Record<string, unknown> | undefined {
  const metadata = {
    ...(statusText?.trim() ? { uiStatusText: String(statusText).trim() } : {}),
    ...(transcriptKind ? { uiTranscriptKind: transcriptKind } : {}),
  }
  return Object.keys(metadata).length > 0 ? metadata : undefined
}

function splitAssistantAroundToolPhase(
  msg: Extract<UiMessage, { role: 'assistant' }>,
): {
  leadingThinking: string
  trailingThinking: string
  trailingMarkdown: string
  shouldSplitTrailingAssistant: boolean
} {
  const contentParts = msg.parts.filter(
    (
      part,
    ): part is Extract<
      (typeof msg.parts)[number],
      | { type: 'thinking' }
      | { type: 'markdown' }
      | { type: 'tool_call' }
      | { type: 'tool_result' }
    > =>
      part.type === 'thinking'
      || part.type === 'markdown'
      || part.type === 'tool_call'
      || part.type === 'tool_result',
  )

  let lastToolPhaseIndex = -1
  for (let i = 0; i < contentParts.length; i++) {
    const part = contentParts[i]
    if (part.type === 'tool_call' || part.type === 'tool_result')
      lastToolPhaseIndex = i
  }

  if (lastToolPhaseIndex < 0) {
    return {
      leadingThinking: msg.parts
        .filter(
          (p): p is Extract<typeof p, { type: 'thinking' }> =>
            p.type === 'thinking',
        )
        .map(p => String(p.content ?? ''))
        .join(''),
      trailingThinking: '',
      trailingMarkdown: '',
      shouldSplitTrailingAssistant: false,
    }
  }

  const leadingThinking = contentParts
    .slice(0, lastToolPhaseIndex + 1)
    .filter(
      (part): part is Extract<typeof part, { type: 'thinking' }> =>
        part.type === 'thinking',
    )
    .map(part => String(part.content ?? ''))
    .join('')

  const trailingThinking = contentParts
    .slice(lastToolPhaseIndex + 1)
    .filter(
      (part): part is Extract<typeof part, { type: 'thinking' }> =>
        part.type === 'thinking',
    )
    .map(part => String(part.content ?? ''))
    .join('')

  const trailingMarkdown = contentParts
    .slice(lastToolPhaseIndex + 1)
    .filter(
      (part): part is Extract<typeof part, { type: 'markdown' }> =>
        part.type === 'markdown',
    )
    .map(part => String(part.markdown ?? ''))
    .join('')

  return {
    leadingThinking,
    trailingThinking,
    trailingMarkdown,
    shouldSplitTrailingAssistant: Boolean(
      trailingThinking.trim() || trailingMarkdown.trim(),
    ),
  }
}

function hasFollowingMatchingToolMessage(
  messages: readonly UiMessage[],
  startIndex: number,
  toolCallIds: readonly string[],
): boolean {
  if (toolCallIds.length === 0)
    return false

  const ids = new Set(
    toolCallIds.map(id => String(id ?? '').trim()).filter(Boolean),
  )
  if (ids.size === 0)
    return false

  for (let i = startIndex + 1; i < messages.length; i++) {
    const next = messages[i]
    if (!next)
      continue
    if (next.role === 'tool') {
      const resultId = String(next.result?.toolCallId ?? '').trim()
      if (resultId && ids.has(resultId))
        return true
      continue
    }
    if (next.role === 'assistant' || next.role === 'user')
      break
  }

  return false
}

function subagentStateLabel(state: string): string {
  switch (state) {
    case 'completed':
      return 'done'
    case 'failed':
      return 'failed'
    case 'cancelled':
      return 'cancelled'
    default:
      return state
  }
}

type SnapshotSubagentTask = Readonly<{
  taskId: string
  taskName: string
  state: string
  summary: string
}>

function createSubagentInfoPart(
  text: string,
  kind: 'header' | 'finished',
  subagentId: string,
): AssistantContentPart {
  return {
    type: 'info',
    text,
    _subagent: kind,
    _subagentId: subagentId,
  } as AssistantContentPart
}

function extractSubagentMetadata(message: unknown): SnapshotSubagentTask[] | null {
  const metadata = asRecord(asRecord(message).metadata)
  if (!metadata._subagentResult || !Array.isArray(metadata._subagentTasks))
    return null
  const tasks = metadata._subagentTasks
    .map((entry) => {
      const task = asRecord(entry)
      const taskId = String(task.taskId ?? '').trim()
      const taskName = String(task.taskName ?? '').trim()
      const state = String(task.state ?? '').trim()
      const summary = String(task.summary ?? '').trim()
      if (!taskId && !taskName && !state && !summary)
        return null
      return { taskId, taskName, state, summary } as const
    })
    .filter((task): task is SnapshotSubagentTask => Boolean(task))
  return tasks.length > 0 ? tasks : null
}

const DELEGATE_TASKS_TOOL_NAME = 'delegate_tasks'

type DelegationTaskMatcher = Readonly<{
  labels: readonly string[]
  strict: boolean
}>

type DelegationSnapshotPlan = Readonly<{
  assistantSummariesByMessageIndex: ReadonlyMap<
    number,
    readonly PersistedSubagentChildSummary[]
  >
  consumedToolMessageIndexes: ReadonlySet<number>
  stripPlaceholderAssistantIndexes: ReadonlySet<number>
}>

function resolveTranscriptSnapshot(snapshot: unknown): Record<string, unknown> {
  const record = asRecord(snapshot)
  return isPlainObject(record.snapshot) ? (record.snapshot as Record<string, unknown>) : record
}

function normalizeDelegationLabel(value: unknown): string {
  return String(value ?? '')
    .trim()
    .replace(/[_-]+/g, ' ')
    .replace(/\s+/g, ' ')
    .toLowerCase()
}

function formatPersistedSubagentHeader(
  summary: PersistedSubagentChildSummary,
): string {
  const summaryText = String(summary.summary ?? '').trim()
  const taskName = String(summary.taskName ?? '').trim()
  const taskId = String(summary.taskId ?? '').trim()
  if (
    summaryText
    && taskName
    && normalizeDelegationLabel(summaryText) !== normalizeDelegationLabel(taskName)
  ) {
    return `${taskName}: ${summaryText}`
  }
  return summaryText || taskName || taskId
}

function readRuntimeSubagentChildSummaries(
  metadata: unknown,
): PersistedSubagentChildSummary[] {
  const runtimeMetadata = asRecord(asRecord(metadata).dimAgentRuntime).metadata
  return readPersistedSubagentChildSummaries(runtimeMetadata)
}

function readExplicitSubagentSummaries(
  value: unknown,
): PersistedSubagentChildSummary[] {
  if (!Array.isArray(value))
    return []
  return value
    .map(entry => normalizePersistedSubagentChildSummary(entry))
    .filter(
      (entry): entry is PersistedSubagentChildSummary => Boolean(entry),
    )
}

function readSessionSubagentSummariesFromSnapshot(
  snapshot: unknown,
  transcriptSnapshot: unknown,
): PersistedSubagentChildSummary[] {
  const summaries: PersistedSubagentChildSummary[] = []
  const seenSessionIds = new Set<string>()
  const append = (entries: readonly PersistedSubagentChildSummary[]): void => {
    for (const entry of entries) {
      if (seenSessionIds.has(entry.sessionId))
        continue
      seenSessionIds.add(entry.sessionId)
      summaries.push(entry)
    }
  }

  const snapshotRecord = asRecord(snapshot)
  const transcriptRecord = asRecord(transcriptSnapshot)
  append(readExplicitSubagentSummaries(snapshotRecord.subagents))
  if (transcriptRecord !== snapshotRecord)
    append(readExplicitSubagentSummaries(transcriptRecord.subagents))
  append(readPersistedSubagentChildSummaries(snapshotRecord.metadata))
  append(readRuntimeSubagentChildSummaries(snapshotRecord.metadata))
  if (transcriptRecord !== snapshotRecord) {
    append(readPersistedSubagentChildSummaries(transcriptRecord.metadata))
    append(readRuntimeSubagentChildSummaries(transcriptRecord.metadata))
  }

  return summaries
}

function isDelegateTasksToolMessage(message: unknown): boolean {
  const record = asRecord(message)
  return String(record.toolName ?? record.tool_name ?? '').trim()
    === DELEGATE_TASKS_TOOL_NAME
}

function readDelegationTaskMatchers(message: unknown): DelegationTaskMatcher[] {
  const structured = asRecord(asRecord(message).structuredContent)
  const tasks = Array.isArray(structured.tasks) ? structured.tasks : []
  return tasks.map((entry, index) => {
    const task = asRecord(entry)
    const labels = [
      typeof task.description === 'string' ? task.description : '',
      typeof task.successCriteria === 'string' ? task.successCriteria : '',
      `Subagent ${index + 1}`,
    ]
      .map(label => normalizeDelegationLabel(label))
      .filter(Boolean)
    const strict = labels.some(label => label !== normalizeDelegationLabel(`Subagent ${index + 1}`))
    return { labels, strict }
  })
}

function matchesDelegationTaskSummary(
  summary: PersistedSubagentChildSummary,
  matcher: DelegationTaskMatcher,
): boolean {
  if (matcher.labels.length === 0)
    return false
  const candidates = [
    summary.summary,
    summary.taskName,
    summary.taskId,
  ]
    .map(value => normalizeDelegationLabel(value))
    .filter(Boolean)
  return candidates.some(candidate => matcher.labels.includes(candidate))
}

function takeDelegationBatchSummaries(
  remaining: PersistedSubagentChildSummary[],
  matchers: readonly DelegationTaskMatcher[],
): PersistedSubagentChildSummary[] {
  const batch: PersistedSubagentChildSummary[] = []
  for (const matcher of matchers) {
    const next = remaining[0]
    if (!next)
      break
    if (matchesDelegationTaskSummary(next, matcher)) {
      batch.push(remaining.shift()!)
      continue
    }
    if (matcher.strict)
      break
    batch.push(remaining.shift()!)
  }
  return batch
}

function findDelegationToolMessageIndex(
  messages: readonly unknown[],
  startIndex: number,
): number {
  for (let index = startIndex; index < messages.length; index++) {
    const role = String(asRecord(messages[index]).role ?? '')
    if (role === 'tool') {
      if (isDelegateTasksToolMessage(messages[index]))
        return index
      continue
    }
    if (role === 'assistant' || role === 'user')
      break
  }
  return -1
}

function buildDelegationSnapshotPlan(
  snapshot: unknown,
  transcriptSnapshot: unknown,
  messages: readonly unknown[],
): DelegationSnapshotPlan {
  const assistantSummariesByMessageIndex = new Map<
    number,
    readonly PersistedSubagentChildSummary[]
  >()
  const consumedToolMessageIndexes = new Set<number>()
  const stripPlaceholderAssistantIndexes = new Set<number>()
  const remainingSummaries = readSessionSubagentSummariesFromSnapshot(
    snapshot,
    transcriptSnapshot,
  )

  for (let index = 0; index < messages.length; index++) {
    const message = asRecord(messages[index])
    if (String(message.role ?? '') !== 'assistant')
      continue
    const text = extractTextContent(message.content)
    if (!isDelegationRecordedFinalMessage(text))
      continue

    const toolMessageIndex = findDelegationToolMessageIndex(messages, index + 1)
    if (toolMessageIndex < 0)
      continue

    stripPlaceholderAssistantIndexes.add(index)
    const batchSummaries = takeDelegationBatchSummaries(
      remainingSummaries,
      readDelegationTaskMatchers(messages[toolMessageIndex]),
    )
    assistantSummariesByMessageIndex.set(index, batchSummaries)
    if (batchSummaries.length > 0)
      consumedToolMessageIndexes.add(toolMessageIndex)
  }

  return {
    assistantSummariesByMessageIndex,
    consumedToolMessageIndexes,
    stripPlaceholderAssistantIndexes,
  }
}

function findLastAssistantMessage(out: UiMessage[]): UiMessage | null {
  for (let i = out.length - 1; i >= 0; i--) {
    if (out[i].role === 'assistant')
      return out[i]
  }
  return null
}

/**
 * Convert a GoatChain `SessionSnapshot` (stored in `session.json`) into UI messages.
 *
 * Note: UI keeps tool outputs in both:
 * - separate `{ role: 'tool' }` messages (used when building ACP history)
 * - `assistant.parts: tool_result` (used for transcript rendering)
 */
export function snapshotToUiMessages(snapshot: unknown): UiMessage[] {
  const transcriptSnapshot = resolveTranscriptSnapshot(snapshot)
  const transcriptContext = asRecord(transcriptSnapshot.context)
  const rawMessages = transcriptSnapshot.messages
  const raw = Array.isArray(rawMessages)
    ? rawMessages
    : transcriptContext.messages
  const messages = Array.isArray(raw) ? raw : []
  const delegationPlan = buildDelegationSnapshotPlan(
    snapshot,
    transcriptSnapshot,
    messages,
  )

  const out: UiMessage[] = []
  const assistantIndexByToolCallId = new Map<string, number>()
  const toolNameByToolCallId = new Map<string, string>()
  const toolCallIdsWithToolMessages = new Set<string>()

  // Only render tool_calls in restored transcripts when we have evidence they were actually executed.
  // Some upstream clients/models may mistakenly attach `tool_calls` to plain assistant text. Persisted
  // snapshots can then incorrectly show tool_call UI styling on restore. A simple, robust signal for
  // "real" tool calls is the presence of a corresponding tool message with `tool_call_id`.
  // Exception: if the assistant content is empty, tool_calls are likely intentional and should render.
  for (let messageIndex = 0; messageIndex < messages.length; messageIndex++) {
    const message = asRecord(messages[messageIndex])
    if (String(message.role ?? '') !== 'tool')
      continue
    const toolCallId = String(
      message.tool_call_id ?? message.toolCallId ?? '',
    ).trim()
    if (toolCallId)
      toolCallIdsWithToolMessages.add(toolCallId)
  }

  for (let messageIndex = 0; messageIndex < messages.length; messageIndex++) {
    const message = asRecord(messages[messageIndex])
    const role = String(message.role ?? '')
    const storedId = String(message.id ?? '').trim()
    const fallbackId = id('m')

    if (role === 'user') {
      const subagentTasks = extractSubagentMetadata(message)
      if (subagentTasks) {
        const lastAssistant = findLastAssistantMessage(out)
        if (lastAssistant && lastAssistant.role === 'assistant') {
          for (const t of subagentTasks) {
            const header = t.summary
              ? `${t.taskName || t.taskId}: ${t.summary}`
              : (t.taskName || t.taskId)
            lastAssistant.parts.push(
              createSubagentInfoPart(header, 'header', t.taskId),
              createSubagentInfoPart(
                subagentStateLabel(t.state),
                'finished',
                t.taskId,
              ),
            )
          }
        }
        continue
      }
      const parsed = parseLeadingFocusFilesBlock(
        extractUserTextContent(message.content),
      )
      out.push({
        id: storedId || fallbackId,
        role: 'user',
        content: parsed.content,
        ...(parsed.focusFiles?.length ? { focusFiles: parsed.focusFiles } : {}),
      })
      continue
    }

    if (role === 'assistant') {
      const transcriptMeta = snapshotTranscriptMetadata(message.metadata)
      const parts: AssistantContentPart[] = []
      const thinking = extractTextContent(
        message.thinking ?? message.reasoning_content,
      )
        .replace(/\r/g, '')
        .trim()
      if (thinking) {
        parts.push({
          type: 'thinking',
          partId: createLocalThinkingPartId(
            String(message.id ?? messageIndex),
            0,
          ),
          title: 'Thinking',
          collapsed: true,
          content: thinking,
        } as const)
      }
      const text = unwrapStrictRespondText(extractTextContent(message.content))
      const stripPlaceholder
        = delegationPlan.stripPlaceholderAssistantIndexes.has(messageIndex)
      const hasText = Boolean(String(text ?? '').trim())
      if (text && !(stripPlaceholder && isDelegationRecordedFinalMessage(text)))
        parts.push({ type: 'markdown', markdown: text } as const)

      const toolCalls = Array.isArray(message.toolCalls)
        ? message.toolCalls
        : Array.isArray(message.tool_calls)
          ? message.tool_calls
          : []
      for (const tc of toolCalls) {
        const toolCall = asRecord(tc)
        const toolFunction = asRecord(toolCall.function)
        const callId = String(toolCall.id ?? '').trim()
        if (callId && !toolCallIdsWithToolMessages.has(callId) && hasText)
          continue
        const name = String(toolFunction.name ?? 'tool')
        const args = normalizeToolArguments(toolFunction.arguments)
        const thoughtSignature
          = typeof toolFunction.thoughtSignature === 'string'
            ? toolFunction.thoughtSignature
            : undefined
        if (!callId)
          continue
        const call: ToolCall = {
          id: callId,
          name,
          arguments: args,
          ...(typeof toolCall.rawArgumentsText === 'string'
            ? { argumentsText: toolCall.rawArgumentsText }
            : {}),
          ...(thoughtSignature ? { thoughtSignature } : {}),
        }
        parts.push({ type: 'tool_call', call } as const)
        assistantIndexByToolCallId.set(callId, out.length)
        toolNameByToolCallId.set(callId, name)
      }

      // Keep the same shape as live messages: an (inert) status part is always present.
      const assistantMessage = {
        id: restoredTranscriptMessageId(
          transcriptMeta.transcriptKind,
          storedId,
          fallbackId,
        ),
        role: 'assistant',
        parts: [
          { type: 'status', text: transcriptMeta.statusText ?? '' } as const,
          ...parts,
        ],
      } satisfies UiMessage
      const batchSummaries
        = delegationPlan.assistantSummariesByMessageIndex.get(messageIndex) ?? []
      if (batchSummaries.length > 0) {
        for (const summary of batchSummaries) {
          const header = formatPersistedSubagentHeader(summary)
          const finishedLabel = subagentStateLabel(summary.state ?? '')
          if (header) {
            assistantMessage.parts.push(
              createSubagentInfoPart(
                header,
                'header',
                summary.sessionId,
              ),
            )
          }
          if (finishedLabel) {
            assistantMessage.parts.push(
              createSubagentInfoPart(
                finishedLabel,
                'finished',
                summary.sessionId,
              ),
            )
          }
        }
      }
      const hasVisibleParts = assistantMessage.parts.some(
        part => part.type !== 'status' || Boolean(String(part.text ?? '').trim()),
      )
      if (hasVisibleParts)
        out.push(assistantMessage)
      continue
    }

    if (role === 'tool') {
      if (delegationPlan.consumedToolMessageIndexes.has(messageIndex))
        continue
      const toolCallId = String(
        message.tool_call_id ?? message.toolCallId ?? '',
      ).trim()
      const toolName = toolCallId
        ? toolNameByToolCallId.get(toolCallId) ?? String(message.toolName ?? '')
        : undefined
      const toolNameLower = canonicalToolName(toolName)
      const isReadTool = isReadLikeToolName(toolNameLower)
      const shouldUnwrapJsonText = isReadTool
      const rawContent = message.content
      const unwrapped
        = shouldUnwrapJsonText && typeof rawContent === 'string'
          ? extractTextFromJsonishString(rawContent)
          : null
      const output = (() => {
        const base = unwrapped ?? extractTextContent(rawContent)
        if (isReadTool) {
          const truncated = truncateReadToolOutput(String(base ?? ''), {
            maxLines: 10,
            ellipsisLine: '...',
          })
          return truncated.output
        }
        return base
      })()
      const status: ToolResult['status'] = message.isError ? 'error' : 'success'
      const result: ToolResult = {
        toolCallId: toolCallId || id('tool'),
        output,
        status,
        ...(isPlainObject(message.structuredContent)
          ? { structuredContent: message.structuredContent }
          : {}),
      }

      out.push({ id: storedId || fallbackId, role: 'tool', result })

      const idx = toolCallId
        ? assistantIndexByToolCallId.get(toolCallId)
        : undefined
      if (idx == null)
        continue
      const prev = out[idx]
      if (!prev || prev.role !== 'assistant')
        continue
      out[idx] = {
        ...prev,
        parts: [...prev.parts, { type: 'tool_result', result } as const],
      }
      continue
    }
  }

  const seenEvaluationIds = new Set<string>()
  const hookEvaluations = [
    ...extractHookEvaluationsFromMetadata(
      transcriptSnapshot.metadata,
      seenEvaluationIds,
    ),
    ...extractHookEvaluationsFromMetadata(
      asRecord(transcriptSnapshot.checkpoint).metadata,
      seenEvaluationIds,
    ),
  ]
  return insertHookEvaluationMessages(out, hookEvaluations)
}

/**
 * Convert UI messages into GoatChain session messages (snapshot format).
 *
 * Used for session forking/seeding.
 */
export function uiMessagesToGoatChainMessages(
  messages: readonly UiMessage[],
): Record<string, unknown>[] {
  const out: Record<string, unknown>[] = []
  let deferredAssistant: Record<string, unknown> | null = null
  let deferredToolCallIds = new Set<string>()

  const isDeferredToolMessage = (message: UiMessage | undefined): boolean => {
    if (!deferredAssistant || !message || message.role !== 'tool')
      return false
    const resultId = String(message.result?.toolCallId ?? '').trim()
    return Boolean(resultId) && deferredToolCallIds.has(resultId)
  }

  const flushDeferredAssistant = (): void => {
    if (!deferredAssistant)
      return
    out.push(deferredAssistant)
    deferredAssistant = null
    deferredToolCallIds = new Set<string>()
  }

  for (let index = 0; index < messages.length; index++) {
    const m = messages[index]
    if (!isDeferredToolMessage(m))
      flushDeferredAssistant()

    if (m.role === 'user') {
      const focusBlock = renderFocusFilesMarkdownBlock(m.focusFiles)
      out.push({
        ...(String(m.id ?? '').trim()
          ? { id: String(m.id).trim() }
          : {}),
        role: 'user',
        content: `${focusBlock}${String(m.content ?? '')}`,
      })
      continue
    }
    if (m.role === 'assistant') {
      const split = splitAssistantAroundToolPhase(m)
      const thinking = m.parts
        .filter(
          (p): p is Extract<typeof p, { type: 'thinking' }> =>
            p.type === 'thinking',
        )
        .map(p => String(p.content ?? ''))
        .join('')
      const markdown = m.parts
        .filter(
          (p): p is Extract<typeof p, { type: 'markdown' }> =>
            p.type === 'markdown',
        )
        .map(p => String(p.markdown ?? ''))
        .join('')
      const toolCalls = m.parts.flatMap(p =>
        p.type === 'tool_call' ? [p.call] : [],
      )
      const tool_calls = toolCalls.length
        ? toolCalls.map(c => ({
            id: c.id,
            type: 'function',
            function: {
              name: c.name,
              arguments: c.arguments,
            },
          }))
        : undefined
      const status = m.parts.find(
        (p): p is Extract<typeof p, { type: 'status' }> => p.type === 'status',
      )
      const transcriptKind = assistantTranscriptKind(m)
      const statusText = String(status?.text ?? '').trim() || undefined
      const metadata = buildAssistantSnapshotMetadata(
        statusText,
        transcriptKind,
      )
      const storedId = String(m.id ?? '').trim()
      const toolCallIds = toolCalls
        .map(call => String(call.id ?? '').trim())
        .filter(Boolean)
      const shouldSplitTrailingAssistant
        = !transcriptKind
          && split.shouldSplitTrailingAssistant
          && toolCallIds.length > 0
          && hasFollowingMatchingToolMessage(messages, index, toolCallIds)

      if (shouldSplitTrailingAssistant) {
        out.push({
          ...(storedId ? { id: `${storedId}::toolcalls` } : {}),
          role: 'assistant',
          content: '',
          ...(split.leadingThinking.trim()
            ? { reasoning_content: split.leadingThinking }
            : {}),
          ...(tool_calls ? { tool_calls } : {}),
        })
        deferredAssistant = {
          ...(storedId ? { id: storedId } : {}),
          role: 'assistant',
          content: split.trailingMarkdown,
          ...(split.trailingThinking.trim()
            ? { reasoning_content: split.trailingThinking }
            : {}),
          ...(statusText
            ? {
                metadata: buildAssistantSnapshotMetadata(statusText, undefined),
              }
            : {}),
        }
        deferredToolCallIds = new Set(toolCallIds)
        continue
      }
      out.push({
        ...(storedId ? { id: storedId } : {}),
        role: 'assistant',
        content: markdown,
        ...(thinking.trim() ? { reasoning_content: thinking } : {}),
        ...(tool_calls ? { tool_calls } : {}),
        ...(metadata ? { metadata } : {}),
      })
      continue
    }
    if (m.role === 'tool') {
      out.push({
        ...(String(m.id ?? '').trim()
          ? { id: String(m.id).trim() }
          : {}),
        role: 'tool',
        tool_call_id: String(m.result.toolCallId ?? ''),
        content: String(m.result.output ?? ''),
        ...(m.result.status === 'error' ? { isError: true } : {}),
      })
      if (!isDeferredToolMessage(messages[index + 1]))
        flushDeferredAssistant()
    }
  }
  flushDeferredAssistant()
  return out
}

function toRuntimeTextContent(text: string): Array<{ type: 'text', text: string }> {
  return [{ type: 'text', text: String(text ?? '') }]
}

/**
 * Convert UI messages into native runtime session messages.
 *
 * Used for runtime-native session seeding and migration.
 */
export function uiMessagesToRuntimeMessages(
  messages: readonly UiMessage[],
): Record<string, unknown>[] {
  const out: Record<string, unknown>[] = []
  const toolNameByCallId = new Map<string, string>()

  for (const message of messages) {
    if (!message)
      continue

    if (message.role === 'user') {
      out.push({
        ...(String(message.id ?? '').trim()
          ? { id: String(message.id).trim() }
          : {}),
        role: 'user',
        content: toRuntimeTextContent(
          `${renderFocusFilesMarkdownBlock(message.focusFiles)}${String(message.content ?? '')}`,
        ),
        createdAt: Number(message.createdAtMs ?? Date.now()) || Date.now(),
      })
      continue
    }

    if (message.role === 'assistant') {
      const thinking = message.parts
        .filter(
          (part): part is Extract<typeof part, { type: 'thinking' }> =>
            part.type === 'thinking',
        )
        .map(part => String(part.content ?? ''))
        .join('')
      const markdown = message.parts
        .filter(
          (part): part is Extract<typeof part, { type: 'markdown' }> =>
            part.type === 'markdown',
        )
        .map(part => String(part.markdown ?? ''))
        .join('')
      const toolCalls = message.parts.flatMap((part) => {
        if (part.type !== 'tool_call')
          return []
        toolNameByCallId.set(part.call.id, part.call.name)
        return [{
          id: part.call.id,
          type: 'function',
          function: {
            name: part.call.name,
            arguments: part.call.arguments,
          },
          ...(typeof part.call.argumentsText === 'string'
            ? { rawArgumentsText: part.call.argumentsText }
            : {}),
        }]
      })

      out.push({
        ...(String(message.id ?? '').trim()
          ? { id: String(message.id).trim() }
          : {}),
        role: 'assistant',
        content: toRuntimeTextContent(markdown),
        createdAt: Number(message.createdAtMs ?? Date.now()) || Date.now(),
        ...(thinking.trim() ? { thinking } : {}),
        ...(toolCalls.length ? { toolCalls } : {}),
      })
      continue
    }

    if (message.role === 'tool') {
      const toolCallId = String(message.result?.toolCallId ?? '').trim()
      out.push({
        ...(String(message.id ?? '').trim()
          ? { id: String(message.id).trim() }
          : {}),
        role: 'tool',
        toolCallId,
        toolName: toolNameByCallId.get(toolCallId) ?? 'tool',
        content: toRuntimeTextContent(String(message.result?.output ?? '')),
        createdAt: Number(message.createdAtMs ?? Date.now()) || Date.now(),
        ...(message.result?.structuredContent
          ? { structuredContent: message.result.structuredContent }
          : {}),
        ...(message.result?.status === 'error' ? { isError: true } : {}),
      })
    }
  }

  return out
}
