import type { ACPMessage, ACPToolCall } from '#cli/core/acp'
import type { Message, ToolCall } from '#cli/core/model'
import { renderFocusFilesMarkdownLinks } from '#cli/core/focus-files'
import { isCompressionTranscriptMessageId } from '#cli/core/supplemental-transcript'
import {
  isErrorStatusText,
  isInterruptedMarkdownText,
  isInterruptedStatusText,
} from '#cli/i18n'
import { canonicalizeSystemMessages } from '#cli/compat/runtime-host'
import { sanitizeStructuredCloneValue } from '#cli/compat/runtime-host'

type LegacySystemMessage = Readonly<{
  id?: string
  role: 'system'
  content?: unknown
  createdAtMs?: number
  createdAt?: number
  metadata?: Record<string, unknown>
}>

type AcpConversationMessage = Message | LegacySystemMessage

function isRuntimeMessage(message: AcpConversationMessage): message is Message {
  return String(message.role ?? '') !== 'system'
}

function assistantMarkdownText(
  m: Extract<Message, { role: 'assistant' }>,
): string {
  return m.parts
    .filter(
      (p): p is Extract<typeof p, { type: 'markdown' }> =>
        p.type === 'markdown',
    )
    .map(p => String(p.markdown ?? ''))
    .join('\n')
}

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

export function toAcpFromToolCall(call: ToolCall): ACPToolCall {
  return {
    id: call.id,
    type: 'function',
    function: {
      name: call.name,
      arguments: sanitizeStructuredCloneValue(call.arguments) as Record<
        string,
        unknown
      >,
      ...(call.thoughtSignature
        ? { thoughtSignature: call.thoughtSignature }
        : {}),
    },
  }
}

export function toAcpFromMessage(m: Message): ACPMessage | null {
  if (m.role === 'user') {
    const focusLinks = renderFocusFilesMarkdownLinks(m.focusFiles)
    const content = String(m.content ?? '')
    if (focusLinks && content.trim())
      return { role: 'user', content: `${focusLinks}${content}` }
    if (focusLinks)
      return { role: 'user', content: focusLinks }
    return { role: 'user', content }
  }
  if (m.role === 'tool') {
    return {
      role: 'tool',
      tool_call_id: m.result.toolCallId,
      content: m.result.output,
      metadata: m.result.status === 'error' ? { error: true } : undefined,
    }
  }
  const text = m.parts
    .filter(
      (p): p is Extract<typeof p, { type: 'markdown' }> =>
        p.type === 'markdown',
    )
    .map(p => p.markdown)
    .join('')
  const toolCalls = m.parts.flatMap(p =>
    p.type === 'tool_call' ? [p.call] : [],
  )
  const tool_calls = toolCalls.length
    ? toolCalls.map(toAcpFromToolCall)
    : undefined
  if (!text && (!tool_calls || tool_calls.length === 0))
    return null
  return { role: 'assistant', content: text || '', tool_calls }
}

export function isInterruptedAssistantMessage(m: Message): boolean {
  if (m.role !== 'assistant')
    return false

  if (isInterruptedStatusText(assistantStatusText(m)))
    return true

  return isInterruptedMarkdownText(assistantMarkdownText(m))
}

export function isErrorAssistantMessage(m: Message): boolean {
  if (m.role !== 'assistant')
    return false

  if (isErrorStatusText(assistantStatusText(m)))
    return true

  return assistantMarkdownText(m).trimStart().startsWith('# Error')
}

export function isSyntheticErrorAssistantMessage(m: Message): boolean {
  if (m.role !== 'assistant')
    return false
  if (!isErrorAssistantMessage(m))
    return false

  // If a tool call was emitted, keep the assistant message so the agent can see the tool context.
  // This heuristic is intended to drop only our synthetic "request failed" error transcript.
  const hasToolCallParts = m.parts.some(p => p.type === 'tool_call')
  if (hasToolCallParts)
    return false

  return assistantMarkdownText(m).trimStart().startsWith('# Error')
}

function isCompressionTranscriptAssistantMessage(m: Message): boolean {
  if (m.role !== 'assistant')
    return false
  return isCompressionTranscriptMessageId((m as any)?.id)
}

function issuedToolCallIdsFromAssistant(
  m: Extract<Message, { role: 'assistant' }>,
): string[] {
  return m.parts
    .filter(
      (p): p is Extract<typeof p, { type: 'tool_call' }> =>
        p.type === 'tool_call',
    )
    .map(p => String(p.call.id ?? '').trim())
    .filter(Boolean)
}

function toolCallIdsFromAssistant(
  m: Extract<Message, { role: 'assistant' }>,
): string[] {
  const ids: string[] = []
  for (const p of m.parts) {
    if (p.type === 'tool_call')
      ids.push(String(p.call.id ?? '').trim())
    else if (p.type === 'tool_output')
      ids.push(String(p.toolCallId ?? '').trim())
    else if (p.type === 'tool_result')
      ids.push(String(p.result.toolCallId ?? '').trim())
  }
  return ids.filter(Boolean)
}

function dropSupersededIncompleteToolRounds(
  messages: readonly Message[],
): Message[] {
  const assistantIndexesToDrop = new Set<number>()
  const toolIndexesToDrop = new Set<number>()

  for (let i = 0; i < messages.length; i++) {
    const message = messages[i]
    if (!message || message.role !== 'assistant')
      continue

    const callIds = issuedToolCallIdsFromAssistant(message)
    if (callIds.length === 0)
      continue

    const seenToolResults = new Set<string>()
    let hasLaterUserTurn = false

    for (let j = i + 1; j < messages.length; j++) {
      const next = messages[j]
      if (!next)
        continue

      if (next.role === 'tool') {
        const resultId = String(next.result.toolCallId ?? '').trim()
        if (callIds.includes(resultId)) {
          seenToolResults.add(resultId)
        }
        continue
      }

      if (next.role === 'user') {
        hasLaterUserTurn = true
        break
      }

      // Stop at the next assistant turn boundary. A complete tool round may later
      // continue with assistant markdown, but an incomplete tool round should only be
      // scrubbed once a later user turn supersedes it.
      break
    }

    if (!hasLaterUserTurn)
      continue
    if (callIds.every(id => seenToolResults.has(id)))
      continue

    assistantIndexesToDrop.add(i)
    for (let j = i + 1; j < messages.length; j++) {
      const next = messages[j]
      if (!next)
        continue
      if (next.role === 'tool') {
        const resultId = String(next.result.toolCallId ?? '').trim()
        if (callIds.includes(resultId))
          toolIndexesToDrop.add(j)
        continue
      }
      if (next.role === 'user' || next.role === 'assistant')
        break
    }
  }

  if (assistantIndexesToDrop.size === 0 && toolIndexesToDrop.size === 0)
    return [...messages]

  const out: Message[] = []
  for (let i = 0; i < messages.length; i++) {
    if (assistantIndexesToDrop.has(i) || toolIndexesToDrop.has(i))
      continue
    out.push(messages[i]!)
  }
  return out
}

function dropOrphanToolMessages(messages: readonly Message[]): Message[] {
  const issuedToolCallIds = new Set<string>()
  let dropped = false
  const out: Message[] = []

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

    if (message.role === 'assistant') {
      for (const callId of issuedToolCallIdsFromAssistant(message))
        issuedToolCallIds.add(callId)
      out.push(message)
      continue
    }

    if (message.role === 'tool') {
      const resultId = String(message.result.toolCallId ?? '').trim()
      if (!resultId || !issuedToolCallIds.has(resultId)) {
        dropped = true
        continue
      }
    }

    out.push(message)
  }

  return dropped ? out : [...messages]
}

function stripLocalOnlyTranscriptMessages(
  messages: readonly Message[],
): Message[] {
  const out: Message[] = []
  const interruptedToolCallIds = new Set<string>()

  for (const m of messages) {
    if (
      m.role === 'tool'
      && interruptedToolCallIds.has(String(m.result.toolCallId ?? '').trim())
    ) {
      continue
    }

    if (m.role === 'assistant' && isInterruptedAssistantMessage(m)) {
      for (const id of toolCallIdsFromAssistant(m))
        interruptedToolCallIds.add(id)

      continue
    }

    if (m.role === 'assistant' && isCompressionTranscriptAssistantMessage(m))
      continue

    if (m.role === 'assistant' && isSyntheticErrorAssistantMessage(m)) {
      continue
    }

    out.push(m)
  }

  return out
}

/**
 * Filters messages before sending them to the agent:
 * - Drop the synthetic "Interrupted" assistant message
 * - Drop tool messages associated with that interrupted assistant message
 * - Drop synthetic "Error" assistant messages caused by request/stream failures
 * - Drop superseded incomplete tool rounds that were interrupted before all tool
 *   results landed, so the next user turn does not inherit orphaned tool calls
 * - Drop any remaining orphan tool messages that no longer have a retained
 *   assistant tool_call in history
 *
 * These messages remain in the UI transcript.
 */
export function filterMessagesForAcp(messages: readonly Message[]): Message[] {
  const stripped = stripLocalOnlyTranscriptMessages(messages)
  const normalized = dropSupersededIncompleteToolRounds(stripped)
  return dropOrphanToolMessages(normalized)
}

export function buildAcpConversation(
  systemPrompt: string,
  messages: readonly AcpConversationMessage[],
): ACPMessage[] {
  const canonical = canonicalizeSystemMessages({
    systemPrompt,
    messages,
  })
  const out: ACPMessage[] = []
  out.push({ role: 'system', content: canonical.systemPrompt })
  for (const m of filterMessagesForAcp(canonical.messages.filter(isRuntimeMessage))) {
    const acp = toAcpFromMessage(m)
    if (acp)
      out.push(acp)
  }
  return out
}
