import type {
  ApprovalRequest,
  Message,
  ToolApprovalsMode,
} from '#cli/core/model'
import type {
  AgentRoundResult,
  ToolExecutionContextInput,
} from './stream-runner'
import type { PausedState } from './types'
import { t } from '#cli/i18n'
import { clearApprovalState } from './approval-manager'

type ToolApprovalResult = Readonly<{ approved: boolean, reason?: string }>

interface InterruptState {
  sessionId?: string
  messages: Message[]
  runningAgentRound: boolean
  runningToolCallId: string | null
  paused: PausedState | null
  approval: {
    active: ApprovalRequest | null
    pendingAutoRun: null | { assistantId: string, toolCallId: string }
    queue: Array<{ request: ApprovalRequest, assistantId: string }>
    decisions: Record<string, ToolApprovalResult>
    mode: ToolApprovalsMode
  }
}

export type SendInterruptControls = Readonly<{
  interrupt: () => void
  interruptAndWait: (opts?: {
    streamCleanupTimeoutMs?: number
  }) => Promise<void>
  submitAskUserAnswers: (
    answers: Record<string, string | string[]>,
  ) => Promise<void>
  cancelAskUser: () => Promise<void>
  getEpoch: () => number
}>

export function createSendInterrupt(
  opts: Readonly<{
    state: InterruptState
    abortActiveAgentRound: () => void
    cancelActiveAgentStream: () => void
    cancelActiveAgentStreamAndWaitWithTimeout: (
      timeoutMs?: number,
    ) => Promise<void>
    setSessionHistoryDirty: (value: boolean) => void
    setAssistantStatus: (messageId: string, text: string) => void
    findToolCall: (callId: string) => { messageId: string } | null
    runAgentRound: (opts?: {
      toolContext?: ToolExecutionContextInput
    }) => Promise<AgentRoundResult>
    serializeReceive: (fn: () => Promise<void>) => Promise<void>
    interruptSession?: (sessionId: string) => Promise<void>
  }>,
): SendInterruptControls {
  let interruptEpoch = 0

  const getEpoch = (): number => interruptEpoch

  function shouldInvalidateSessionHistory(): boolean {
    return Boolean(opts.state.runningAgentRound || opts.state.runningToolCallId)
  }

  async function interruptAndWait(opts2?: {
    streamCleanupTimeoutMs?: number
  }): Promise<void> {
    interruptEpoch++
    const invalidateSessionHistory = shouldInvalidateSessionHistory()
    // Mark dirty before clearing running state so fire-and-forget interrupts (`store.interrupt()`)
    // preserve the resync intent whenever an already-started round may have diverged from ACP.
    if (invalidateSessionHistory)
      opts.setSessionHistoryDirty(true)

    const activeSessionId = String(opts.state.sessionId ?? '').trim()
    if (
      invalidateSessionHistory
      && activeSessionId
      && typeof opts.interruptSession === 'function'
    ) {
      try {
        await opts.interruptSession(activeSessionId)
      }
      catch {
        // Best-effort only. Stream cancellation below remains the primary stop path.
      }
    }

    // Best-effort cancellation of an in-flight agent stream.
    if (opts.state.runningAgentRound) {
      opts.abortActiveAgentRound()
    }

    // Clear running state to stop loading indicator immediately.
    if (opts.state.runningToolCallId) {
      const callId = opts.state.runningToolCallId
      opts.state.runningToolCallId = null

      // Find the assistant message and add interrupted status
      const found = opts.findToolCall(callId)
      if (found) {
        opts.setAssistantStatus(found.messageId, t('chatStatus.interrupted'))
      }
    }

    // Clear any pending approval immediately.
    if (
      opts.state.approval.pendingAutoRun
      || opts.state.approval.active
      || opts.state.approval.queue.length
    ) {
      clearApprovalState(opts.state.approval)
    }

    // IMPORTANT: Await generator cleanup so upstream agents can run `finally` blocks
    // before we attempt to send again (prevents "Cannot send while receiving messages").
    await opts.cancelActiveAgentStreamAndWaitWithTimeout(
      opts2?.streamCleanupTimeoutMs ?? 200,
    )
  }

  function interrupt(): void {
    void interruptAndWait({ streamCleanupTimeoutMs: 200 })
  }

  async function submitAskUserAnswers(
    answers: Record<string, string | string[]>,
  ): Promise<void> {
    const paused = opts.state.paused
    if (!paused || paused.kind !== 'ask_user')
      return
    const toolCallId = String(paused.toolCallId ?? '').trim()
    const actionId = String(paused.actionId ?? '').trim()
    if (!actionId)
      return
    opts.state.paused = null

    // Clear the stale "Awaiting answers" status so the UI doesn't show it
    // while the agent round resumes.
    const found = toolCallId ? opts.findToolCall(toolCallId) : null
    if (found) {
      opts.setAssistantStatus(found.messageId, '')
    }

    const resume = async (): Promise<void> => {
      const result = await opts.runAgentRound({
        toolContext: {
          requiresAction: {
            actionId,
            ...(toolCallId ? { toolCallId } : {}),
            kind: 'ask_user',
          },
          askUser: {
            answers: {
              [toolCallId || actionId]: { ...(answers as any) },
            },
          },
        },
      })
      // Handle errors gracefully
      if (!result.success && result.error) {
        console.warn('Failed to resume after ask user:', result.error.message)
      }
    }
    await opts.serializeReceive(resume)
  }

  async function cancelAskUser(): Promise<void> {
    const paused = opts.state.paused
    if (!paused || paused.kind !== 'ask_user')
      return
    const toolCallId = String(paused.toolCallId ?? '').trim()
    const actionId = String(paused.actionId ?? '').trim()
    if (!actionId)
      return
    opts.state.paused = null

    // Clear the stale "Awaiting answers" status so the UI doesn't show it
    // while the agent round resumes.
    const found = toolCallId ? opts.findToolCall(toolCallId) : null
    if (found) {
      opts.setAssistantStatus(found.messageId, '')
    }

    const resume = async (): Promise<void> => {
      const result = await opts.runAgentRound({
        toolContext: {
          requiresAction: {
            actionId,
            ...(toolCallId ? { toolCallId } : {}),
            kind: 'ask_user',
          },
          askUser: {
            answers: {},
            rejected: {
              [toolCallId || actionId]: 'User cancelled the question',
            },
          },
        },
      })
      // Handle errors gracefully
      if (!result.success && result.error) {
        console.warn(
          'Failed to resume after cancel ask user:',
          result.error.message,
        )
      }
    }
    await opts.serializeReceive(resume)
  }

  return {
    interrupt,
    interruptAndWait,
    submitAskUserAnswers,
    cancelAskUser,
    getEpoch,
  }
}
