import type { Message } from '#cli/core/model'
import type { FileRevert } from './file-reverts'
import type { AskUserQuestion } from './types'
import { debugLog } from '#cli/core/debug-log'
import { normalizeText, safeJson } from '#cli/core/utils'
import { t } from '#cli/i18n'
import { normalizeRuntimeToolName } from '#cli/compat/runtime-host'

export type ToolCallInfo = Readonly<{ assistantId: string, toolName: string }>

export type StreamEventMetadata = Record<string, unknown> & {
  tool_call_id?: string
  toolCallId?: string
  callId?: string
  call_id?: string
  tool_name?: string
  toolName?: string
  originalEventType?: string
  session_id?: string
  usage?: unknown
  lastRequestBudget?: unknown
  subagent?: boolean
  subagentId?: string
  subagent_id?: string
  taskDescription?: string
  task_description?: string
  description?: string
  subagentType?: string
  subagent_type?: string
  tool_output_start?: boolean
  tool_output_delta?: boolean
  delta?: string
  argsTextDelta?: string
  linesWritten?: number
  lines_written?: number
  approval_requested?: boolean
  approval_args?: unknown
  approvalArgs?: unknown
  args?: unknown
  risk_level?: string
  requires_action?: boolean
  kind?: string
  action_kind?: string
  actionId?: string
  action_id?: string
  checkpoint?: unknown
  checkpointRef?: unknown
  checkpoint_ref?: unknown
  questions?: AskUserQuestion[]
  done?: boolean
  stopReason?: string
  modelStopReason?: string
  error?: unknown
  modelId?: string
  structuredContent?: unknown
  isComplete?: boolean
  reason?: string
  assessorName?: string
  followUpRound?: number
}

const MAX_REASON_DETAIL_CHARS = 180

export function toSingleLine(text: string): string {
  return normalizeText(text).replace(/\s+/g, ' ').trim()
}

export function truncateText(text: string, max: number): string {
  if (text.length <= max)
    return text
  if (max <= 3)
    return text.slice(0, max)
  return `${text.slice(0, max - 3).trimEnd()}...`
}

export function summarizeApprovalArgs(
  toolName: string,
  args?: Record<string, unknown>,
): string | null {
  if (!args || typeof args !== 'object' || Array.isArray(args))
    return null
  const obj = args as Record<string, unknown>
  const lowerName = normalizeRuntimeToolName(toolName).toLowerCase()
  if (lowerName === 'shell') {
    const command
      = typeof obj.command === 'string'
        ? toSingleLine(obj.command)
        : Array.isArray(obj.argv) && obj.argv.every(value => typeof value === 'string')
            ? toSingleLine((obj.argv as string[]).join(' '))
            : ''
    const description
      = typeof obj.description === 'string' ? toSingleLine(obj.description) : ''
    if (command && description) {
      return t('toolApproval.args.commandWithDescription', {
        command,
        description,
      })
    }
    if (command)
      return t('toolApproval.args.command', { command })
    if (description)
      return t('toolApproval.args.description', { description })
  }
  const json = toSingleLine(safeJson(obj))
  if (!json || json === '{}' || json === 'null')
    return null
  return t('toolApproval.args.raw', { args: json })
}

export function buildApprovalReason(
  base: string,
  toolName: string,
  args?: Record<string, unknown>,
): string {
  const lowerName = normalizeRuntimeToolName(toolName).toLowerCase()
  const detail = summarizeApprovalArgs(toolName, args)
  const baseText = base.trim()
  if (!detail)
    return baseText
  if (lowerName === 'shell')
    return baseText
  if (!baseText)
    return truncateText(detail, MAX_REASON_DETAIL_CHARS)
  return `${baseText} ${truncateText(detail, MAX_REASON_DETAIL_CHARS)}`
}

export function normalizeToolName(
  name: string | undefined | null,
  fallback?: string,
): string {
  const safeFallback = fallback ?? t('chatStatus.toolFallback')
  return String(name ?? '').trim() || safeFallback
}

export function normalizeSubagentId(id: unknown): string {
  return typeof id === 'string' ? id.trim() : ''
}

export function normalizeSubagentType(v: unknown): string {
  return typeof v === 'string' ? v.trim() : ''
}

export function normalizeTaskDescription(v: unknown): string {
  return typeof v === 'string' ? v.trim() : ''
}

export function assertValidAssistantId(id: string): void {
  if (!id || typeof id !== 'string' || !id.trim())
    throw new Error(`Invalid assistant ID: ${JSON.stringify(id)}`)
}

export function assertValidSessionId(id: string | undefined | null): void {
  if (id && typeof id === 'string' && !id.trim())
    throw new Error('Session ID cannot be empty string')
}

export function assistantHasMarkdownOutput(
  messages: readonly Message[],
  assistantId: string,
): boolean {
  const msg = messages.find(m => m.id === assistantId)
  if (!msg || msg.role !== 'assistant')
    return false
  return msg.parts.some(
    p => p.type === 'markdown' && Boolean(String(p.markdown ?? '').trim()),
  )
}

export function formatInterruptedMarkdown(opts: {
  appendToExisting: boolean
}): string {
  const NL = '\n'
  const line = t('chatStatus.interruptedMarkdown')
  if (opts.appendToExisting) {
    return ['', '', line].join(NL)
  }
  return line
}

export function normalizeEventMetadata(
  md: StreamEventMetadata | null | undefined,
): StreamEventMetadata | null | undefined {
  if (!md)
    return md

  // Normalize tool call ID field names
  if (md.tool_call_id && !md.toolCallId) {
    md.toolCallId = md.tool_call_id
  }
  if (md.call_id && !md.callId) {
    md.callId = md.call_id
  }
  if (md.callId && !md.toolCallId) {
    md.toolCallId = md.callId
  }

  // Normalize tool name field names
  if (md.tool_name && !md.toolName) {
    md.toolName = md.tool_name
  }

  // Normalize action metadata field names
  if (md.action_id && !md.actionId) {
    md.actionId = md.action_id
  }
  if (md.action_kind && !md.kind) {
    md.kind = md.action_kind
  }
  if (md.checkpoint_ref && !md.checkpointRef) {
    md.checkpointRef = md.checkpoint_ref
  }

  return md
}

export function normalizeFileRevert(
  input: Record<string, unknown> | null | undefined,
): FileRevert | null {
  if (!input || typeof input !== 'object')
    return null
  const filePath
    = typeof input.filePath === 'string' ? input.filePath.trim() : ''
  if (!filePath)
    return null
  const existedBefore = Boolean(
    (input as Record<string, unknown>).existedBefore
    ?? (input as Record<string, unknown>).existed,
  )
  const previousContent
    = typeof input.previousContent === 'string' ? input.previousContent : null
  const nextContent
    = typeof input.nextContent === 'string' ? input.nextContent : null
  return { filePath, existedBefore, previousContent, nextContent }
}

export function extractToolCallId(
  md: StreamEventMetadata | null | undefined,
  context: { eventType: string, assistantId: string, sessionId: string },
): string {
  const id = String(
    md?.tool_call_id ?? md?.toolCallId ?? md?.callId ?? '',
  ).trim()

  if (!id) {
    debugLog('missing_tool_call_id', {
      session_id: String(context.sessionId ?? ''),
      event_type: context.eventType,
      assistant_id: context.assistantId,
      metadata_keys: Object.keys(md ?? {}).filter(
        k =>
          k.toLowerCase().includes('call') || k.toLowerCase().includes('id'),
      ),
    })
    return ''
  }

  return id
}

export function createToolCallTranscriptLookup(
  getMessages: () => readonly Message[],
): Readonly<{
  findCallInfo: (callId: string) => ToolCallInfo | null
  findArgs: (callId: string) => Record<string, unknown> | null
}> {
  let lastMessageCount = -1
  const cache = new Map<string, ToolCallInfo>()

  const ensureFresh = () => {
    const msgs = getMessages()
    const n = msgs.length
    if (n !== lastMessageCount) {
      cache.clear()
      lastMessageCount = n
    }
    return msgs
  }

  const findCallInfo = (callId: string): ToolCallInfo | null => {
    const id = String(callId ?? '').trim()
    if (!id)
      return null

    ensureFresh()
    const cached = cache.get(id)
    if (cached)
      return cached

    const msgs = getMessages()
    for (let i = msgs.length - 1; i >= 0; i--) {
      const m = msgs[i]
      if (!m || m.role !== 'assistant')
        continue
      for (const p of m.parts) {
        if (p.type === 'tool_call' && p.call.id === id) {
          const toolName = normalizeToolName(p.call.name)
          const info = { assistantId: m.id, toolName }
          cache.set(id, info)
          return info
        }
      }
    }
    return null
  }

  const findArgs = (callId: string): Record<string, unknown> | null => {
    const id = String(callId ?? '').trim()
    if (!id)
      return null
    const msgs = getMessages()
    for (let i = msgs.length - 1; i >= 0; i--) {
      const m = msgs[i]
      if (!m || m.role !== 'assistant')
        continue
      for (const p of m.parts) {
        if (p.type === 'tool_call' && p.call.id === id) {
          const args = p.call.arguments
          if (args && typeof args === 'object' && !Array.isArray(args))
            return args as Record<string, unknown>
          return null
        }
      }
    }
    return null
  }

  return { findCallInfo, findArgs } as const
}

export async function waitOrTimeoutWithAbort<T>(
  p: Promise<T>,
  ms: number,
  abortPromise: Promise<void>,
): Promise<
  { kind: 'value', value: T } | { kind: 'timeout' } | { kind: 'abort' }
> {
  const waitMs = Math.max(0, Math.floor(ms))
  if (waitMs <= 0)
    return { kind: 'timeout' }
  let timer: ReturnType<typeof setTimeout> | null = null
  let abortHappened = false
  const abortUnlink = abortPromise.then(() => {
    abortHappened = true
  })
  try {
    const timeoutP = new Promise<{ kind: 'timeout' }>((resolve) => {
      timer = setTimeout(() => resolve({ kind: 'timeout' }), waitMs)
    })
    const abortP = abortPromise.then(() => ({ kind: 'abort' }) as const)
    const winner = await Promise.race([
      abortP,
      p.then(value => ({ kind: 'value', value }) as const),
      timeoutP,
    ])
    if (abortHappened)
      return { kind: 'abort' }
    return winner
  }
  finally {
    if (timer)
      clearTimeout(timer)
    abortUnlink.catch(() => {
      // ignore
    })
  }
}
