// Focus event 分类器 —— 动态上下文记忆池架构第 5b 步（v1 LLM 语义判断）
//
// 角色：v0 启发式（ngram + 字面交集）跑在前面，本模块只在「栈结构会变化」时被叫起来
// （pushed / returned），用 LLM 仲裁校验 + 重写 topic 关键词。
//
// 设计要点：
//   - 800ms 硬超时（参考 injector.js embedding 兜底），LLM 慢一拍就回退 v0
//   - 失败必须降级 —— 解析失败、超时、abort、配额限流，都返回 null 让上层用 v0
//   - 不依赖 SQLite / 上游状态 / 当前 process —— 纯函数，可单元测试（callLLM 可 stub）
//   - 不修改 state，不发事件，不写 db；返回值由上层 focus.js 应用
//
// 来自 DynamicMemoryPool.md 7.4「对话动作类型」：
//   kept / pushed / returned / leaf 是对话级动作；本模块按这四类输出 action。
//   leaf = 一次性短问，不动栈；映射到 v0 的 noop（不创建新帧也不深化栈顶）。

const CLASSIFIER_TIMEOUT_MS = 800
const CLASSIFIER_MAX_TOKENS = 120
const CLASSIFIER_TEMPERATURE = 0.2

const SYSTEM_PROMPT = `焦点分类器。保守判 kept，不轻易 push。
重叠度：高=对象同→kept；中=同域不同子任务→pushed；低=异域→pushed/leaf。
kept：栈顶重叠高且细化/追问/承诺/确认。
pushed：与所有帧重叠低且持续性新任务。
returned：与非栈顶旧帧重叠高且明确回指（depth=该帧索引，栈顶=length-1）。
leaf：无承接且一次性短问/闲聊（不动栈）。
例 A [前端 React]+"写个 Hook"→kept
例 B [DB 查询]+"现在网速咋样"→leaf
例 C [配置→部署→监控]+"回头看最初配置"→returned d=0
topic 写 2-3 个语义词非 ngram。只输 JSON。`

// 把当前栈渲染成简短字符串：[栈底"a, b" → "c, d" → 栈顶"e, f"]
function describeStack(stack) {
  if (!Array.isArray(stack) || stack.length === 0) return '[空栈]'
  const parts = stack.map((f, i) => {
    const topic = Array.isArray(f?.topic) ? f.topic.join(', ') : String(f?.topic || '')
    const conclusions = Array.isArray(f?.conclusions) && f.conclusions.length > 0
      ? `（结论: ${f.conclusions[f.conclusions.length - 1]}）`
      : ''
    const tag = i === 0 ? '栈底' : (i === stack.length - 1 ? '栈顶' : `第${i}层`)
    return `${tag}"${topic}"${conclusions}`
  })
  return '[' + parts.join(' → ') + ']'
}

// 构造用户输入文本
function buildUserPrompt({ newMessage, v0Event, v0Topic, currentStack }) {
  const v0TopicStr = Array.isArray(v0Topic) ? v0Topic.join(', ') : String(v0Topic || '')
  const stackStr = describeStack(currentStack)
  const lengthHint = currentStack?.length ? `栈深=${currentStack.length}，栈顶索引=${currentStack.length - 1}` : '栈深=0'
  // newMessage 截断到 400 字，省 token 也减少打架风险
  const msg = String(newMessage || '').slice(0, 400)
  return [
    `v0 判定 = ${v0Event}，候选 topic = [${v0TopicStr}]`,
    `当前栈（${lengthHint}） = ${stackStr}`,
    `新消息 = "${msg}"`,
    '',
    '请输出 JSON：{"action": "kept|pushed|returned|leaf", "topic_refined": ["词1","词2","词3"], "returns_to_depth": 0}',
    '（returns_to_depth 仅 returned 时有值；其他动作填 -1 或省略）',
  ].join('\n')
}

// 提取 LLM 文本中的 JSON 对象。容忍 ```json 包裹、前后多余文字。
function parseClassifierJson(text) {
  if (!text || typeof text !== 'string') return null
  // 去掉 <think> 块（如果模型把思考也输出了）
  let body = text.replace(/<think>[\s\S]*?<\/think>/g, '').trim()
  // 去掉 ```json ... ``` 围栏
  const fenceMatch = body.match(/```(?:json)?\s*([\s\S]*?)```/)
  if (fenceMatch) body = fenceMatch[1].trim()
  // 找第一个 { 到最后一个 }
  const first = body.indexOf('{')
  const last = body.lastIndexOf('}')
  if (first < 0 || last <= first) return null
  const jsonStr = body.slice(first, last + 1)
  try {
    return JSON.parse(jsonStr)
  } catch {
    return null
  }
}

// 校验 + 规范化 LLM 返回的 JSON
function normalizeClassifierResult(raw, currentStack) {
  if (!raw || typeof raw !== 'object') return null
  const action = String(raw.action || '').toLowerCase().trim()
  if (!['kept', 'pushed', 'returned', 'leaf'].includes(action)) return null

  let topic = []
  if (Array.isArray(raw.topic_refined)) {
    topic = raw.topic_refined
      .map(t => String(t || '').trim())
      .filter(t => t.length > 0 && t.length <= 32)
      .slice(0, 3)
  }

  let returnsToDepth = -1
  if (action === 'returned') {
    const d = Number.isInteger(raw.returns_to_depth) ? raw.returns_to_depth : -1
    const stackLen = Array.isArray(currentStack) ? currentStack.length : 0
    if (d < 0 || d >= stackLen) {
      // returned 但深度非法 → 视为不合理，拒掉
      return null
    }
    returnsToDepth = d
  }

  return { action, topic, returnsToDepth }
}

/**
 * 调 LLM 仲裁 focus 事件。
 *
 * @param {object} args
 * @param {string} args.newMessage   - 当前用户消息正文
 * @param {string} args.v0Event      - v0 启发式判定的 event（pushed / returned）
 * @param {string[]} args.v0Topic    - v0 抽出的候选 topic 关键词
 * @param {object[]} args.currentStack - 当前 focus 栈快照（不会被修改）
 * @param {AbortSignal} [args.signal] - 上层 abort 信号
 * @returns {Promise<{action:'kept'|'pushed'|'returned'|'leaf', topic:string[], returnsToDepth:number} | null>}
 *   返回 null 表示「失败 / 超时 / 解析不出来」，让上层回退到 v0。
 */
export async function classifyFocusEvent({
  newMessage,
  v0Event,
  v0Topic,
  currentStack,
  signal,
} = {}) {
  // 边界保护
  if (!newMessage || typeof newMessage !== 'string') return null
  if (signal?.aborted) return null

  const v0TopicStr = Array.isArray(v0Topic) ? v0Topic.join(',') : String(v0Topic || '')
  const tag = `[focus-classifier] v0=${v0Event} topic=[${v0TopicStr}]`

  // 动态 import callLLM —— 跟 injector.js 同款，避免在测试环境/早期模块加载时拉起一切
  let callLLM
  try {
    const llm = await import('../llm.js')
    callLLM = llm.callLLM
  } catch (e) {
    console.log(`${tag} → llm.js import 失败 (${e?.message || 'unknown'}) → 回退 v0`)
    return null
  }
  if (typeof callLLM !== 'function') {
    console.log(`${tag} → callLLM 不是函数 → 回退 v0`)
    return null
  }

  const userPrompt = buildUserPrompt({ newMessage, v0Event, v0Topic, currentStack })
  const t0 = Date.now()

  // 800ms 硬超时 + LLM 调用赛跑
  let timeoutHandle = null
  const timeoutPromise = new Promise(resolve => {
    timeoutHandle = setTimeout(() => resolve({ __timeout: true }), CLASSIFIER_TIMEOUT_MS)
  })

  let result
  try {
    result = await Promise.race([
      callLLM({
        systemPrompt: SYSTEM_PROMPT,
        message: userPrompt,
        temperature: CLASSIFIER_TEMPERATURE,
        thinking: false,
        tools: [],
        maxTokens: CLASSIFIER_MAX_TOKENS,
        mustReply: false,
        signal,
      }),
      timeoutPromise,
    ])
  } catch (e) {
    if (timeoutHandle) clearTimeout(timeoutHandle)
    const dt = Date.now() - t0
    console.log(`${tag} → LLM 抛错 (${dt}ms, ${e?.message || 'unknown'}) → 回退 v0`)
    return null
  }
  if (timeoutHandle) clearTimeout(timeoutHandle)

  const dt = Date.now() - t0
  if (!result || result.__timeout) {
    console.log(`${tag} → LLM 超时 (${CLASSIFIER_TIMEOUT_MS}ms 硬超时, 实际 ${dt}ms) → 回退 v0`)
    return null
  }
  if (result.aborted) {
    console.log(`${tag} → LLM aborted (${dt}ms) → 回退 v0`)
    return null
  }

  const content = typeof result === 'string' ? result : (result.content || '')
  const preview = String(content).replace(/\s+/g, ' ').slice(0, 200)
  const raw = parseClassifierJson(content)
  if (!raw) {
    console.log(`${tag} → LLM 返回 (${dt}ms) 但 JSON 解析失败 raw="${preview}" → 回退 v0`)
    return null
  }

  const normalized = normalizeClassifierResult(raw, currentStack)
  if (!normalized) {
    console.log(`${tag} → LLM 返回 (${dt}ms) action=${raw.action} 但 normalize 拒掉 (非法 action 或越界 depth) raw="${preview}" → 回退 v0`)
    return null
  }

  const refinedStr = normalized.topic.join(',')
  const depthStr = normalized.action === 'returned' ? ` d=${normalized.returnsToDepth}` : ''
  console.log(`${tag} → llm=${normalized.action}${depthStr} (${dt}ms) refined=[${refinedStr}] ok`)
  return normalized
}

// 暴露内部辅助函数，便于测试
export const __internal = {
  describeStack,
  buildUserPrompt,
  parseClassifierJson,
  normalizeClassifierResult,
  SYSTEM_PROMPT,
  CLASSIFIER_TIMEOUT_MS,
}
