//! AI 聊天 Store — 模块级单例 state + useAiStore() 统一入口
//!
//! 架构(路线A:state 单例 + 逻辑外移到 composables):
//! - 本文件只保留模块级单例 reactive state(19 字段)+ useAiStore() 统一入口
//! - 所有方法搬到 src/composables/ai/*,通过 ...useAiXxx(state) 展开到 useAiStore() 返回值
//! - 组件用法不变:`const { state, sendMessage, ... } = useAiStore()` 零改动
//! - composables 之间通过模块级 import 直接互调(events↔send↔stream 耦合)
//!
//! composables 职责:
//! - useAiEvents    startListener/stopListener/handleEvent/flushCurrentText/findToolCall/friendlyError/notifyConversationChanged
//! - useAiStream    onStreamTimeout/resetStreamWatchdog/clearStreamWatchdog + STREAM_TIMEOUT_MS
//! - useAiSend      sendMessage/approveToolCall/drainQueue/cancelQueued/clearQueue/stopChat
//! - useAiConversations  loadConversations/newConversation/switchConversation/deleteConversation/renameConversation/archiveConversation/toggleArchivedFold/toggleSidebar
//! - useAiWindow    detachPanel/reattachPanel/resumeInDetached/closeDetachedWindow/dockDetached/syncToMain/startFollowMain/stopFollowMain
//! - useAiPanel     togglePanel/toggleMaximize/loadProviders/loadSkills/setProvider/clearChat + restoreUiState/persistUiState
//!
//! 注意:
//! - 各 composable 模块加载时会执行其顶层副作用(useAiPanel.restoreUiState + watch appSettings),
//!   故 useAiStore() 之外无需手动 init;以下 import 即触发恢复逻辑
//! - state 为模块级单例,全应用共享同一引用

import { computed, reactive, watch } from 'vue'
import type { AiChatEvent, AiConversationSummary, AiMessage, AiProviderConfig, AiToolCallInfo, ContentPart, SkillInfo } from '@/api/types'

/**
 * 单对话 messages 软上限(滚动淘汰)。
 *
 * 设计要点:
 * - 单对话内 messages 线性增长(useAiSend push user+ai / useAiEvents push err 审批 / useAiStream push delta),
 *   长会话 + 大工具结果内存可观;push 触发后,若超限 splice 掉最早条,保近期消息可见。
 * - 切换对话走 `state.messages = rawMsgs` 整体替换(见 useAiConversations),是另一减负点,
 *   不受本上限约束 —— watch 仅拦截"push 式增长"(length 小幅单调增,oldVal>0 且增量≤2),
 *   对整体替换(length 大跳变 / oldVal=0 首次加载)放行,保留后端历史全量。
 * - 现有 push 逻辑(composables 内)零改动,上限由本模块 watch 统一兜底。
 */
const MESSAGE_CAP = 200

/**
 * 单对话 messages 的 parts 体积软上限(UX-260617-20)。
 *
 * 背景:MESSAGE_CAP 仅按"条数"裁剪,不感知单条体积。多模态 base64 图片(F-260614-05)
 * 单条可达数 MB,极端 200 条 × 5MB ≈ 1GB 会撑爆渲染内存。
 *
 * 设计:
 * - 仅度量 image 片的 base64/url 字符串长度(其余 text/diff 普通字符串可忽略量级);
 *   base64 char 数 ≈ 编码后字节数(3/4 编码开销),量纲稳定可作预算代理。
 * - 超出预算时,从最旧消息起逐条裁掉其 parts(置空数组),保条数与文本可见、丢历史图片。
 * - 与 MESSAGE_CAP 互补:条数先裁体积小场景,体积裁多模态大体积场景;两 watch 各司其职。
 */
const PARTS_SIZE_BUDGET = 50 * 1024 * 1024 // 50MB(base64 char 计)
import { useAiEvents } from '@/composables/ai/useAiEvents'
import { useAiStream } from '@/composables/ai/useAiStream'
// 批4 双轨收口:getConvState 从 aiShared 取(下沉破环,避免 stores/ai↔useAiEvents 反向环)。
// F-09 per-conv streaming/currentText(DEC-07a 父④):convStreamStates Map + 辅助函数亦从 aiShared 取。
import { getConvState, getConvStreamState, setConvStreaming, setConvCurrentText, __bindMessages } from '@/composables/ai/aiShared'
import { useAiSend, initDrainQueueListener } from '@/composables/ai/useAiSend'
import { useAiApproval } from '@/composables/ai/useAiApproval'
import { useAiConversations } from '@/composables/ai/useAiConversations'
import { useAiWindow } from '@/composables/ai/useAiWindow'
import { useAiPanel } from '@/composables/ai/useAiPanel'
import { useAiContext } from '@/composables/ai/useAiContext'

/// 模块级单例 state — 全应用共享(composables 通过 `import { state } from '@/stores/ai'` 取用)
//
// F-09 B 路线 per-conv streaming/currentText(DEC-07a 父④):
// `streaming`/`currentText` 不再是全局单例字段,改为 accessor 委派 aiShared.convStreamStates
// (per-conv Map,定义于 composables/ai/aiShared.ts)按 activeConversationId 索引读写。
//
// 单会话回归铁律:单会话场景 activeConversationId 恒定,Map 仅此一项,accessor 行为等价原全局
// 单例(读返回该项值,写改该项值)。所有现有消费方 `store.state.streaming` / `store.state.currentText`
// 零改动透明继承(read = get active 项,write = set active 项)。
//
// 多会话收益:切会话(activeConversationId 变)即切到该会话的 stream state,A 后台生成中切到 B,
// B 的 streaming/currentText 读各自 Map 项,A 残留 currentText 不串到 B 视图(BUG-260624-01 根因清除)。
//
// accessor 实现要点:
// - get:读 activeConversationId → 取 convStreamStates 项;无 active/无项 → streaming 返回 false、
//   currentText 返回 ''(与原全局单例初值一致,空安全)。
// - set:读 activeConversationId → 写 convStreamStates 对应项;无 active 则 no-op(无目标 conv
//   无法路由,原全局单例此场景也写无意义的全局值,no-op 更干净)。
// - reactive 追踪:Vue 3 reactive 包装带 accessor 描述符的对象时,会保留 get/set 并经
//   Reflect.get/set 调用,track/trigger 正常工作。`state.streaming` 读取建立对 activeConversationId
//   + 对应 Map 项的响应式依赖,任一变化触发更新。
// - 闭包引用 state 本身:get/set 需读 state.activeConversationId,用下方 `stateRef` 闭包变量指向
//   reactive 包装后的对象(先声明后赋值,accessor 闭包延迟读 stateRef,避免 TDZ)。
const _stateBase: {
  messages: AiMessage[]
  // F-09 per-conv:streaming/currentText 是 accessor(下方 Object.defineProperty 定义),
  // 委派 aiShared.convStreamStates 按 activeConversationId 索引读写。streaming/currentText
  // 不在字面量显式赋值(由 accessor 提供),故 _stateBase 类型不含此两键;
  // 通过下方 StateWithStream 接口扩展导出 state 的完整类型(消费方编译可见)。
  pendingApprovals: AiToolCallInfo[]
  providers: AiProviderConfig[]
  activeProvider: string | null
  panelOpen: boolean
  maximized: boolean
  conversations: AiConversationSummary[]
  activeConversationId: string | null
  sidebarOpen: boolean
  sidebarWidth: number
  detached: boolean
  docked: boolean
  skills: SkillInfo[]
  queue: { text: string; skill?: string; enqueuedAt: number; parts?: ContentPart[]; conversationId?: string | null }[]
  agentRound: number
  /// 已完成工具调用数(本轮累计,AiToolCallCompleted 时 +1,AiAgentRound 时重置)
  completedTools: number
  archivedCollapsed: boolean
  foldedGroups: Record<string, boolean>
  searchQuery: string
  lastTokenUsage: { prompt: number; completion: number; total: number } | null
  convTokenTotal: { prompt: number; completion: number; total: number } | null
} = {
  messages: [],
  pendingApprovals: [],
  // 批4 双轨收口:generatingConvs(Set,bool 轨)已退役。会话生成态真相归 convStates
  // (reactive Map,enum 轨),定义于 composables/ai/aiShared.ts(下沉破环)。
  // 消费方经 isGenerating(convId) helper 或直接 getConvState(convId) 读,不再读 state.xxx。
  providers: [],
  activeProvider: null,
  panelOpen: true,
  // 面板模式：maximized=全宽占满main | sidebar=固定宽侧栏(可拖拽)
  maximized: true,
  // 对话管理
  conversations: [],
  activeConversationId: null,
  sidebarOpen: false,
  // 侧栏宽度(px,可拖拽;clamp 120~280,默认 200;持久化于 df-ai-ui。UX-2025-16)
  sidebarWidth: 200,
  // 窗口分离
  detached: false,
  // 吸附跟随中
  docked: false,
  // 本机技能（`/` 联想）
  skills: [],
  // 待发送队列（生成中发的消息排队，AiCompleted 后自动续发；enqueuedAt 记录入队时间供超时检测）
  // F-260614-05 Phase 2b: parts 挂多模态图片片(供 drainQueue/force 续发时本地 user 消息渲染图)
  queue: [],
  // Agentic 循环当前轮次(0=非agentic/未开始;>0=收到后端 AiAgentRound 事件的 round)
  // AE-2025-07:进度条用; AiCompleted/AiError 复位为 0
  agentRound: 0,
  completedTools: 0,
  // 归档分组折叠态(默认折叠)
  archivedCollapsed: true,
  // 时间分组折叠态(默认:更早折叠,今天/昨天展开;持久化进 df-ai-ui)
  foldedGroups: { today: false, yesterday: false, earlier: true },
  // 对话搜索关键字(UX-06 §3.1):非空时侧栏取消时间分组,平铺展示标题匹配的会话(updated_at DESC)。
  // 清空恢复原分组。消费方:Sidebar 渲染(读 searchQuery 决定走 filteredConversations 还是 groupedActive)。
  searchQuery: '',
  // token 用量展示(受 df-show-token-usage 开关控制;lastTokenUsage=最近一条回复,convTokenTotal=当前对话累计)
  lastTokenUsage: null,
  convTokenTotal: null,
}

// F-09 per-conv streaming/currentText accessor 委派层:先声明 stateRef 占位,
// reactive 包装后回填,accessor 闭包经 stateRef 读 activeConversationId。
let stateRef: typeof _stateBase | null = null

// 在 _stateBase 上定义 streaming/currentText accessor(委派 convStreamStates per-conv Map)。
// 注:用 Object.defineProperty 而非字面量 getter,避免与上方类型声明冲突;reactive 会保留此描述符。
Object.defineProperty(_stateBase, 'streaming', {
  enumerable: true,
  configurable: true,
  get(): boolean {
    const s = stateRef
    if (!s) return false
    return getConvStreamState(s.activeConversationId)?.streaming ?? false
  },
  set(value: boolean) {
    const s = stateRef
    if (!s) return
    setConvStreaming(s.activeConversationId, value)
  },
})
Object.defineProperty(_stateBase, 'currentText', {
  enumerable: true,
  configurable: true,
  get(): string {
    const s = stateRef
    if (!s) return ''
    return getConvStreamState(s.activeConversationId)?.currentText ?? ''
  },
  set(value: string) {
    const s = stateRef
    if (!s) return
    setConvCurrentText(s.activeConversationId, value)
  },
})

export const state = reactive(_stateBase) as typeof _stateBase & {
  streaming: boolean
  currentText: string
}
// 回填 stateRef:accessor 闭包读此引用(经 reactive 包装后的代理对象,activeConversationId 变化可追踪)。
stateRef = state
// 注入 messages getter 到 aiShared 共享层(破循环依赖,避免 Vite 打包 TDZ)。
// 用 getter 而非数组引用:switchConversation/newConversation 会整体替换 state.messages,
// 缓存引用会指向陈旧数组(findToolCall 找不到新数组的工具卡片 → Completed 事件无法更新 → 卡片卡 running)。
// 延迟执行:wrap 在 try/catch 中,防 ES module 初始化顺序导致 aiShared 未就绪时炸
try {
  __bindMessages(() => state.messages)
} catch (e) {
  // aiShared 模块初始化中(ES module 循环),延迟到微任务里重试
  Promise.resolve().then(() => __bindMessages(() => state.messages))
}

// 旁注:此处不再保留 type-only 导出(AiChatEvent 等),因组件直接从 api/types import。
// 若有外部模块仍从本文件 import 这些类型,下方 re-export 兜底:
export type { AiChatEvent, AiConversationSummary, AiMessage, AiProviderConfig, AiToolCallInfo, SkillInfo }

/**
 * messages 软上限守卫:push 式增长超 MESSAGE_CAP 时,splice 掉最早条滚动淘汰。
 *
 * 仅拦截 push(oldVal>0 且增量≤2);整体替换(切换对话)
 * 放行,避免截断后端历史。watch 在模块加载即注册(state 单例生命周期内常驻)。
 */
watch(
  () => state.messages.length,
  (newLen, oldLen) => {
    if (
      oldLen > 0 &&
      newLen - oldLen <= 2 && // 最多连续 push 2 条触发滚动(useAiSend user+ai=2)
      newLen > MESSAGE_CAP
    ) {
      state.messages.splice(0, newLen - MESSAGE_CAP)
    }
  },
)

/**
 * 估算单条消息 parts 的字符体积(base64/url 字符串长度之和,UX-260617-20)。
 *
 * 只计 image 片的 base64 与 url(大体量载荷);text 片(正文/工具结果文本)不在此预算口径内,
 * 避免对常规长文本消息误伤。无 parts 或非 image 片返回 0。
 */
function estimatePartsSize(parts: ContentPart[] | undefined): number {
  if (!parts || parts.length === 0) return 0
  let size = 0
  for (const p of parts) {
    if (p.type === 'image') {
      if (p.base64) size += p.base64.length
      if (p.url) size += p.url.length
    }
  }
  return size
}

/**
 * parts 体积软上限守卫(UX-260617-20):push 式增长使总 parts 体积超 PARTS_SIZE_BUDGET 时,
 * 从最旧消息起逐条置空其 parts(丢历史多模态图,保文本/条数)。
 *
 * 仅拦截 push(同上 length watch 的判定),避免切换对话整体替换时误裁后端历史图。
 */
watch(
  () => state.messages.length,
  (newLen, oldLen) => {
    if (oldLen <= 0 || newLen - oldLen > 2) return
    let total = 0
    for (const m of state.messages) total += estimatePartsSize(m.parts)
    if (total <= PARTS_SIZE_BUDGET) return
    // 超预算:从最旧起逐条清 parts,直到总量回落到预算内
    for (const m of state.messages) {
      if (total <= PARTS_SIZE_BUDGET) break
      const partSize = estimatePartsSize(m.parts)
      if (partSize > 0) {
        m.parts = []
        total -= partSize
      }
    }
  },
)

/**
 * 对话搜索过滤结果(UX-06 §3.1)。
 *
 * 行为:
 * - searchQuery 为空 → 返回 null(信号:消费方走原时间分组 groupedActive / 归档分组)。
 * - 非空 → 在全部对话(活跃+归档)中按标题做 case-insensitive 子串匹配,平铺返回
 *   (取消分组),按 updated_at DESC 排序,最近改过的在最前。
 *
 * 设计:过滤逻辑放 store 而非组件,供主窗口/分离窗口两处侧栏复用同一结果;
 * 消费方仅需 `store.filteredConversations` 即得数组或 null,UI 连接在侧栏视图内完成。
 */
const filteredConversations = computed<AiConversationSummary[] | null>(() => {
  const q = state.searchQuery.trim().toLowerCase()
  if (!q) return null
  return state.conversations
    .filter(c => (c.title ?? '').toLowerCase().includes(q))
    .sort((a, b) => (b.updated_at > a.updated_at ? 1 : b.updated_at < a.updated_at ? -1 : 0))
})

/**
 * F-09 多会话并发:生成态 helper(批4 双轨收口后改为派生)。
 *
 * 批4 收口:generatingConvs(Set,bool 轨)已退役,生成态真相归 convStates(enum Map,定义于
 * aiShared.ts)。本 helper 改派生自 getConvState:conv_state∈{generating,stopping,compressed}
 * (非终止三态)视为生成中,idle/error/null 视为不在生成。桥接语义对齐原 bool 轨 has() 判定。
 *
 * Vue 3 reactive 对 Map 的 get 有 proxy 跟踪,在 computed/template 内调用 isGenerating(convId)
 * 仍建立响应式依赖,convStates 变更时自动触发更新(等价原 Set 行为)。
 * addGenerating/removeGenerating 写 helper 已删(写收敛到 useAiEvents setConvState 唯一入口)。
 */
function isGenerating(convId: string | null | undefined): boolean {
  if (!convId) return false
  const cs = getConvState(convId)
  return cs === 'generating' || cs === 'stopping' || cs === 'compressed'
}

/**
 * AI Store 统一入口 — 返回单例 state + 全部方法。
 *
 * 组件零改动:解构 shape 与拆分前完全一致。每次调用都重新展开 composable 方法
 * (composable 内部全部是模块级单例实现,展开的只是引用,无重复实例化开销)。
 */
export function useAiStore() {
  // ARC-06: 启动 drain-queue 事件监听(由 useAiEvents AiCompleted 经事件总线触发)
  void initDrainQueueListener()
  return {
    state,
    filteredConversations,
    // F-09: 生成态 helper(批4 双轨收口:仅读 isGenerating 派生自 getConvState;
    // addGenerating/removeGenerating 写 helper 已删,写收敛到 useAiEvents setConvState)
    isGenerating,
    ...useAiEvents(),
    ...useAiStream(),
    ...useAiSend(),
    ...useAiApproval(),
    ...useAiConversations(),
    ...useAiWindow(),
    ...useAiPanel(),
    ...useAiContext(),
  }
}
