import type { Message } from '#cli/core/model'
import { execFileSync } from 'node:child_process'
import { mkdirSync, realpathSync, rmSync, writeFileSync } from 'node:fs'
import path from 'node:path'

export type FileRevert = Readonly<{
  filePath: string
  existedBefore: boolean
  previousContent: string | null
  nextContent: string | null
}>

type AssistantMessage = Extract<Message, { role: 'assistant' }>
type LlmMetaPart = Extract<AssistantMessage['parts'][number], { type: 'llm_meta' }>

function readChangedFileDetails(part: LlmMetaPart): string[] {
  return (part.changedFileDetails ?? [])
    .map(item => String(item.path ?? '').trim())
    .filter(Boolean)
}

function readChangedFiles(part: LlmMetaPart): string[] {
  return (part.changedFiles ?? [])
    .map(item => String(item ?? '').trim())
    .filter(Boolean)
}

export function createFileRevertHandlers(
  opts: Readonly<{
    state: {
      messages: Message[]
      context?: { cwd?: string }
      revertedMessages: Message[]
      revertedFilesApplied: boolean
      fileRevertsByToolCallId: Map<string, FileRevert>
    }
    debugLog: (event: string, data?: unknown) => void
    revertFromMessageBase: (messageId: string) => number
    restoreRevertedMessagesBase: () => number
    clearRevertedMessagesBase: () => void
  }>,
): Readonly<{
  revertFromMessage: (
    messageId: string,
    opts?: { keepFiles?: boolean },
  ) => number
  revertFilesFromMessage: (messageId: string) => number
  restoreRevertedMessages: () => number
  clearRevertedMessages: () => void
}> {
  const state = opts.state

  function collectToolCallIds(messages: readonly Message[]): string[] {
    const ids: string[] = []
    for (const m of messages) {
      if (!m || m.role !== 'assistant')
        continue
      for (const p of m.parts) {
        if (p.type === 'tool_call' && p.call?.id)
          ids.push(String(p.call.id))
      }
    }
    return ids
  }

  function resolveFileReverts(toolCallIds: readonly string[]): FileRevert[] {
    const out: FileRevert[] = []
    for (const id of toolCallIds) {
      const entry = state.fileRevertsByToolCallId.get(id)
      if (entry)
        out.push(entry)
    }
    return out
  }

  function collectChangedFilePathsFromMessage(message: Message): string[] {
    if (!message || message.role !== 'assistant')
      return []
    const out: string[] = []
    const seen = new Set<string>()
    for (const part of message.parts) {
      if (part.type !== 'llm_meta')
        continue
      const detailed = readChangedFileDetails(part)
      const basic = readChangedFiles(part)
      for (const filePath of [...detailed, ...basic]) {
        if (seen.has(filePath))
          continue
        seen.add(filePath)
        out.push(filePath)
      }
    }
    if (out.length > 0)
      return out
    const entries = resolveFileReverts(collectToolCallIds([message]))
    for (const entry of entries) {
      const filePath = String(entry.filePath ?? '').trim()
      if (!filePath || seen.has(filePath))
        continue
      seen.add(filePath)
      out.push(filePath)
    }
    return out
  }

  function applyFileRevert(entry: FileRevert, mode: 'undo' | 'redo'): void {
    try {
      if (mode === 'undo') {
        if (!entry.existedBefore) {
          rmSync(entry.filePath, { force: true })
          return
        }
        const parentDir = path.dirname(entry.filePath)
        mkdirSync(parentDir, { recursive: true })
        writeFileSync(entry.filePath, entry.previousContent ?? '', 'utf-8')
        return
      }

      if (entry.nextContent == null)
        return
      const parentDir = path.dirname(entry.filePath)
      mkdirSync(parentDir, { recursive: true })
      writeFileSync(entry.filePath, entry.nextContent, 'utf-8')
    }
    catch (error) {
      opts.debugLog('file_revert_failed', {
        filePath: entry.filePath,
        mode,
        error: error instanceof Error ? error.message : String(error),
      })
    }
  }

  function applyFileRevertsFromMessages(
    messages: readonly Message[],
    mode: 'undo' | 'redo',
  ): void {
    const toolCallIds = collectToolCallIds(messages)
    if (toolCallIds.length === 0)
      return
    const entries = resolveFileReverts(toolCallIds)
    if (entries.length === 0)
      return
    const ordered = mode === 'undo' ? [...entries].reverse() : entries
    for (const entry of ordered) applyFileRevert(entry, mode)
  }

  function tryGitRestoreFiles(filePaths: readonly string[]): boolean {
    const cwd = String(state.context?.cwd ?? '').trim()
    if (!cwd || filePaths.length === 0)
      return false
    try {
      const repoRoot = execFileSync(
        'git',
        ['-C', cwd, 'rev-parse', '--show-toplevel'],
        {
          encoding: 'utf-8',
          stdio: ['ignore', 'pipe', 'ignore'],
        },
      ).trim()
      if (!repoRoot)
        return false
      const normalizedRepoRoot = realpathSync(repoRoot)

      const tracked: string[] = []
      const untracked: string[] = []
      for (const rawFilePath of filePaths) {
        const absPathRaw = path.isAbsolute(rawFilePath)
          ? path.normalize(rawFilePath)
          : path.resolve(cwd, rawFilePath)
        const absPath = realpathSync(path.dirname(absPathRaw))
        const normalizedAbsPath = path.join(absPath, path.basename(absPathRaw))
        const relPath = path.relative(normalizedRepoRoot, normalizedAbsPath)
        if (!relPath || relPath.startsWith('..') || path.isAbsolute(relPath))
          continue
        try {
          execFileSync(
            'git',
            [
              '-C',
              normalizedRepoRoot,
              'ls-files',
              '--error-unmatch',
              '--',
              relPath,
            ],
            {
              stdio: 'ignore',
            },
          )
          tracked.push(relPath)
        }
        catch {
          untracked.push(relPath)
        }
      }

      if (tracked.length === 0 && untracked.length === 0)
        return false
      if (tracked.length > 0) {
        execFileSync(
          'git',
          [
            '-C',
            normalizedRepoRoot,
            'restore',
            '--worktree',
            '--source=HEAD',
            '--',
            ...tracked,
          ],
          {
            stdio: 'ignore',
          },
        )
      }
      if (untracked.length > 0) {
        execFileSync(
          'git',
          ['-C', normalizedRepoRoot, 'clean', '-fd', '--', ...untracked],
          {
            stdio: 'ignore',
          },
        )
      }
      return true
    }
    catch (error) {
      opts.debugLog('git_file_revert_failed', {
        cwd,
        files: filePaths,
        error: error instanceof Error ? error.message : String(error),
      })
      return false
    }
  }

  function revertFilesFromMessage(messageId: string): number {
    const id = String(messageId ?? '').trim()
    if (!id)
      return 0
    const target = state.messages.find(m => m.id === id)
    if (!target || target.role !== 'assistant')
      return 0
    const changedFiles = collectChangedFilePathsFromMessage(target)
    if (changedFiles.length > 0 && tryGitRestoreFiles(changedFiles))
      return changedFiles.length
    const entries = resolveFileReverts(collectToolCallIds([target]))
    if (entries.length === 0)
      return 0
    for (const entry of [...entries].reverse()) applyFileRevert(entry, 'undo')
    return entries.length
  }

  function revertFromMessage(
    messageId: string,
    opts2?: { keepFiles?: boolean },
  ): number {
    const removed = opts.revertFromMessageBase(messageId)
    if (removed > 0) {
      if (opts2?.keepFiles) {
        state.revertedFilesApplied = false
      }
      else {
        state.revertedFilesApplied = true
        applyFileRevertsFromMessages(state.revertedMessages, 'undo')
      }
    }
    return removed
  }

  function restoreRevertedMessages(): number {
    const removedMessages = state.revertedMessages.slice()
    const count = opts.restoreRevertedMessagesBase()
    if (count > 0) {
      if (state.revertedFilesApplied)
        applyFileRevertsFromMessages(removedMessages, 'redo')
      state.revertedFilesApplied = false
    }
    return count
  }

  function clearRevertedMessages(): void {
    opts.clearRevertedMessagesBase()
    state.revertedFilesApplied = false
  }

  return {
    revertFromMessage,
    revertFilesFromMessage,
    restoreRevertedMessages,
    clearRevertedMessages,
  }
}
