//! ToolCard 纯函数与常量 —— 工具卡片渲染逻辑层
//!
//! 职责:
//! - 工具结果 JSON 解析(parseResult)与人类可读摘要格式化(formatToolResult/toolResultSummary)
//! - 工具元数据(图标/分类/显示名片段/路径短化/字节格式化)
//! - 审批参数键值转换(toolArgsEntries/formatArgValue/argLabel)
//! - run_command 输出合并与截断(combineOutputs/combineAndTruncateLines)
//! - 工具失败语义检测(isToolFailure)
//!
//! 抽离自 ToolCard.vue(原 <script lang="ts"> 模块级纯函数 + ToolResult 类型 + 图标常量,
//! 及原 <script setup> 中仅依赖 i18n.t 的 toolResultSummary)。零行为变更,纯搬迁。
//!
//! 设计:
//! - 非组件上下文(无 setup),用 @/i18n/i18n-helpers 的类型安全 t(规避 vue-i18n TS2589)。
//! - 全部为纯函数(入参出参,无响应式无副作用),供 ToolCard.vue 模板/setup 直接 import 调用。
//! - store 依赖的工具显示名(toolDisplayName/displayArgValue/projectNameById/taskNameById)
//!   仍留 ToolCard.vue setup(耦合 projectStore,非纯函数)。
//!
//! 耦合:
//! - i18n 翻译(经 @/i18n/i18n-helpers)
//! - AiToolCallInfo(@/api/types)

import { t } from '@/i18n/i18n-helpers'
import type { AiToolCallInfo } from '@/api/types'

/** 工具结果 JSON 已知字段(全可选;list_* 运行时返回数组,靠 Array.isArray 区分) */
export interface ToolResult {
  path?: string
  pattern?: string
  from?: string
  to?: string
  content?: string
  lines?: number | null
  size?: number | null
  bytes_written?: number
  new_size?: number
  bytes_moved?: number
  entries?: Array<{ name: string; type: string; size?: number; depth?: number }>
  name?: string
  title?: string
  field?: string
  id?: string
  status?: string
  stack?: string
  bound?: boolean
  deleted?: boolean
  restored?: boolean
  purged?: boolean
  updated?: boolean
  changed?: boolean
  renamed?: boolean
  exists?: boolean
  is_binary?: boolean
  is_dir?: boolean
  permanent?: boolean
  backed_up?: boolean
  size_diff?: number
  exit_code?: number
  duration_ms?: number
  stdout?: string
  stderr?: string
  truncated?: boolean
  results?: Array<{ path?: string; size?: number }>
  total?: number
  has_more?: boolean
  warning?: string
  review_rounds?: number
  // UX-260618-05~12:补全后端返回前端消费的字段(review_rounds/truncated/bytes_moved/has_more/total/entries 已存在)
  task_id?: string
  execution_id?: string
  target_status?: string
  note?: string
  diff?: string
  matches_found?: number
  backup_path?: string
  returned_lines?: number
  items?: Array<Record<string, unknown>>
  modified?: number | null
  old_size?: number | null
  encoding?: string
  cross_volume?: boolean
  // http_request result fields
  method?: string
  url?: string
  body?: string
  body_bytes?: number
  elapsed_ms?: number
  // read_file search 模式(BUG-260625-02):传 search 参数返命中行 matches[{line,content}] 非全文 content
  search?: string
  // grep 跨文件内容搜索结果字段(F-260621):
  // - output_mode: content(默认)/ files_with_matches / count
  // - matches: content 模式命中行 [{file,line,content,context?}]
  // - files: files_with_matches 模式命中文件名数组
  // - counts: count 模式每文件命中行数 [{file,count}]
  // - total: 命中总数(content 模式=命中行数,count 模式=命中行数,files_with_matches 模式=命中文件数)
  // - total_files: count 模式命中文件数
  output_mode?: string
  matches?: Array<{ file?: string; line?: number | null; content?: string; context?: string }>
  files?: string[]
  counts?: Array<{ file?: string; count?: number }>
  total_files?: number
  // fetch_url result fields(commands/ai/fetch_url.rs execute_fetch_url):
  // - markdown: 已清洗 + 截断的 markdown 主体
  // - title: HTML <title> 或 obscura markdown 首个 H1/首行(可能为 null)
  // - length: markdown 字符数(后端 result.length)
  // - render_mode: 'static' | 'static_fallback' | 'obscura'(obscura=成功用 JS 渲染)
  //   static_fallback = render=true 但 obscura 未装/失败/超时 → 回退静态 + 附 hint(引导安装 obscura)
  // - hint: 仅 static_fallback 时存在,obscura 安装引导提示
  markdown?: string
  length?: number | null
  render_mode?: string
  hint?: string
}

/** 内联 SVG 图标字符串(经 v-html 注入,常量安全;图标库改造见决策记录) */
export const dirIcon = '<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M22 19a2 2 0 01-2 2H4a2 2 0 01-2-2V5a2 2 0 012-2h5l2 3h9a2 2 0 012 2z"/></svg>'
export const fileIcon = '<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>'
/** grep 工具放大镜图标(跨文件内容搜索,F-260621) */
export const searchIcon = '<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>'

/** 默认保持展开:执行中/待审批/已拒绝/write_file——折叠无收益 */
export function shouldKeepOpen(tc: AiToolCallInfo): boolean {
  return tc.status === 'running'
    || tc.status === 'pending_approval'
    || tc.status === 'rejected'
    || tc.name === 'write_file'
}

/**
 * UX-260616-01: 工具执行失败检测(completed 状态下的失败语义)。
 *
 * 后端 AR-6:Low 风险工具失败不 emit AiError,改 emit AiToolCallCompleted(result=错误),
 * 故失败工具 status 仍为 'completed',与成功工具视觉同态(原绿色边框+绿色结果框)。
 * 此函数区分两类失败:
 *  - run_command exit_code≠0:结果 JSON 合法,parseResult 命中,看 exit_code 判失败
 *  - 通用工具失败:AR-6 路径后端塞 serde_json::Value::String("工具 X 执行失败: ..."),
 *    前端 parseResult 返回 null(非 JSON 对象)→ 走 raw 文本兜底,无结构信号
 *    此时按行首前缀 "执行失败"/"Error:"/"Failed:" 启发式判定(对齐后端 err_msg 格式,
 *    行首锚定避免误伤含这些子串的正常产出,见 UX-260617-18)。
 *
 * 注意:status==='rejected'(用户拒绝)走独立分支,不算工具失败,不进此函数。
 * running/pending_approval 自然 false(无 result 或 result 未稳定)。
 */
export function isToolFailure(tc: AiToolCallInfo): boolean {
  if (tc.status !== 'completed') return false
  // run_command:结构化 exit_code 判定(可靠)
  if (tc.name === 'run_command') {
    const r = parseResult(tc.result)
    if (r && typeof r.exit_code === 'number') return r.exit_code !== 0
    // run_command 但无 exit_code(异常)→ 保守判失败
    if (r) return true
  }
  // 通用工具:parseResult 返回 null(非 JSON,AR-6 错误字符串路径)→ 看文本前缀
  if (!parseResult(tc.result)) {
    const raw = typeof tc.result === 'string' ? tc.result : ''
    if (!raw) return false
    // UX-260617-18:收窄为行首锚定(原 /执行失败|failed|error[:\s]/i 匹配任意位置的子串,
    // 误伤 search_files 命中 error.log / run_command stderr 含 "warning" 等正常产出)。
    // 仅当某行以「执行失败」(中文 err_msg 前缀)/「Error:」/「Failed:」开头才判失败。
    return /^(执行失败|Error:|Failed:)/m.test(raw)
  }
  return false
}

export function formatToolName(name: string): string {
  if (!name) return ''
  return name.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase())
}

/** run_command 命令首行截断(头部展示;多行命令取首行,超长省略) */
export function shortCmd(cmd: string, max = 48): string {
  const firstLine = cmd.split('\n')[0].replace(/\s+/g, ' ').trim()
  return firstLine.length > max ? firstLine.slice(0, max) + '…' : firstLine
}

export function formatJson(data: unknown): string {
  try {
    return JSON.stringify(data, null, 2)
  } catch {
    return String(data)
  }
}

/** 审批参数 key → 友好标签映射(动态 i18n);未命中回退原 key */
export function argLabel(key: string): string {
  const map: Record<string, string> = {
    id: t('aiTool.argLabel.id'),
    project_id: t('aiTool.argLabel.projectId'),
    name: t('aiTool.argLabel.name'),
    title: t('aiTool.argLabel.title'),
    description: t('aiTool.argLabel.description'),
    field: t('aiTool.argLabel.field'),
    value: t('aiTool.argLabel.value'),
    path: t('aiTool.argLabel.path'),
    priority: t('aiTool.argLabel.priority'),
    tags: t('aiTool.argLabel.tags'),
    source: t('aiTool.argLabel.source'),
  }
  return map[key] || key
}

/** 审批参数转键值对数组(非对象/空时返回空数组,防 args 非 object 时 v-for 报错) */
export function toolArgsEntries(args: unknown): Array<{ key: string; label: string; val: unknown }> {
  if (args && typeof args === 'object' && !Array.isArray(args)) {
    return Object.entries(args as Record<string, unknown>).map(([key, val]) => ({
      key,
      label: argLabel(key),
      val,
    }))
  }
  return []
}

/** 格式化单个审批参数值:字符串直接展示(超长截断),对象/数组 JSON 化后截断 */
export function formatArgValue(val: unknown): string {
  if (val == null) return ''
  if (typeof val === 'string') return val.length > 300 ? val.slice(0, 300) + '…' : val
  if (typeof val === 'object') {
    try {
      const s = JSON.stringify(val)
      return s.length > 300 ? s.slice(0, 300) + '…' : s
    } catch {
      return String(val)
    }
  }
  return String(val)
}

/** 安全解析工具结果 JSON;list_* 运行时是数组,类型归 ToolResult 靠 Array.isArray 区分 */
export function parseResult(result: unknown): ToolResult | null {
  if (!result) return null
  try {
    return (typeof result === 'string' ? JSON.parse(result) : result) as ToolResult
  } catch {
    return null
  }
}

export function toolCategory(name: string): string {
  if (!name) return 'default'
  if (name === 'read_file') return 'file-read'
  if (name === 'write_file') return 'file-write'
  if (name === 'list_directory') return 'dir'
  if (name.includes('create')) return 'create'
  if (name.includes('delete')) return 'delete'
  if (name.includes('list')) return 'list'
  return 'default'
}

export function toolIcon(name: string): string {
  if (name === 'read_file') return '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>'
  if (name === 'write_file') return '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>'
  if (name === 'grep') return '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>'
  if (name === 'list_directory') return dirIcon
  if (name.includes('create')) return '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>'
  if (name.includes('delete')) return '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg>'
  return '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>'
}

export function shortPath(p: string): string {
  const parts = p.replace(/\\/g, '/').split('/')
  return parts.length <= 2 ? p : '.../' + parts.slice(-2).join('/')
}

/** 格式化字节数(兜底:undefined/null/负/NaN/Infinity 统一降级为 '0 B',防渲染异常) */
export function formatBytes(bytes: number | undefined | null): string {
  if (typeof bytes !== 'number' || !Number.isFinite(bytes) || bytes <= 0) return '0 B'
  if (bytes < 1024) return bytes + ' B'
  if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB'
  return (bytes / 1048576).toFixed(1) + ' MB'
}

/** 从工具 args(unknown)中安全取字符串字段;非对象/字段非 string 时返回空串 */
export function argString(args: unknown, key: string): string {
  if (args && typeof args === 'object' && !Array.isArray(args)) {
    const v = (args as Record<string, unknown>)[key]
    return typeof v === 'string' ? v : ''
  }
  return ''
}

/** size_diff 数字 → "+N"/"-N"/"+0" 字符串(负数补减号,正数补加号) */
export function formatSizeDiff(diff: number | undefined | null): string {
  const n = typeof diff === 'number' ? diff : 0
  return n >= 0 ? `+${n}` : `${n}`
}

/** SW-260618-19: 合并 stdout/stderr(空串跳过),供 cmdOutput 展开 + combineAndTruncateLines 截断共享 DRY */
export function combineOutputs(stdout: string | undefined, stderr: string | undefined): string {
  const parts: string[] = []
  if (stdout) parts.push(stdout)
  if (stderr) parts.push(stderr)
  return parts.join('\n')
}

/** 合并 stdout/stderr 取前 N 行,返回截断后文本 + 是否截断 + 总行数(合并复用 combineOutputs) */
export function combineAndTruncateLines(stdout: string | undefined, stderr: string | undefined, maxLines: number): { text: string; truncated: boolean; total: number } {
  const combined = combineOutputs(stdout, stderr)
  if (!combined) return { text: '', truncated: false, total: 0 }
  const allLines = combined.split('\n')
  const total = allLines.length
  if (total <= maxLines) {
    return { text: combined, truncated: false, total }
  }
  // 取前 N 行 + 省略提示
  return { text: allLines.slice(0, maxLines).join('\n'), truncated: true, total }
}

/**
 * run_command 结果摘要:exit_code 标题 + stdout/stderr 前 N 行 + 总行数提示(截断,不全量显示)。
 * 输出可能很大(编译/find/cat 大文件),仅显示前若干行 + 总行数提示,防撑爆卡片。
 */
export function formatCommandResult(r: ToolResult): string {
  const code = typeof r.exit_code === 'number' ? r.exit_code : -1
  const ms = typeof r.duration_ms === 'number' ? r.duration_ms : 0
  const head = code === 0
    ? t('aiTool.commandOk', { ms })
    : t('aiTool.commandFailed', { code })
  // 合并 stdout + stderr(实际 stderr 通常含报错),各自取前若干行
  const MAX_LINES = 20
  const out = combineAndTruncateLines(r.stdout, r.stderr, MAX_LINES)
  const parts = [head]
  if (out.text) {
    parts.push(out.text)
    if (out.truncated) {
      parts.push(t('aiTool.commandOutputLines', { n: out.total }))
    }
  }
  return parts.join('\n')
}

/** file_info 结果摘要:存在性 + 大小 + 行数(文本)/二进制/目录 标注 */
export function formatFileInfoResult(r: ToolResult): string {
  if (r.exists === false) {
    return t('aiTool.fileInfoMissing', { path: r.path || '' })
  }
  let base = t('aiTool.fileInfoSize', { path: r.path || '', size: formatBytes(typeof r.size === 'number' ? r.size : 0) })
  const extras: string[] = []
  // UX-260618-12: file_info modified 时间(epoch_ms → 客户端 toLocaleString)
  if (typeof r.modified === 'number' && r.modified > 0) {
    extras.push(t('aiTool.fileInfoModified', { time: new Date(r.modified).toLocaleString() }))
  }
  if (r.is_dir) {
    extras.push(t('aiTool.fileInfoDir'))
  } else {
    if (typeof r.lines === 'number') {
      extras.push(t('aiTool.fileInfoLines', { n: r.lines }))
    }
    if (r.is_binary) {
      extras.push(t('aiTool.fileInfoBinary'))
    }
  }
  return base + extras.join('')
}

/**
 * 通用工具结果格式化(非文件类工具)。
 * UX-260616-04:统一 switch 覆盖全部工具产出人类可读摘要,不再走裸 JSON 兜底。
 * read_file/list_directory/write_file 有模板特化分支,不进此函数。
 *
 * P0-3: switch 17 case 拆为策略表 TOOL_FORMATTERS(name → 纯函数),formatToolResult 查表分发。
 * 新增工具只在此表加一行,零改 formatToolResult 主体。逻辑与原 switch 等价(零行为变更)。
 */

// list_* 系列(list_projects/list_tasks/list_ideas/list_trash)共享格式化:
// 渲染结构化列表替代裸 JSON,头行按 name 选 i18n key,条目取 name/title/id + status/stack 标注。
function formatListResult(r: ToolResult, name: string): string {
  // UX-260618-12: list_* 后端返 {items,total,has_more} 对象(非数组)
  const items = Array.isArray(r) ? r : (Array.isArray(r.items) ? r.items : [])
  const total = typeof r.total === 'number' ? r.total : items.length
  const head = name === 'list_projects' ? t('aiTool.projectCount', { n: total })
    : name === 'list_tasks' ? t('aiTool.taskCount', { n: total })
    : name === 'list_ideas' ? t('aiTool.ideaCount', { n: total })
    : t('aiTool.trashCount', { n: total })
  const lines = items.slice(0, 10).map((it: Record<string, unknown>) => {
    const label = typeof it.name === 'string' ? it.name
      : typeof it.title === 'string' ? it.title
      : typeof it.id === 'string' ? it.id
      : ''
    const status = typeof it.status === 'string' ? ` [${it.status}]` : ''
    const stack = typeof it.stack === 'string' ? ` (${it.stack})` : ''
    return label ? `- ${label}${status}${stack}` : ''
  }).filter(Boolean)
  const parts = [head, ...lines]
  if (r.has_more) parts.push(t('aiTool.listHasMore', { n: items.length }))
  return parts.join('\n')
}

/** advance_task 共享格式化(body + summary 一致): review_rounds>0 显退回累加轮数,否则首推原文案 */
function formatAdvanceTask(r: ToolResult): string {
  // UX-260618-09: review_rounds>0 显退回累加轮数,=0 回退原文案(首推无噪音)
  // UX-260618-14: 后端返 TaskRecord(含 title),优先显 title,无则回退 id(防裸 UUID)
  return typeof r.review_rounds === 'number' && r.review_rounds > 0
    ? t('aiTool.advancedTaskWithRounds', { title: r.title || r.id || '', status: r.status || '', n: r.review_rounds })
    : t('aiTool.advancedTask', { title: r.title || r.id || '', status: r.status || '' })
}

/** update_task 共享格式化: updated!==false 显字段更新,否则通用 updated */
function formatUpdateTask(r: ToolResult): string {
  // UX-260619-04: title 优先(后端补返 title),无则回退 id(防裸 UUID),对齐 advance_task。
  return r.updated !== false
    ? t('aiTool.updatedTaskField', { title: r.title || r.id || '', field: r.field || '' })
    : t('aiTool.updated')
}

/** delete_task/delete_project 共享格式化: deleted 标志 */
function formatDelete(r: ToolResult): string {
  return r.deleted ? t('aiTool.deleted') : t('aiTool.deleteIneffective')
}

/** delete_file 共享格式化: permanent 走永久删,否则软删带 backup_path */
function formatDeleteFile(r: ToolResult): string {
  return r.permanent
    ? t('aiTool.deleteFilePermanent', { path: r.path || '' })
    : t('aiTool.deleteFileSoft', { path: r.path || '', backup_path: r.backup_path || '' })
}

/** patch_file 共享格式化: changed===false 显无变化,否则显 size_diff */
function formatPatchFile(r: ToolResult): string {
  return r.changed === false
    ? t('aiTool.patchedNoChange', { path: r.path || '' })
    : t('aiTool.patchedFile', { path: r.path || '', diff: formatSizeDiff(r.size_diff) })
}

/** append_file 共享格式化: path + bytes_written */
function formatAppendFile(r: ToolResult): string {
  return t('aiTool.appendedTo', { path: r.path || '', bytes: formatBytes(r.bytes_written) })
}

/** search_files 共享格式化: total/results.length 取命中数 */
function formatSearchFiles(r: ToolResult): string {
  const n = typeof r.total === 'number' ? r.total : (Array.isArray(r.results) ? r.results.length : 0)
  return t('aiTool.foundN', { n })
}

/**
 * grep 摘要/命中计数标签(F-260621):按 output_mode 取合适文案,三分支:
 * - content(默认):「命中 N 处」(total = 命中行数,total 缺省回退 matches.length)
 * - files_with_matches:「命中 N 文件」(files.length)
 * - count:「命中 N 处 / M 文件」(total 行数 + total_files)
 *
 * 抽公共单一函数 G-1:原 ToolResultBody.vue grepHitLabel 与此处 formatGrep 三分支逐行等价,
 * 现 export 此函数,ToolResultBody.vue 复用,消除文案漂移风险。i18n 走 @/i18n/i18n-helpers 类型安全 t。
 */
export function formatGrep(r: ToolResult): string {
  const mode = r.output_mode
  if (mode === 'files_with_matches') {
    const n = Array.isArray(r.files) ? r.files.length : 0
    return t('aiTool.grepFilesN', { n })
  }
  if (mode === 'count') {
    const total = typeof r.total === 'number' ? r.total : 0
    const files = typeof r.total_files === 'number' ? r.total_files : 0
    return t('aiTool.grepCountSummary', { n: total, files })
  }
  // content(默认)
  const n = typeof r.total === 'number' ? r.total : (Array.isArray(r.matches) ? r.matches.length : 0)
  return t('aiTool.grepHitsN', { n })
}

/**
 * rename_file 格式化(body 版): renamed===false 走 formatJson 兜底(summary 版走空串)。
 * 故 body 与 summary 分别登记不同 formatter。
 */
function formatRenameFile(r: ToolResult): string {
  return r.renamed
    ? (r.cross_volume
        ? t('aiTool.renamedCrossVolume', { from: r.from || '', to: r.to || '', bytes: formatBytes(r.bytes_moved) })
        : t('aiTool.renamedWithBytes', { from: r.from || '', to: r.to || '', bytes: formatBytes(r.bytes_moved) }))
    : formatJson(r)
}

/** bind_directory 格式化(body 版): bound===false 走 formatJson 兜底(summary 版走空串) */
function formatBindDir(r: ToolResult): string {
  return r.bound
    ? t('aiTool.boundDir', { path: r.path || '', stack: r.stack || '' })
    : formatJson(r)
}

/** run_workflow 格式化(body 版):无 execution_id 走 formatJson,否则显完整三要素(task_id/target_status/exec) */
function formatRunWorkflow(r: ToolResult): string {
  // UX-260618-05: 后端返 {task_id,target_status,execution_id,status,note},显 execution_id 对应执行实例
  const execId = typeof r.execution_id === 'string' && r.execution_id
    ? r.execution_id
    : (typeof r.id === 'string' ? r.id : '')
  if (!execId) return formatJson(r)
  return t('aiTool.workflowTriggered', { id: r.task_id || '', status: r.target_status || '', exec: execId })
}

/** 工具结果格式化策略表(name → 纯函数)。新增工具在此加一行,不动 formatToolResult 主体。 */
const TOOL_FORMATTERS: Record<string, (r: ToolResult) => string> = {
  list_projects: r => formatListResult(r, 'list_projects'),
  list_tasks: r => formatListResult(r, 'list_tasks'),
  list_ideas: r => formatListResult(r, 'list_ideas'),
  list_trash: r => formatListResult(r, 'list_trash'),
  write_file: r => `${r.path} (${formatBytes(r.bytes_written)})`,
  update_task: formatUpdateTask,
  advance_task: formatAdvanceTask,
  delete_task: formatDelete,
  delete_file: formatDeleteFile,
  run_command: formatCommandResult,
  patch_file: formatPatchFile,
  append_file: formatAppendFile,
  rename_file: formatRenameFile,
  search_files: formatSearchFiles,
  file_info: formatFileInfoResult,
  bind_directory: formatBindDir,
  run_workflow: formatRunWorkflow,
  http_request: formatHttpResult,
  fetch_url: formatFetchUrl,
  grep: formatGrep,
}

export function formatToolResult(tc: AiToolCallInfo): string {
  const r = parseResult(tc.result)
  if (!r) {
    // 非 JSON(纯文本:审批占位/错误/拒绝)→ 原样显示,审批占位时补拟执行参数避免空白
    const raw = typeof tc.result === 'string' ? tc.result : ''
    if (raw && tc.args && typeof tc.args === 'object') {
      const a = tc.args as Record<string, unknown>
      const detail = typeof a.title === 'string' ? a.title
        : typeof a.name === 'string' ? a.name
        : typeof a.path === 'string' ? a.path
        : ''
      if (detail) return t('aiTool.resultDetail', { raw, detail })
    }
    return raw
  }
  const fn = TOOL_FORMATTERS[tc.name]
  return fn ? fn(r) : formatJson(r)
}

/**
 * http_request 结果摘要:状态码标题(method + 最终 URL) + 截断的 body(前 N 行) + 耗时/字节数。
 * 后端已截断 body 到 50KB + parse 格式化(json pretty/auto),此处仅做行数限制(前 20 行)
 * 防大 JSON 撑爆折叠态卡片。展开态由 ToolCard.vue http_request 分支全量展示。
 */
export function formatHttpResult(r: ToolResult): string {
  const status = typeof r.status === 'number' ? r.status : 0
  const method = typeof r.method === 'string' ? r.method : 'HTTP'
  const url = typeof r.url === 'string' ? r.url : ''
  const ms = typeof r.elapsed_ms === 'number' ? r.elapsed_ms : 0
  const ok = status >= 200 && status < 300
  const head = ok
    ? t('aiTool.httpOk', { method, status, ms })
    : t('aiTool.httpFailed', { method, status })
  const parts: string[] = [head, url]
  const body = typeof r.body === 'string' ? r.body : ''
  if (body) {
    const MAX_LINES = 20
    const lines = body.split('\n')
    if (lines.length > MAX_LINES) {
      parts.push(lines.slice(0, MAX_LINES).join('\n'))
      parts.push(t('aiTool.commandOutputLines', { n: lines.length }))
    } else {
      parts.push(body)
    }
  }
  const trunc = r.truncated === true
  const bytes = typeof r.body_bytes === 'number' ? r.body_bytes : 0
  if (trunc) {
    parts.push(t('aiTool.httpTruncated', { bytes: formatBytes(bytes) }))
  }
  return parts.join('\n')
}

/**
 * fetch_url 结果摘要(commands/ai/fetch_url.rs):URL + render_mode 标识 + markdown(截断)。
 *
 * 通用 fallback 文案(供 formatToolResult 在 ToolResultBody 无专用模板前/或解析失败时回退)。
 * 折叠态 header 文案由 summaryFetchUrl 单独提供(更短)。markdown 主体由 ToolResultBody
 * fetch_url 专用模板渲染(可折叠 + obscura 引导按钮),此 formatter 仅做兜底文本。
 *
 * - render_mode='obscura':成功用 obscura JS 渲染,显已渲染标识。
 * - render_mode='static_fallback':render=true 但 obscura 未装/失败 → 回退静态,显 hint。
 * - render_mode='static':普通静态渲染(默认)。
 */
export function formatFetchUrl(r: ToolResult): string {
  const url = typeof r.url === 'string' ? r.url : ''
  const title = typeof r.title === 'string' && r.title ? r.title : ''
  const mode = typeof r.render_mode === 'string' ? r.render_mode : 'static'
  const parts: string[] = []
  if (title) parts.push(t('aiTool.fetchUrlTitle', { title }))
  parts.push(url)
  if (mode === 'obscura') {
    parts.push(t('aiTool.fetchUrlRendered'))
  } else if (mode === 'static_fallback') {
    parts.push(t('aiTool.fetchUrlFallbackHint'))
  }
  const md = typeof r.markdown === 'string' ? r.markdown : ''
  if (md) {
    const MAX_LINES = 20
    const lines = md.split('\n')
    if (lines.length > MAX_LINES) {
      parts.push(lines.slice(0, MAX_LINES).join('\n'))
      parts.push(t('aiTool.commandOutputLines', { n: lines.length }))
    } else {
      parts.push(md)
    }
  }
  return parts.join('\n')
}

/**
 * completed 工具结果的一句话摘要(折叠态 header 也能看到,避免光秃秃)。
 * UX-260616-04:覆盖范围与 formatToolResult 对齐(header+body 一致);
 * read_file/list_directory/write_file 有模板特化分支,header 摘要仍显示关键计数。
 *
 * P0-3: switch 拆为策略表 TOOL_SUMMARIES(name → 纯函数),toolResultSummary 查表分发。
 * 与 TOOL_FORMATTERS 不共用:summary 版部分 case 输出更短(create/update_project/restore/purge 仅 summary 有;
 * rename/bind 的 false 分支返空串而非 formatJson;run_workflow 用 workflowTriggeredShort;list_* 多 has_more 判断差异)。
 * 共享语义的纯函数(formatAdvanceTask/formatUpdateTask/formatDelete/formatDeleteFile/formatPatchFile/
 * formatAppendFile/formatSearchFiles)两个表共用,DRY。零行为变更。
 */

/** list_* summary 版:n=0 返空串(折叠态无噪音),按 name 选 i18n key */
function summaryList(r: ToolResult, name: string): string {
  // UX-260618-10/12: 后端返 {items,total,has_more} 对象(非数组),原 Array.isArray(r) 恒 false→折叠态无计数
  const n = Array.isArray(r) ? r.length
    : (typeof r.total === 'number' ? r.total
      : (Array.isArray(r.items) ? r.items.length : 0))
  if (!n) return ''
  const key = name === 'list_projects' ? 'projectCount'
    : name === 'list_tasks' ? 'taskCount'
    : name === 'list_ideas' ? 'ideaCount'
    : 'trashCount'
  return t(`aiTool.${key}`, { n })
}

/** run_command summary 版:仅 exit_code + duration_ms 一行(不带输出) */
function summaryRunCommand(r: ToolResult): string {
  const code = typeof r.exit_code === 'number' ? r.exit_code : -1
  const ms = typeof r.duration_ms === 'number' ? r.duration_ms : 0
  return code === 0 ? t('aiTool.commandOk', { ms }) : t('aiTool.commandFailed', { code })
}

/** file_info summary 版:存在性 + 大小(不带 modified/lines/binary extras) */
function summaryFileInfo(r: ToolResult): string {
  return r.exists === false
    ? t('aiTool.fileInfoMissing', { path: r.path || '' })
    : t('aiTool.fileInfoSize', { path: r.path || '', size: formatBytes(typeof r.size === 'number' ? r.size : 0) })
}

/** rename_file summary 版:renamed===false 返空串(body 版走 formatJson) */
function summaryRenameFile(r: ToolResult): string {
  return r.renamed
    ? (r.cross_volume
        ? t('aiTool.renamedCrossVolume', { from: r.from || '', to: r.to || '', bytes: formatBytes(r.bytes_moved) })
        : t('aiTool.renamedWithBytes', { from: r.from || '', to: r.to || '', bytes: formatBytes(r.bytes_moved) }))
    : ''
}

/** bind_directory summary 版:bound===false 返空串(body 版走 formatJson) */
function summaryBindDir(r: ToolResult): string {
  return r.bound
    ? t('aiTool.boundDir', { path: r.path || '', stack: r.stack || '' })
    : ''
}

/** run_workflow summary 版:无 execution_id 返空串,有则显 workflowTriggeredShort(body 版显完整三要素) */
function summaryRunWorkflow(r: ToolResult): string {
  // UX-260618-05: header 副标题显 execution_id 概要(原 workflowHint「请到工作流页面运行」误导——工作流已触发)
  const execId = typeof r.execution_id === 'string' && r.execution_id
    ? r.execution_id
    : (typeof r.id === 'string' ? r.id : '')
  return execId ? t('aiTool.workflowTriggeredShort', { exec: execId }) : ''
}

/** http_request summary 版:仅状态码标题(method + status,不带 url/body) */
function summaryHttpRequest(r: ToolResult): string {
  const status = typeof r.status === 'number' ? r.status : 0
  const method = typeof r.method === 'string' ? r.method : 'HTTP'
  const ms = typeof r.elapsed_ms === 'number' ? r.elapsed_ms : 0
  const ok = status >= 200 && status < 300
  return ok
    ? t('aiTool.httpOk', { method, status, ms })
    : t('aiTool.httpFailed', { method, status })
}

/**
 * fetch_url summary 版(折叠态 header 副标题):
 * - obscura 已装渲染成功 → "已 JS 渲染"
 * - static_fallback(obscura 未装/失败) → "obscura 未装,已静态回退"(红色提示,引导安装)
 * - static(默认静态) → 显 markdown 截断字符数或空(无 markdown 静默)
 * 返空串 → header 不显副标题(对齐其他 summary 缺数据返空)。
 */
function summaryFetchUrl(r: ToolResult): string {
  const mode = typeof r.render_mode === 'string' ? r.render_mode : 'static'
  if (mode === 'obscura') return t('aiTool.fetchUrlRendered')
  if (mode === 'static_fallback') return t('aiTool.fetchUrlFallbackHint')
  const len = typeof r.length === 'number' ? r.length : 0
  return len > 0 ? t('aiTool.fetchUrlChars', { n: len }) : ''
}

/** 工具结果摘要策略表(name → 纯函数)。仅 summary 独有的工具(create/update_project/restore/purge)在此登记。 */
const TOOL_SUMMARIES: Record<string, (r: ToolResult) => string> = {
  list_projects: r => summaryList(r, 'list_projects'),
  list_tasks: r => summaryList(r, 'list_tasks'),
  list_ideas: r => summaryList(r, 'list_ideas'),
  list_trash: r => summaryList(r, 'list_trash'),
  create_project: r => r.name ? t('aiTool.createdWithName', { name: r.name }) : t('aiTool.created'),
  create_idea: r => r.title ? t('aiTool.createdWithName', { name: r.title }) : t('aiTool.created'),
  create_task: r => r.title ? t('aiTool.createdWithName', { name: r.title }) : t('aiTool.created'),
  update_project: r => r.field ? t('aiTool.updatedField', { field: r.field }) : t('aiTool.updated'),
  update_task: formatUpdateTask,
  advance_task: formatAdvanceTask,
  delete_project: formatDelete,
  delete_task: formatDelete,
  delete_file: formatDeleteFile,
  restore_project: r => r.restored ? t('aiTool.restored') : t('aiTool.restoreIneffective'),
  purge_project: r => r.purged ? t('aiTool.purged') : t('aiTool.purgeIneffective'),
  run_command: summaryRunCommand,
  patch_file: formatPatchFile,
  append_file: formatAppendFile,
  rename_file: summaryRenameFile,
  search_files: formatSearchFiles,
  file_info: summaryFileInfo,
  bind_directory: summaryBindDir,
  run_workflow: summaryRunWorkflow,
  http_request: summaryHttpRequest,
  fetch_url: summaryFetchUrl,
  grep: formatGrep,
}

export function toolResultSummary(tc: AiToolCallInfo): string {
  if (tc.status !== 'completed') return ''
  const r = parseResult(tc.result)
  if (!r) return ''
  const fn = TOOL_SUMMARIES[tc.name]
  return fn ? fn(r) : ''
}
