import type {
  ApprovalRequest,
  ToolApprovalsMode,
  ToolCall,
  ToolPermission,
} from '#cli/core/model'
import {
  isEditLikeToolName,
  isReadLikeToolName,
  isSearchLikeToolName,
  isWriteLikeToolName,
} from '#cli/core/tool-metadata'
import { t } from '#cli/i18n'
import { normalizeRuntimeToolName } from '#cli/compat/runtime-host'

// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------

export type ApprovalDecision
  = | Readonly<{ type: 'approved' }>
    | Readonly<{ type: 'denied', reason?: string }>

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

export type PendingApproval = Readonly<{
  request: ApprovalRequest
  assistantId: string
}>

export interface ApprovalState {
  active: ApprovalRequest | null
  approvedToolCalls: Record<string, true | undefined>
  pendingAutoRun: null | { assistantId: string, toolCallId: string }
  mode: ToolApprovalsMode
  queue: PendingApproval[]
  decisions: Record<string, ToolApprovalResult>
}

// ---------------------------------------------------------------------------
// Pure helpers
// ---------------------------------------------------------------------------

/**
 * Decide whether a tool approval request should prompt the user in `auto` mode.
 *
 * Conservative by default:
 * - unknown risk levels prompt
 * - dangerous permissions prompt
 */
export function shouldPromptForToolApproval(req: ApprovalRequest): boolean {
  const permission = req.permission
  const risk = String(req.riskLevel ?? '')
    .trim()
    .toLowerCase()
  if (permission === 'require_action')
    return true
  if (!risk)
    return true
  return risk === 'high' || risk === 'critical'
}

export function shouldAutoApproveRequest(
  mode: ToolApprovalsMode,
  req: ApprovalRequest,
): boolean {
  if (mode === 'auto')
    return true
  if (mode === 'strict')
    return false
  // normal: auto-approve unless the request itself is high-risk
  return !shouldPromptForToolApproval(req)
}

export function shouldAutoApproveLocalPermission(
  mode: ToolApprovalsMode,
  permission: ToolPermission,
): boolean {
  if (mode === 'auto')
    return true
  if (mode === 'strict')
    return false
  // normal: auto-approve read-only (pass), prompt for require_action
  return permission !== 'require_action'
}

// ---------------------------------------------------------------------------
// Exec command whitelist — read-only commands that bypass approval
// ---------------------------------------------------------------------------

/**
 * Whitelist of command prefixes considered read-only.
 * Each entry is tested against the *first token(s)* of the trimmed command.
 * Commands that match are auto-approved in `normal` mode.
 */
const EXEC_READ_ONLY_PREFIXES: readonly string[] = [
  // filesystem listing / inspection
  'ls',
  'ls ',
  'll',
  'll ',
  'la',
  'la ',
  'tree',
  'tree ',
  'stat ',
  'file ',
  'du ',
  'df',
  'df ',
  'wc ',

  // file reading
  'cat ',
  'head ',
  'tail ',
  'less ',
  'more ',

  // search / find
  'find ',
  'grep ',
  'rg ',
  'fd ',
  'ag ',
  'which ',
  'where ',
  'whereis ',
  'type ',
  'command -v ',

  // text processing (read-only)
  'sort ',
  'uniq ',
  'cut ',
  'tr ',
  'awk ',
  'sed -n ',
  'sed -e ',
  'jq ',
  'yq ',
  'xargs grep ',
  'xargs rg ',

  // git (read-only)
  'git status',
  'git log',
  'git log ',
  'git diff',
  'git diff ',
  'git show',
  'git show ',
  'git branch',
  'git branch ',
  'git tag',
  'git tag ',
  'git remote',
  'git remote ',
  'git rev-parse',
  'git rev-parse ',
  'git rev-list ',
  'git describe',
  'git describe ',
  'git ls-files',
  'git ls-files ',
  'git ls-tree ',
  'git blame ',
  'git shortlog',
  'git shortlog ',
  'git cat-file ',
  'git show-ref',
  'git show-ref ',
  'git ls-remote',
  'git ls-remote ',
  'git for-each-ref',
  'git for-each-ref ',
  'git merge-base ',
  'git diff-tree ',
  'git cherry ',
  'git name-rev ',
  'git check-ignore ',
  'git reflog',
  'git reflog show',
  'git reflog show ',
  'git stash list',
  'git stash show',
  'git stash show ',
  'git config --get',
  'git config --get ',
  'git config --get-regexp',
  'git config --get-regexp ',
  'git config --list',
  'git config -l',

  // environment / system info
  'echo ',
  'printf ',
  'env',
  'env ',
  'printenv',
  'printenv ',
  'uname',
  'uname ',
  'hostname',
  'whoami',
  'id',
  'id ',
  'date',
  'date ',
  'uptime',
  'pwd',

  // package info (read-only)
  'npm ls',
  'npm ls ',
  'npm list',
  'npm list ',
  'npm info ',
  'npm view ',
  'npm outdated',
  'npm outdated ',
  'npm audit',
  'npm audit ',
  'pnpm ls',
  'pnpm ls ',
  'pnpm list',
  'pnpm list ',
  'pnpm why ',
  'pnpm outdated',
  'pnpm outdated ',
  'bun pm ls',
  'bun pm ls ',
  'bun pm hash',
  'bun pm hash ',
  'bun pm cache',
  'bun pm cache ',
  'bun --version',
  'bun -v',
  'yarn list',
  'yarn list ',
  'yarn info ',
  'yarn why ',

  // process / network info
  'ps',
  'ps ',
  'top -l ',
  'lsof ',
  'netstat',
  'netstat ',
  'ss ',
  'ifconfig',
  'ip addr',
  'ip route',
  'ping ',
  'dig ',
  'nslookup ',
  'host ',
  'curl -s',
  'curl -sS',
  'curl --silent',
  'curl -I ',
  'curl --head ',

  // misc read-only
  'realpath ',
  'basename ',
  'dirname ',
  'md5sum ',
  'sha256sum ',
  'sha1sum ',
  'shasum ',
  'xxd ',
  'hexdump ',
  'strings ',
  'nm ',
  'ldd ',
  'otool ',
  'man ',
  'help ',
]

/**
 * Patterns that indicate a command is NOT read-only even if its prefix matches.
 * Catches redirections, writes, and chained destructive subcommands.
 */
const EXEC_WRITE_INDICATORS: readonly RegExp[] = [
  />\s*[^|&]/, // output redirection  (but not `|` or `&&`)
  />>/, // append redirection
  /\btee\b/, // tee writes to files
  /\bxargs\s+(?!grep|rg|cat|head|tail|wc|file|stat|ls)/, // xargs with non-read command
  /\brm\b/,
  /\bmkdir\b/,
  /\btouch\b/,
  /\bmv\b/,
  /\bcp\b/,
  /\bsudo\b/,
  /\bchmod\b/,
  /\bchown\b/,
  /`[^`]*`/, // backtick subshell — hard to verify safety
  /\$\(/, // $() subshell
]

/**
 * Determine whether a shell command is read-only and can be auto-approved.
 */
export function isExecCommandReadOnly(command: string): boolean {
  const trimmed = command.trim()
  if (!trimmed)
    return false

  // Check against write indicators first — if any match, it's NOT read-only
  for (const pattern of EXEC_WRITE_INDICATORS) {
    if (pattern.test(trimmed))
      return false
  }

  // Check whether the command starts with a whitelisted read-only prefix
  for (const prefix of EXEC_READ_ONLY_PREFIXES) {
    if (trimmed === prefix.trimEnd() || trimmed.startsWith(prefix))
      return true
  }

  // Piped commands: if every segment is read-only, the whole pipeline is read-only
  if (trimmed.includes(' | ')) {
    const segments = trimmed.split(/\s*\|\s*/)
    if (segments.length > 1 && segments.every(seg => isExecCommandReadOnly(seg)))
      return true
  }

  return false
}

// ---------------------------------------------------------------------------
// Tool permission resolution
// ---------------------------------------------------------------------------

/**
 * Determine the permission level for a tool call based on its name and arguments.
 */
export function toolPermission(
  call: ToolCall,
): { permission: ToolPermission, reason: string } | null {
  const name = normalizeRuntimeToolName(call.name).toLowerCase()
  if (name === 'search') {
    return {
      permission: 'require_action',
      reason: t('toolApproval.reason.search'),
    }
  }
  if (name === 'shell') {
    const command = typeof call.arguments?.command === 'string'
      ? call.arguments.command
      : Array.isArray(call.arguments?.argv) && call.arguments.argv.every(arg => typeof arg === 'string')
          ? call.arguments.argv.join(' ')
          : ''
    if (command && isExecCommandReadOnly(command))
      return { permission: 'pass', reason: t('toolApproval.reason.grep') }
    return {
      permission: 'require_action',
      reason: t('toolApproval.reason.bash'),
    }
  }
  if (isWriteLikeToolName(name) || isEditLikeToolName(name)) {
    return {
      permission: 'require_action',
      reason: t('toolApproval.reason.edit'),
    }
  }
  if (isReadLikeToolName(name) || isSearchLikeToolName(name))
    return { permission: 'pass', reason: t('toolApproval.reason.grep') }
  return null
}

/**
 * Minimal shape required by {@link clearApprovalState}.
 * Intentionally narrow so callers that lack `approvedToolCalls` (e.g. stream-runner) can still use it.
 */
export interface ClearableApprovalState {
  active: ApprovalRequest | null
  pendingAutoRun: null | { assistantId: string, toolCallId: string }
  queue: PendingApproval[]
  decisions: Record<string, ToolApprovalResult>
}

/**
 * Clear all mutable approval state fields in-place.
 * Consolidates the repeated cleanup pattern used by interrupt and error paths.
 */
export function clearApprovalState(approval: ClearableApprovalState): void {
  approval.active = null
  approval.pendingAutoRun = null
  approval.queue = []
  approval.decisions = {}
}
