import type { DiffPreview, Message, ToolCall } from '#cli/core/model'
import type { ThemeId } from '#cli/core/theme'
import { debugLog } from '#cli/core/debug-log'
import {
  createEnvReader,
  readProcessEnv,
} from '#cli/core/env'
import { isEditLikeToolName } from '#cli/core/tool-metadata'
import { resolveShikiTheme } from '#cli/core/theme'
import { computeDiffPreviewWithShiki } from '../../ui/chat/diff-highlight'
import { createLruCache } from './highlight-cache'

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

type AssistantMessage = Extract<Message, { role: 'assistant' }>
type ToolCallPart = Extract<AssistantMessage['parts'][number], { type: 'tool_call' }>
type DiffHighlightState = Readonly<{
  messages: Message[]
  ui?: {
    theme?: {
      mode?: ThemeId
    }
  }
}>

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

function extractEditOldNew(
  args: Record<string, unknown> | undefined,
): { oldString: string; newString: string; filePath: string | null } | null {
  const a = args ?? {}
  const oldString =
    readString(a, 'old_string')
    ?? readString(a, 'oldString')
    ?? ''
  const newString =
    readString(a, 'new_string')
    ?? readString(a, 'newString')
    ?? ''
  const filePath = readString(a, 'file_path') ?? readString(a, 'filePath')

  if (!oldString && !newString) return null
  return { oldString, newString, filePath }
}

function findToolCallPart(
  message: AssistantMessage,
  callId: string,
): ToolCallPart | null {
  return (
    message.parts.find(
      (part): part is ToolCallPart =>
        part.type === 'tool_call' && part.call.id === callId,
    ) ?? null
  )
}

function findAssistantContainingToolCall(
  messages: readonly Message[],
  callId: string,
): { assistantId: string; call: ToolCall } | null {
  for (const message of messages) {
    if (message.role !== 'assistant') continue
    const part = findToolCallPart(message, callId)
    if (part) return { assistantId: message.id, call: part.call }
  }
  return null
}

function diffPreviewHasAnyTokens(preview: DiffPreview | undefined): boolean {
  if (!preview || preview.kind !== 'shiki')
    return false
  return preview.lines.some(line => line.tokens != null && line.tokens.length > 0)
}

const env = createEnvReader(readProcessEnv())
const inFlight = new Set<string>()
const diffCache = createLruCache<string, DiffPreview>(
  (() => {
    const raw = env.prefixedStr('DIFF_HIGHLIGHT_CACHE')
    const n = Number(raw)
    if (Number.isFinite(n) && n > 0)
      return Math.min(10_000, Math.max(50, Math.floor(n)))
    return 300
  })(),
)

function getTextHash(text: string): string {
  let hash = 0
  const str = String(text ?? '')
  for (let i = 0; i < str.length; i++) {
    const ch = str.charCodeAt(i)
    hash = (hash << 5) - hash + ch
    hash = hash & hash
  }
  return hash.toString(36)
}

function yieldToMacrotask(): Promise<void> {
  return new Promise((resolve) => {
    const si = (
      globalThis as typeof globalThis & {
        setImmediate?: (fn: () => void) => void
      }
    ).setImmediate
    if (typeof si === 'function') si(resolve)
    else setTimeout(resolve, 0)
  })
}

function resolveDiffTheme(state: DiffHighlightState): string {
  return resolveShikiTheme(state.ui?.theme?.mode)
}

export function clearDiffHighlightInFlight(): void {
  inFlight.clear()
  diffCache.clear()
}

export function scheduleToolCallDiffHighlight(
  opts: Readonly<{
    state: DiffHighlightState
    assistantId: string
    callId: string
    toolName: string
    args: Record<string, unknown>
    updateAssistant: (
      assistantId: string,
      fn: (
        m: Extract<Message, { role: 'assistant' }>,
      ) => Extract<Message, { role: 'assistant' }>,
    ) => void
  }>,
): void {
  if (!isEditLikeToolName(opts.toolName)) return

  const extracted = extractEditOldNew(opts.args)
  if (!extracted) return

  const theme = resolveDiffTheme(opts.state)
  const diffHash = `${getTextHash(extracted.oldString)}:${getTextHash(extracted.newString)}`
  const cacheKey = `end:${diffHash}:${extracted.filePath ?? ''}:${theme}`
  const cachedPreview = diffCache.get(cacheKey)
  if (cachedPreview && cachedPreview.kind === 'shiki') {
    opts.updateAssistant(opts.assistantId, (m) => {
      const parts = m.parts.slice()
      const idx = parts.findIndex(
        (p) => p.type === 'tool_call' && p.call.id === opts.callId,
      )
      if (idx < 0) return m

      const prevPart = parts[idx] as Extract<
        (typeof parts)[number],
        { type: 'tool_call' }
      >
      const prevCall = prevPart.call as ToolCall
      if (!isPlainObject(prevCall.arguments)) return m

      const now = extractEditOldNew(prevCall.arguments)
      if (!now) return m
      if (
        now.oldString !== extracted.oldString ||
        now.newString !== extracted.newString
      )
        return m

      if (
        prevCall.diffPreview?.kind === 'shiki' &&
        prevCall.diffPreview.oldString === extracted.oldString &&
        prevCall.diffPreview.newString === extracted.newString &&
        String(prevCall.diffPreview.theme ?? '').trim() === theme &&
        diffPreviewHasAnyTokens(prevCall.diffPreview)
      ) {
        return m
      }

      parts[idx] = {
        type: 'tool_call',
        call: { ...prevCall, diffPreview: cachedPreview },
      } as const
      return { ...m, parts }
    })
    return
  }
  const flightKey = `end:${opts.callId}:${extracted.oldString.length}:${extracted.newString.length}:${extracted.filePath ?? ''}:${theme}`
  if (inFlight.has(flightKey)) return

  void (async () => {
    inFlight.add(flightKey)
    try {
      // Let the UI render plain text first; compute highlighting afterward.
      await yieldToMacrotask()
      const preview = await computeDiffPreviewWithShiki(
        extracted.oldString,
        extracted.newString,
        {
          filePath: extracted.filePath ?? undefined,
          theme,
        },
      )
      if (!preview) return
      diffCache.set(cacheKey, preview)

      opts.updateAssistant(opts.assistantId, (m) => {
        const parts = m.parts.slice()
        const idx = parts.findIndex(
          (p) => p.type === 'tool_call' && p.call.id === opts.callId,
        )
        if (idx < 0) return m

        const prevPart = parts[idx] as Extract<
          (typeof parts)[number],
          { type: 'tool_call' }
        >
        const prevCall = prevPart.call as ToolCall
        if (!isPlainObject(prevCall.arguments)) return m

        const now = extractEditOldNew(prevCall.arguments)
        if (!now) return m
        if (
          now.oldString !== extracted.oldString ||
          now.newString !== extracted.newString
        )
          return m

        if (
          prevCall.diffPreview?.kind === 'shiki' &&
          prevCall.diffPreview.oldString === extracted.oldString &&
          prevCall.diffPreview.newString === extracted.newString &&
          String(prevCall.diffPreview.theme ?? '').trim() === theme &&
          diffPreviewHasAnyTokens(prevCall.diffPreview)
        ) {
          return m
        }

        parts[idx] = {
          type: 'tool_call',
          call: { ...prevCall, diffPreview: preview },
        } as const
        return { ...m, parts }
      })
    } catch (e) {
      debugLog('diff_highlight_error', {
        tool: 'edit-workspace',
        callId: opts.callId,
        assistantId: opts.assistantId,
        filePath: extracted.filePath,
        error:
          e instanceof Error
            ? { message: e.message, stack: e.stack }
            : String(e),
      })
    } finally {
      inFlight.delete(flightKey)
    }
  })()
}

export function scheduleToolCallDiffHighlightByCallId(
  opts: Readonly<{
    state: DiffHighlightState
    callId: string
    updateAssistant: (
      assistantId: string,
      fn: (
        m: Extract<Message, { role: 'assistant' }>,
      ) => Extract<Message, { role: 'assistant' }>,
    ) => void
  }>,
): void {
  const callId = String(opts.callId ?? '').trim()
  if (!callId) return

  const found = findAssistantContainingToolCall(opts.state.messages, callId)
  if (!found) return

  if (!isEditLikeToolName(found.call.name)) return

  const extracted = extractEditOldNew(
    isPlainObject(found.call.arguments) ? found.call.arguments : undefined,
  )
  if (!extracted) return

  const theme = resolveDiffTheme(opts.state)
  if (
    found.call.diffPreview?.kind === 'shiki' &&
    found.call.diffPreview.oldString === extracted.oldString &&
    found.call.diffPreview.newString === extracted.newString &&
    String(found.call.diffPreview.theme ?? '').trim() === theme &&
    diffPreviewHasAnyTokens(found.call.diffPreview)
  ) {
    return
  }

  const diffHash = `${getTextHash(extracted.oldString)}:${getTextHash(extracted.newString)}`
  const cacheKey = `sel:${diffHash}:${extracted.filePath ?? ''}:${theme}`
  const cachedPreview = diffCache.get(cacheKey)
  if (cachedPreview && cachedPreview.kind === 'shiki') {
    opts.updateAssistant(found.assistantId, (m) => {
      const parts = m.parts.slice()
      const idx = parts.findIndex(
        (p) => p.type === 'tool_call' && p.call.id === callId,
      )
      if (idx < 0) return m

      const prevPart = parts[idx] as Extract<
        (typeof parts)[number],
        { type: 'tool_call' }
      >
      const prevCall = prevPart.call as ToolCall
      if (!isEditLikeToolName(prevCall.name))
        return m
      if (!isPlainObject(prevCall.arguments)) return m

      const now = extractEditOldNew(prevCall.arguments)
      if (!now) return m
      if (
        now.oldString !== extracted.oldString ||
        now.newString !== extracted.newString
      )
        return m

      if (
        prevCall.diffPreview?.kind === 'shiki' &&
        prevCall.diffPreview.oldString === extracted.oldString &&
        prevCall.diffPreview.newString === extracted.newString &&
        String(prevCall.diffPreview.theme ?? '').trim() === theme &&
        diffPreviewHasAnyTokens(prevCall.diffPreview)
      ) {
        return m
      }

      parts[idx] = {
        type: 'tool_call',
        call: { ...prevCall, diffPreview: cachedPreview },
      } as const
      return { ...m, parts }
    })
    return
  }

  const flightKey = `sel:${callId}:${extracted.oldString.length}:${extracted.newString.length}:${extracted.filePath ?? ''}:${theme}`
  if (inFlight.has(flightKey)) return

  void (async () => {
    inFlight.add(flightKey)
    try {
      // Let the UI render plain text first; compute highlighting afterward.
      await yieldToMacrotask()
      const preview = await computeDiffPreviewWithShiki(
        extracted.oldString,
        extracted.newString,
        {
          filePath: extracted.filePath ?? undefined,
          theme,
        },
      )
      if (!preview) return
      diffCache.set(cacheKey, preview)

      opts.updateAssistant(found.assistantId, (m) => {
        const parts = m.parts.slice()
        const idx = parts.findIndex(
          (p) => p.type === 'tool_call' && p.call.id === callId,
        )
        if (idx < 0) return m

        const prevPart = parts[idx] as Extract<
          (typeof parts)[number],
          { type: 'tool_call' }
        >
        const prevCall = prevPart.call as ToolCall
        if (!isEditLikeToolName(prevCall.name))
          return m
        if (!isPlainObject(prevCall.arguments)) return m

        const now = extractEditOldNew(prevCall.arguments)
        if (!now) return m
        if (
          now.oldString !== extracted.oldString ||
          now.newString !== extracted.newString
        )
          return m

        if (
          prevCall.diffPreview?.kind === 'shiki' &&
          prevCall.diffPreview.oldString === extracted.oldString &&
          prevCall.diffPreview.newString === extracted.newString &&
          String(prevCall.diffPreview.theme ?? '').trim() === theme &&
          diffPreviewHasAnyTokens(prevCall.diffPreview)
        ) {
          return m
        }

        parts[idx] = {
          type: 'tool_call',
          call: { ...prevCall, diffPreview: preview },
        } as const
        return { ...m, parts }
      })
    } catch (e) {
      debugLog('diff_highlight_error', {
        tool: 'edit-workspace',
        callId,
        assistantId: found.assistantId,
        filePath: extracted.filePath,
        error:
          e instanceof Error
            ? { message: e.message, stack: e.stack }
            : String(e),
      })
    } finally {
      inFlight.delete(flightKey)
    }
  })()
}
