//! AI composables 共享层 — 多 composable 共用的模块级状态与纯函数
//!
//! 存在意义:打破 composable 间循环依赖。
//! - 原环1(useAiEvents ↔ useAiStream):nextMsgId 下沉到本模块消除
//! - 原环2(useAiEvents ↔ useAiSend):findToolCall + 审批计时器 下沉到本模块消除
//!
//! 下沉范围(严格限定为「无依赖、被多个 composable 共用」的成员):
//! - nextMsgId / _msgCounter:events/stream/send/window 四个 composable 共用同一计数器
//! - findToolCall:events/send 两个 composable 共用的纯查找函数(仅读 state.messages)
//! - 审批计时器(startApprovalTimer/clearApprovalTimer/clearAllApprovalTimers):
//!   events 模块触发(start/clear/allClear),send 模块也需导出给组件,故下沉到共享层

import { reactive } from 'vue'
import { aiApi } from '@/api'
import { state } from '@/stores/ai'
import { useAppSettingsStore } from '@/stores/appSettings'
import { t } from '@/i18n/i18n-helpers'
import type { AiToolCallInfo, ConvState } from '@/api/types'

const appSettings = useAppSettingsStore()

/**
 * 解析 AI 回复语言(useAiSend.sendMessage / useAiContext.compressContext 共用)。
 *
 * df-ai-language='auto' → 回落到 df-language(应用主语言);否则用 df-ai-language 指定值;
 * 兜底 'zh-CN'。
 *
 * 原分别在 useAiSend.resolveLang(三元)与 useAiContext.resolveLanguage(if/else)各一份,
 * 语义相同仅写法不同,提取至此共用。
 */
export function resolveAiLang(): string {
  const raw = appSettings.get<string>('df-ai-language', 'auto')
  return raw === 'auto'
    ? appSettings.get<string>('df-language', 'zh-CN')
    : raw
}

/** 客户端消息自增 id(全局唯一,多个 composable 共用 nextMsgId) */
let _msgCounter = 0

/** 全局消息 id 自增(供 events/stream/send/window 各 composable 共享同一计数器) */
export function nextMsgId(): number {
  return ++_msgCounter
}

/**
 * 在全部消息中查找指定 id 的工具调用卡片(用于状态流转)
 *
 * 扫描策略:从尾部反向遍历并提前退出。tool_result/approval/completed
 * 通常对应最近触发的 tool_use(agent 顺序执行工具并尽快回填结果),
 * 反向扫描使其命中落在尾部 → 平均 O(1)、最坏 O(n)。(原正向全量扫描为 O(n²),
 * 每条 tool_result 都从头扫整条历史。)
 *
 * 注:未引入 Map<id, tc> 索引。state.messages 在 newConversation /
 * switchConversation / deleteConversation 等处被整体替换(且这些操作散落在
 * useAiEvents 之外),独立索引极易与响应式数组失配形成"陈旧引用"——状态卡
 * 片读到已不存在的对象。反向扫描零额外状态、改动最小且行为完全一致。
 */
export function findToolCall(id: string): AiToolCallInfo | undefined {
  for (let i = state.messages.length - 1; i >= 0; i--) {
    const toolCalls = state.messages[i].toolCalls
    if (!toolCalls) continue
    // 同一消息内工具调用通常很少,沿用正向扫描;命中即返回
    for (let j = 0; j < toolCalls.length; j++) {
      if (toolCalls[j].id === id) return toolCalls[j]
    }
  }
  return undefined
}

// ── 审批超时计时器(原 useAiSend,下沉打破 events↔send 循环依赖) ──

/// 审批等待超时阈值(ms) — 用户 5 分钟内不处理则自动拒绝
const APPROVAL_TIMEOUT_MS = 5 * 60 * 1000

/** toolCallId → 审批超时计时器 */
const _approvalTimers = new Map<string, ReturnType<typeof setTimeout>>()

/**
 * 启动审批超时计时器(幂等:同 id 重复启动不重建)。
 * 到点回调按 kind 分派拒绝路径 + push 系统错误消息:
 * - kind='risk'(risk 类审批,如 write_file):调 aiApi.approve(id, false) → 后端 ai_approve(kind==Risk)。
 * - kind='path'(路径授权挂起):调 aiApi.authorizeDir(id, 'deny') → 后端 ai_authorize_dir 续 loop 返 Err。
 *   修复回归:path 类若错调 ai_approve,后端 ai_approve 校验 kind==Risk 直接拒,IPC 不进续 loop → 卡死。
 *   memory: aiShared.startApprovalTimer 回调按 kind 分派(risk→approve / path→authorizeDir('deny')),
 *   path 类错误复用 risk 的 ai_approve 致后端 kind 校验拒、续 loop 不触发 → 卡死。
 */
export function startApprovalTimer(
  toolCallId: string,
  toolName: string,
  kind: 'risk' | 'path' = 'risk',
): void {
  if (_approvalTimers.has(toolCallId)) return
  const timer = setTimeout(() => {
    _approvalTimers.delete(toolCallId)
    // 按 kind 分派拒绝路径,避免 path 类错调 ai_approve 致后端 kind 校验拒而卡死
    const denyP = kind === 'path'
      ? aiApi.authorizeDir(toolCallId, 'deny')
      : aiApi.approve(toolCallId, false)
    denyP.catch(e => {
      console.error('[AI] 审批超时自动拒绝 IPC 未送达:', e)
    })
    state.messages.push({
      id: `approval-timeout-${nextMsgId()}`,
      role: 'assistant',
      content: t('ai.approvalTimeout', { toolName }),
      isError: true,
      timestamp: Date.now(),
    })
  }, APPROVAL_TIMEOUT_MS)
  _approvalTimers.set(toolCallId, timer)
}

/** 清除单个审批计时器 */
export function clearApprovalTimer(toolCallId: string): void {
  const timer = _approvalTimers.get(toolCallId)
  if (timer) {
    clearTimeout(timer)
    _approvalTimers.delete(toolCallId)
  }
}

/** 清除全部审批计时器(整流超时收尾/组件卸载时调用) */
export function clearAllApprovalTimers(): void {
  for (const timer of _approvalTimers.values()) clearTimeout(timer)
  _approvalTimers.clear()
}

// ── L2 统一状态机 convStates(下沉自 useAiEvents,批4 双轨收口) ──
//
// 下沉原因:批4 退役 generatingConvs(bool 轨)后,stores/ai.ts(isGenerating)/
// useAiStream.ts(onStreamTimeout stillGenerating)/useAiConversations.ts(switchConversation 重算)
// 等读点均需 import getConvState。若 getConvState 留在 useAiEvents,这些模块反向 import
// useAiEvents 会与 useAiEvents→useAiStream/useAiEvents←stores 的现有依赖构成环。
// 故将 convStates Map + getConvState + setConvState 全部下沉到本共享层(与 nextMsgId/findToolCall/
// 审批计时器同位置,沿用既有破环模式)。useAiEvents 改为从本模块 import 它们。

/**
 * per-conv conv_state 真相源(消费后端 AiConvStateChanged 事件)。
 *
 * 模块级 reactive Map(convId → ConvState),不进 store.state(与生成生命周期语义独立于
 * 消息/会话列表,同 pendingMaxRounds/pendingHelp 模式)。批4 收口后,本 Map 是「会话是否生成中」
 * 的唯一真相源,generatingConvs bool 轨已退役。
 */
export const convStates = reactive(new Map<string, ConvState>())

/**
 * 取某 conv 的 conv_state;未追踪过返回 null。
 *
 * 桥接语义(批4 收口):bool 轨 generatingConvs.has(id) 等价于本函数返回值 ∈
 * {'generating','stopping','compressed'}(非终止三态);idle/error/null 对应 bool 轨 has()=false。
 * 依据:handleConvStateEvent 原同步逻辑将三态都 add,终止态 delete。
 */
export function getConvState(convId: string | null | undefined): ConvState | null {
  if (!convId) return null
  return convStates.get(convId) ?? null
}

/**
 * 写入 conv_state(useAiEvents.handleConvStateEvent 的 AiConvStateChanged case 唯一写入口,
 * 及活跃事件兜底写 generating 态)。
 *
 * idle 为终态收敛:写后删 Map 项(与后端 generating=false 语义对齐,Map 不持陈旧 idle 项)。
 */
export function setConvState(convId: string | null | undefined, s: ConvState): void {
  if (!convId) return
  if (s === 'idle') {
    convStates.delete(convId)
  } else {
    convStates.set(convId, s)
  }
}
