import type {
  ApprovalRequest,
  Message,
  ToolApprovalsMode,
  ToolCall,
  ToolResult,
} from '#cli/core/model'
import type { PausedState } from './types'
import { createEnvReader, readProcessEnv } from '#cli/core/env'
import {
  isOneShotEnabled,
  isPerfEnabled,
  perfLog as perfLogLine,
  withPerfSpanAsync,
} from '#cli/core/observability/perf-log'
import { traceLog } from '#cli/core/observability/trace-log'
import { isReadLikeToolName } from '#cli/core/tool-metadata'
import { normalizeText } from '#cli/core/utils'
import { t } from '#cli/i18n'
import { shouldAutoApproveLocalPermission, toolPermission } from './approval-manager'
import { truncateReadToolOutput } from './read-output'

type ToolApprovalResult = Readonly<{ approved: boolean, reason?: string }>
type PendingApproval = Readonly<{
  request: ApprovalRequest
  assistantId: string
}>

type StoreState = Readonly<{
  messages: Message[]
  approval: Readonly<{
    active: ApprovalRequest | null
    approvedToolCalls: Record<string, true | undefined>
    pendingAutoRun: null | { assistantId: string, toolCallId: string }
    mode: ToolApprovalsMode
    queue: PendingApproval[]
    decisions: Record<string, ToolApprovalResult>
  }>
  paused: PausedState | null
  runningToolCallId: string | null
  ui?: Readonly<{
    tool?: Readonly<{
      callId?: string | null
      name?: string
      startedAtMs?: number
    }>
  }>
}>

type ToolExecutionContextInput = Readonly<{
  requiresAction?: Readonly<{
    actionId?: string
    toolCallId?: string
    kind?: 'tool_approval' | 'ask_user'
  }>
  approval?: Readonly<{
    autoApprove?: boolean
    alwaysAllowForConversation?: boolean
    decisions?: Readonly<Record<string, ToolApprovalResult>>
  }>
  askUser?: Readonly<{
    answers: Readonly<
      Record<string, Readonly<Record<string, string | string[]>>>
    >
    rejected?: Readonly<Record<string, string>>
  }>
}>

function isToolExecProfileEnabled(): boolean {
  const env = createEnvReader(readProcessEnv())
  return (
    isPerfEnabled()
    && (isOneShotEnabled() || env.prefixedStr('PROFILE_TOOL_EXECUTION') === '1')
  )
}

export function createToolExecution(
  opts: Readonly<{
    state: any & StoreState
    externalAgentProvided: boolean
    toolRunner: (call: ToolCall) => Promise<ToolResult>
    id: (prefix: string) => string
    pushMessage: (m: Message) => void
    updateAssistant: (
      messageId: string,
      fn: (
        m: Extract<Message, { role: 'assistant' }>,
      ) => Extract<Message, { role: 'assistant' }>,
    ) => void
    setAssistantStatus: (messageId: string, text: string) => void
    insertApprovalPart: (assistantId: string, request: ApprovalRequest) => void
    attachToolResultPart: (assistantId: string, result: ToolResult) => void
    runAgentRound: (opts?: {
      toolContext?: ToolExecutionContextInput
    }) => Promise<unknown>
    sleep: (ms: number) => Promise<void>
    serializeReceive: (fn: () => Promise<void>) => Promise<void>
    isInSendQueueRun: () => boolean
  }>,
): Readonly<{
  ensureApproved: (assistantId: string, call: ToolCall) => 'ok' | 'pending'
  findToolCall: (callId: string) => { messageId: string, call: ToolCall } | null
  resolveActiveApproval: (decision: 'approved' | 'denied') => Promise<void>
  runToolCall: (callId: string) => Promise<ToolResult | null>
}> {
  function ensureApproved(
    assistantId: string,
    call: ToolCall,
  ): 'ok' | 'pending' {
    const perm = toolPermission(call)
    if (!perm)
      return 'ok'
    if (
      shouldAutoApproveLocalPermission(
        opts.state.approval.mode,
        perm.permission,
      )
    ) {
      return 'ok'
    }
    if (opts.state.approval.approvedToolCalls[call.id])
      return 'ok'

    const active = opts.state.approval.active
    if (active?.toolCallId === call.id)
      return 'pending'
    if (active) {
      opts.setAssistantStatus(
        assistantId,
        t('chatStatus.approvalPendingOther', { permission: active.permission }),
      )
      return 'pending'
    }

    const req: ApprovalRequest = {
      id: opts.id('approve'),
      toolCallId: call.id,
      tool: call.name,
      permission: perm.permission,
      reason: perm.reason,
      args: call.arguments,
      status: 'pending',
    }
    opts.state.approval.active = req
    opts.state.approval.pendingAutoRun = { assistantId, toolCallId: call.id }
    opts.insertApprovalPart(assistantId, req)
    opts.setAssistantStatus(
      assistantId,
      t('chatStatus.awaitingApprovalWithPerm', { permission: perm.permission }),
    )
    return 'pending'
  }

  function findToolCall(
    callId: string,
  ): { messageId: string, call: ToolCall } | null {
    for (const m of opts.state.messages) {
      if (m.role !== 'assistant')
        continue
      for (const part of m.parts) {
        if (part.type === 'tool_call' && part.call.id === callId)
          return { messageId: m.id, call: part.call }
      }
    }
    return null
  }

  async function resolveActiveApproval(
    decision: 'approved' | 'denied',
  ): Promise<void> {
    const active = opts.state.approval.active
    if (!active)
      return
    const next: ApprovalRequest = { ...active, status: decision }
    const pending = opts.state.approval.pendingAutoRun
    const assistantId = pending?.assistantId
    if (assistantId) {
      opts.updateAssistant(assistantId, (m) => {
        const parts = m.parts.map((p) => {
          if (p.type === 'approve' && p.request.id === active.id)
            return { type: 'approve', request: next } as const
          return p
        })
        return { ...m, parts }
      })
    }

    if ((active.source ?? 'local') === 'goatchain') {
      opts.state.approval.decisions[active.toolCallId]
        = decision === 'approved'
          ? { approved: true }
          : {
              approved: false,
              reason: active.reason || t('toolApproval.decision.userDenied'),
            }

      const queued = opts.state.approval.queue.shift() ?? null
      if (queued) {
        opts.state.approval.active = queued.request
        opts.state.approval.pendingAutoRun = {
          assistantId: queued.assistantId,
          toolCallId: queued.request.toolCallId,
        }
        if (opts.state.paused?.kind === 'tool_approval') {
          opts.state.paused = {
            ...opts.state.paused,
            toolCallId: queued.request.toolCallId,
          }
        }
        opts.setAssistantStatus(
          queued.assistantId,
          t('chatStatus.awaitingApprovalWithRisk', {
            risk: String(
              queued.request.riskLevel
              ?? queued.request.permission
              ?? t('chatStatus.riskFallback'),
            ),
          }),
        )
        return
      }

      opts.state.approval.active = null
      opts.state.approval.pendingAutoRun = null

      const decisions = { ...opts.state.approval.decisions }
      opts.state.approval.decisions = {}

      const decisionList = Object.values(decisions) as ToolApprovalResult[]
      const approvedAny = decisionList.some(item => item.approved)
      const deniedAny = decisionList.some(item => !item.approved)
      if (assistantId) {
        const status
          = approvedAny && !deniedAny
            ? t('chatStatus.resumingApproved')
            : deniedAny && !approvedAny
              ? t('chatStatus.resumingDenied')
              : t('chatStatus.resuming')
        opts.setAssistantStatus(assistantId, status)
      }
      const pausedActionId = opts.state.paused?.actionId

      const resume = async (): Promise<void> => {
        opts.state.paused = null
        await opts.runAgentRound({
          toolContext: {
            requiresAction: {
              actionId: pausedActionId ?? undefined,
              toolCallId: active.toolCallId,
              kind: 'tool_approval',
            },
            approval: {
              decisions,
              alwaysAllowForConversation: opts.state.approval.mode === 'auto',
            },
          },
        })
      }

      if (opts.isInSendQueueRun()) {
        await resume()
        return
      }

      await opts.serializeReceive(resume)
      return
    }

    if (decision === 'approved')
      opts.state.approval.approvedToolCalls[active.toolCallId] = true

    opts.state.approval.active = null
    opts.state.approval.pendingAutoRun = null

    if (decision === 'denied') {
      if (!assistantId)
        return
      opts.setAssistantStatus(assistantId, t('chatStatus.permissionDenied'))
      const denial = [
        t('toolApproval.denied.title'),
        '',
        t('toolApproval.denied.toolLine', { tool: active.tool }),
        t('toolApproval.denied.permissionLine', {
          permission: active.permission,
        }),
        '',
        t('toolApproval.denied.hint'),
      ].join('\n')
      const modelId
        = String((opts.state as any)?.config?.model ?? '').trim() || undefined
      opts.pushMessage({
        id: opts.id('m'),
        role: 'assistant',
        parts: [{ type: 'markdown', markdown: denial }],
        createdAtMs: Date.now(),
        ...(modelId ? { modelId } : {}),
      })
      return
    }

    if (!assistantId)
      return

    opts.setAssistantStatus(
      assistantId,
      t('chatStatus.runningTool', { tool: active.tool }),
    )
    await opts.sleep(180)
    const result = await runToolCall(active.toolCallId)
    opts.setAssistantStatus(assistantId, t('chatStatus.done'))
    if (!result)
      return

    const summary = [
      t('toolApproval.summary.title'),
      '',
      t('toolApproval.summary.permissionLine', {
        permission: active.permission,
        tool: active.tool,
      }),
      '',
      '```text',
      normalizeText(result.output).split('\n').slice(0, 8).join('\n'),
      '```',
    ].join('\n')
    const modelId
      = String((opts.state as any)?.config?.model ?? '').trim() || undefined
    opts.pushMessage({
      id: opts.id('m'),
      role: 'assistant',
      parts: [{ type: 'markdown', markdown: summary }],
      createdAtMs: Date.now(),
      ...(modelId ? { modelId } : {}),
    })
  }

  async function runToolCall(callId: string): Promise<ToolResult | null> {
    if (opts.externalAgentProvided) {
      // Silently return - tool execution is handled by the external agent stream
      return null
    }

    const found = findToolCall(callId)
    if (!found)
      return null

    if (!opts.state.approval.approvedToolCalls[callId]) {
      if (ensureApproved(found.messageId, found.call) === 'pending')
        return null
    }

    opts.state.runningToolCallId = callId
    const uiState: any = (opts.state as any).ui
    if (uiState) {
      uiState.tool = {
        callId,
        name: String(found.call.name ?? ''),
        startedAtMs: Date.now(),
      }
    }
    try {
      const profile = isToolExecProfileEnabled()
      const t0 = profile ? Date.now() : 0
      traceLog({
        tag: 'TOOL',
        message: 'run.start',
        payload: {
          callId,
          tool: String(found.call.name ?? ''),
          sessionId: String((opts.state as any)?.sessionId ?? ''),
        },
      })
      const result = await (async () => {
        try {
          return await withPerfSpanAsync(
            'toolRunner',
            `tool=${String(found.call.name ?? '')} callId=${callId}`,
            async () => {
              return await opts.toolRunner(found.call)
            },
          )
        }
        catch (e) {
          const errText = e instanceof Error ? e.stack || e.message : String(e)
          return {
            toolCallId: callId,
            status: 'error',
            output: `Tool runner error:\n${errText}`,
          } satisfies ToolResult
        }
      })()
      const toolNameLower = String(found.call.name ?? '')
        .trim()
        .toLowerCase()
      const normalizedResult: ToolResult
        = isReadLikeToolName(toolNameLower)
          ? (() => {
              const truncated = truncateReadToolOutput(
                String(result.output ?? ''),
                { maxLines: 10, ellipsisLine: '...' },
              )
              return {
                ...result,
                output: truncated.output,
                // Drop any precomputed ANSI to avoid holding large strings.
                outputAnsi: undefined,
              } satisfies ToolResult
            })()
          : result
      if (profile) {
        const ms = Date.now() - t0
        if (ms >= (isOneShotEnabled() ? 5 : 40)) {
          const argsKeys = (() => {
            const a: any = found.call.arguments as any
            if (!a || typeof a !== 'object' || Array.isArray(a))
              return 0
            try {
              return Object.keys(a).length
            }
            catch {
              return 0
            }
          })()
          const outChars = String(normalizedResult.output ?? '').length
          const outAnsiChars = String(normalizedResult.outputAnsi ?? '').length
          perfLogLine(
            `[TOOL_EXEC] ${ms}ms callId=${callId} tool=${toolNameLower || 'tool'} argsKeys=${argsKeys} outChars=${outChars} outAnsiChars=${outAnsiChars}`,
          )
        }
      }
      traceLog({
        tag: 'TOOL',
        message: 'run.end',
        payload: {
          callId,
          tool: toolNameLower || 'tool',
          status: normalizedResult.status,
          outChars: String(normalizedResult.output ?? '').length,
        },
      })
      opts.pushMessage({
        id: opts.id('m'),
        role: 'tool',
        result: normalizedResult,
      })
      opts.attachToolResultPart(found.messageId, normalizedResult)
      return normalizedResult
    }
    finally {
      opts.state.runningToolCallId = null
      const uiState: any = (opts.state as any).ui
      if (uiState?.tool) {
        uiState.tool = {
          callId: null,
          name: '',
          startedAtMs: 0,
        }
      }
    }
  }

  return {
    ensureApproved,
    findToolCall,
    resolveActiveApproval,
    runToolCall,
  }
}
