import type { ACPMessage, ACPToolCall } from './acp'
import { sanitizeStructuredCloneValue } from '#cli/compat/runtime-host'
import { unwrapStrictRespondText } from '#cli/core/strict-respond'

export type NormalizeAcpHistoryOptions = Readonly<{
  dropTrailingPendingUserInput?: boolean
}>

export type NormalizedSessionMessage
  = | {
    role: 'user'
    content: string
  }
  | {
    role: 'assistant'
    content: string
    reasoning_content?: string
    tool_calls?: ACPToolCall[]
  }
  | {
    role: 'tool'
    tool_call_id: string
    content: string
    isError?: boolean
  }

interface AssistantAccumulator {
  content: string[]
  thinking: string[]
  toolCalls: ACPToolCall[]
  toolCallIds: Set<string>
}

interface ToolCallIdNormalizerState {
  normalizedByOriginal: Map<string, string>
  usedNormalizedIds: Set<string>
}

const ASSISTANT_TOOLCALL_EVENT_TYPES = new Set(['tool_call_end', 'text_delta'])

const TOOL_CALL_ID_ALLOWED_CHARS_RE = /[^\w-]+/g

function asRecord(value: unknown): Record<string, unknown> | null {
  return value && typeof value === 'object'
    ? (value as Record<string, unknown>)
    : null
}

function readString(
  record: Record<string, unknown> | null,
  key: string,
): string | null {
  return typeof record?.[key] === 'string' ? String(record[key]) : null
}

function createToolCallIdNormalizerState(): ToolCallIdNormalizerState {
  return {
    normalizedByOriginal: new Map(),
    usedNormalizedIds: new Set(),
  }
}

function toProviderSafeToolCallId(value: unknown): string {
  const raw = String(value ?? '').trim()
  if (!raw)
    return ''

  const normalized = raw
    .replace(TOOL_CALL_ID_ALLOWED_CHARS_RE, '_')
    .replace(/^_+|_+$/g, '')
    .replace(/_{2,}/g, '_')

  return normalized || 'tool_call'
}

function normalizeToolCallId(
  value: unknown,
  state: ToolCallIdNormalizerState,
): string {
  const raw = String(value ?? '').trim()
  if (!raw)
    return ''

  const cached = state.normalizedByOriginal.get(raw)
  if (cached)
    return cached

  const base = toProviderSafeToolCallId(raw)
  let candidate = base
  let index = 2
  while (state.usedNormalizedIds.has(candidate)) {
    candidate = `${base}_${index}`
    index += 1
  }

  state.normalizedByOriginal.set(raw, candidate)
  state.usedNormalizedIds.add(candidate)
  return candidate
}

function extractTextContent(content: unknown): string {
  if (content == null)
    return ''
  if (typeof content === 'string')
    return content
  if (Array.isArray(content)) {
    const text = content
      .map((item) => {
        if (typeof item === 'string')
          return item
        const entry = asRecord(item)
        if (!entry)
          return ''
        const text = readString(entry, 'text')
        if (text)
          return text
        const nestedContent = readString(entry, 'content')
        if (nestedContent)
          return nestedContent
        return ''
      })
      .filter(Boolean)
      .join('')
    if (text)
      return text
    try {
      return JSON.stringify(content)
    }
    catch {
      return ''
    }
  }
  const contentRecord = asRecord(content)
  if (contentRecord) {
    const text = readString(contentRecord, 'text')
    if (text)
      return text
    if (contentRecord.content != null)
      return extractTextContent(contentRecord.content)
  }
  return String(content)
}

function serializeToolContent(content: unknown): string {
  if (content == null)
    return ''
  if (typeof content === 'string')
    return content
  try {
    return JSON.stringify(content) ?? ''
  }
  catch {
    return extractTextContent(content)
  }
}

function normalizeToolCalls(
  raw: unknown,
  text: string,
  eventType: string,
  toolCallIdState: ToolCallIdNormalizerState,
): ACPToolCall[] {
  if (!Array.isArray(raw))
    return []

  const shouldIncludeToolCalls = ASSISTANT_TOOLCALL_EVENT_TYPES.has(eventType)
    ? !text.trim() || eventType === 'tool_call_end'
    : !eventType && !text.trim()
  if (!shouldIncludeToolCalls)
    return []

  const out: ACPToolCall[] = []
  for (const item of raw) {
    const toolCallRecord = asRecord(item)
    if (!toolCallRecord)
      continue
    const id = normalizeToolCallId(toolCallRecord.id, toolCallIdState)
    const fn = asRecord(toolCallRecord.function)
    const name = String(fn?.name ?? '').trim()
    if (!id || !name)
      continue
    const argsRaw = fn?.arguments
    const sanitizedArgs = argsRaw && typeof argsRaw === 'object'
      ? sanitizeStructuredCloneValue(argsRaw)
      : null
    const args
      = typeof argsRaw === 'string'
        ? argsRaw
        : asRecord(sanitizedArgs)
          ? sanitizedArgs
          : {}
    const thoughtSignature = String(fn?.thoughtSignature ?? '').trim()

    out.push({
      id,
      type: 'function',
      function: {
        name,
        arguments: args,
        ...(thoughtSignature ? { thoughtSignature } : {}),
      },
    })
  }
  return out
}

function createAssistantAccumulator(): AssistantAccumulator {
  return {
    content: [],
    thinking: [],
    toolCalls: [],
    toolCallIds: new Set<string>(),
  }
}

function hasAssistantPayload(acc: AssistantAccumulator): boolean {
  return (
    acc.content.length > 0
    || acc.thinking.length > 0
    || acc.toolCalls.length > 0
  )
}

function appendThinking(
  acc: AssistantAccumulator,
  delta: string,
  eventType: string,
): void {
  if (!delta)
    return
  if (eventType === 'thinking_end') {
    const current = acc.thinking.join('')
    if (!current) {
      acc.thinking.push(delta)
      return
    }
    if (delta.startsWith(current)) {
      acc.thinking = [delta]
      return
    }
    if (!current.startsWith(delta))
      acc.thinking.push(delta)
    return
  }
  acc.thinking.push(delta)
}

function flushAssistant(
  acc: AssistantAccumulator,
  out: NormalizedSessionMessage[],
): void {
  if (!hasAssistantPayload(acc))
    return

  const content = acc.content.join('')
  const reasoning = acc.thinking.join('')
  out.push({
    role: 'assistant',
    content,
    ...(reasoning ? { reasoning_content: reasoning } : {}),
    ...(acc.toolCalls.length > 0 ? { tool_calls: acc.toolCalls } : {}),
  })

  acc.content = []
  acc.thinking = []
  acc.toolCalls = []
  acc.toolCallIds.clear()
}

export function normalizeAcpHistoryToSessionMessages(
  acpMessages: readonly ACPMessage[],
  options?: NormalizeAcpHistoryOptions,
): NormalizedSessionMessage[] {
  const nonSystem = acpMessages.filter(message => message?.role !== 'system')
  const lastUserIdx = (() => {
    for (let i = nonSystem.length - 1; i >= 0; i -= 1) {
      if (nonSystem[i]?.role === 'user')
        return i
    }
    return -1
  })()
  const shouldDropTrailingUser
    = options?.dropTrailingPendingUserInput === true
      && lastUserIdx === nonSystem.length - 1

  const out: NormalizedSessionMessage[] = []
  const assistant = createAssistantAccumulator()
  const toolCallIdState = createToolCallIdNormalizerState()

  for (let i = 0; i < nonSystem.length; i += 1) {
    if (shouldDropTrailingUser && i === lastUserIdx)
      continue

    const message = nonSystem[i]
    if (!message || typeof message !== 'object')
      continue

    if (message.role === 'user') {
      flushAssistant(assistant, out)
      out.push({
        role: 'user',
        content: extractTextContent(message.content),
      })
      continue
    }

    if (message.role === 'tool') {
      flushAssistant(assistant, out)
      const toolCallId = normalizeToolCallId(
        message.tool_call_id,
        toolCallIdState,
      )
      if (!toolCallId)
        continue
      out.push({
        role: 'tool',
        tool_call_id: toolCallId,
        content: serializeToolContent(message.content),
        ...(message.metadata?.error === true ? { isError: true } : {}),
      })
      continue
    }

    if (message.role !== 'assistant')
      continue

    const eventType = String(message.metadata?.originalEventType ?? '').trim()
    const text = unwrapStrictRespondText(extractTextContent(message.content))
    const reasoning = extractTextContent(message.reasoning_content)
    const isThinking
      = message.metadata?.thinking === true || eventType.startsWith('thinking_')

    if (reasoning)
      appendThinking(assistant, reasoning, 'thinking_end')

    if (isThinking && !reasoning)
      appendThinking(assistant, text, eventType)
    else if (text)
      assistant.content.push(text)

    for (const toolCall of normalizeToolCalls(
      message.tool_calls,
      text,
      eventType,
      toolCallIdState,
    )) {
      if (assistant.toolCallIds.has(toolCall.id))
        continue
      assistant.toolCallIds.add(toolCall.id)
      assistant.toolCalls.push(toolCall)
    }
  }

  flushAssistant(assistant, out)
  return out
}
