/**
 * 客户端检索（轻量 RAG 的 Retrieve 部分）
 *
 * 不依赖任何外部服务，纯前端在浏览器里跑：
 * 1) 用一个简单的中文/英文分词器把 chunk 和用户问题切成“词项”；
 * 2) 用类 BM25 的打分公式对 knowledge.json 中每个段落打分；
 * 3) 返回与问题最相关的 top-k 段落，作为送给大模型的“上下文”。
 *
 * 这样做的目的：只把“相关文档片段”喂给大模型，既省钱、又让模型回答严格
 * 基于帮助中心内容；检索不到任何相关内容时，直接判定为“无关”，无需调用模型。
 */
import knowledge from './knowledge.json';

/** 简单分词：中文按字 + 英文/数字按词 */
export function tokenize(text) {
  if (!text) return [];
  const lower = String(text).toLowerCase();
  const tokens = [];
  // 英文/数字词
  const wordRe = /[a-z0-9_]+/g;
  let m;
  while ((m = wordRe.exec(lower)) !== null) {
    if (m[0].length >= 2) tokens.push(m[0]);
  }
  // 中文字（单字，做朴素中文检索）
  const cjkRe = /[一-龥]/g;
  while ((m = cjkRe.exec(lower)) !== null) {
    tokens.push(m[0]);
  }
  return tokens;
}

/** 停用词（去掉太常见的单字，降低噪声） */
const STOP = new Set([
  '的', '了', '是', '在', '和', '与', '或', '也', '都', '就', '你', '我', '他',
  '她', '它', '这', '那', '有', '为', '及', '以', '上', '下', '不', '个', '中',
  'the', 'to', 'of', 'and', 'a', 'in', 'is', 'it', 'for', 'on', 'how', 'what',
]);

/** 构建索引：为每个 chunk 预计算 token 频次，并统计全局 df 与平均长度 */
let INDEX = null;
function buildIndex() {
  const docs = knowledge.map((chunk, i) => {
    // 标题/小节加权：把标题重复拼进被检索文本，让标题命中得分更高
    const hay = `${chunk.title} ${chunk.section} ${chunk.title} ${chunk.section} ${chunk.text}`;
    const toks = tokenize(hay).filter((t) => !STOP.has(t));
    const tf = new Map();
    for (const t of toks) tf.set(t, (tf.get(t) || 0) + 1);
    return { id: i, len: toks.length || 1, tf };
  });
  const N = docs.length;
  const avg = docs.reduce((s, d) => s + d.len, 0) / (N || 1);
  const df = new Map();
  for (const d of docs) {
    for (const t of d.tf.keys()) df.set(t, (df.get(t) || 0) + 1);
  }
  INDEX = { docs, N, avg, df };
}

/**
 * BM25 打分检索
 * @param {string} query 用户问题
 * @param {number} k 返回条数，默认 4
 * @returns {{chunks: any[], maxScore: number, hasHit: boolean}}
 */
export function retrieve(query, k = 4) {
  if (!INDEX) buildIndex();
  const qTerms = Array.from(new Set(tokenize(query).filter((t) => !STOP.has(t))));
  if (qTerms.length === 0) return { chunks: [], maxScore: 0, hasHit: false };

  const { docs, N, avg, df } = INDEX;
  const k1 = 1.5;
  const b = 0.75;
  const scored = [];
  for (const d of docs) {
    let score = 0;
    for (const t of qTerms) {
      const f = d.tf.get(t);
      if (!f) continue;
      const n = df.get(t) || 1;
      const idf = Math.log(1 + (N - n + 0.5) / (n + 0.5));
      const denom = f + k1 * (1 - b + (b * d.len) / avg);
      score += idf * ((f * (k1 + 1)) / denom);
    }
    if (score > 0) scored.push({ id: d.id, score });
  }

  if (scored.length === 0) return { chunks: [], maxScore: 0, hasHit: false };
  scored.sort((a, c) => c.score - a.score);
  const top = scored.slice(0, k);
  const maxScore = top[0].score;
  const chunks = top.map((s) => knowledge[s.id]);
  return { chunks, maxScore, hasHit: true };
}
