// engine_index.mbt
// =====================================================================
// P10: 从 engine.mbt 拆出 — 同包(@lib)多文件,调用点零修改。
// 主题: TM 文档频率/IDF/term 倒排索引(fuzzy 倒排剪枝与 enforce/check 加速依赖)

// ---- S1 检索强化:TM IDF 表(确定性重建,逻辑时钟无关)----

// 全量重建 TM 文档频率 / IDF / 倒排(幂等;add_tm/consolidate-prune/restore/from_json 后触发)。
// P1:同一遍 O(N·|Q|) 扫描顺手把 token -> mids 倒排也建好,给 fuzzy_match 候选剪枝用,
// 避免二次扫描(成本约为 N·|Q| token 遍历,本就绕不开)。
// P1:复用节点缓存的 m.tm_toks(调用前须保证特征缓存新鲜——from_json 先 features 后 idf,
//   其余写路径均在置脏前已同步 tm_toks);顺带刷新 tm_count 供 count_tm O(1) 读。
fn ProphecyEngine::rebuild_tm_idf(self : ProphecyEngine) -> Unit {
  let df : Map[String, Int] = Map::from_iter(([] : Array[(String, Int)]).iter())
  let postings : Map[String, Array[String]] =
    Map::from_iter(([] : Array[(String, Array[String])]).iter())
  let mut n = 0
  for mid, m in self.memories.iter2() {
    if m.mtype != "tm" {
      continue
    }
    n = n + 1
    let seen : Map[String, Bool] = Map::from_iter(([] : Array[(String, Bool)]).iter())
    for t in m.tm_toks {
      if seen.contains(t) {
        continue
      }
      seen.set(t, true)
      let v = match df.get(t) {
        Some(x) => x
        None => 0
      }
      df.set(t, v + 1)
      // P1 倒排:mid 去重后入 postings(同一 TM 含重复 token 不重复推 mid)
      match postings.get(t) {
        Some(arr) =>
          if !arr_contains(arr, mid) {
            arr.push(mid)
          }
        None => {
          let a : Array[String] = [mid]
          postings.set(t, a)
        }
      }
    }
  }
  self.tm_df = df
  let idf : Map[String, Double] = Map::from_iter(([] : Array[(String, Double)]).iter())
  for t, f in self.tm_df.iter2() {
    // idf = ln((N+1)/(df+1)) + 1:常见词(df 高)权重低,罕见术语(df 低)权重高;N=0 时 idf=1
    idf.set(t, @math.ln((n + 1).to_double() / (f + 1).to_double()) + 1.0)
  }
  self.tm_idf = idf
  self.tm_postings = postings
  self.tm_count = n
  self.tm_idf_dirty = false
}

fn ProphecyEngine::mark_tm_idf_dirty(self : ProphecyEngine) -> Unit {
  self.tm_idf_dirty = true
}

// 惰性重算:fuzzy_match 开头调用,仅当 dirty 时全量重建(保证每次 fuzzy_match 的 IDF/postings 表新鲜)
fn ProphecyEngine::ensure_tm_idf(self : ProphecyEngine) -> Unit {
  if self.tm_idf_dirty {
    self.rebuild_tm_idf()
  }
}

// 全量重建 TM 特征缓存(幂等;from_json 后调用,使 tm_toks/tm_ngrams 与重建的 IDF 表对齐)
fn ProphecyEngine::rebuild_tm_features(self : ProphecyEngine) -> Unit {
  for mid, m in self.memories.iter2() {
    if m.mtype != "tm" {
      continue
    }
    let m2 = m
    m2.tm_toks = yimai_tokenize(m.text)
    m2.tm_toks_set = to_set(m2.tm_toks)
    m2.tm_ngrams = char_ngram_set(m.text, 2)
    self.memories.set(mid, m2)
  }
}

// ---- P4-2 术语首字符倒排(内部态,不进 to_json;惰性重建,幂等)----

fn ProphecyEngine::mark_term_index_dirty(self : ProphecyEngine) -> Unit {
  self.term_idx_dirty = true
}

fn ProphecyEngine::ensure_term_index(self : ProphecyEngine) -> Unit {
  if self.term_idx_dirty {
    self.rebuild_term_index()
  }
}

fn ProphecyEngine::rebuild_term_index(self : ProphecyEngine) -> Unit {
  let idx : Map[String, Array[String]] = Map::from_iter(([] : Array[(String, Array[String])]).iter())
  for mid, m in self.memories.iter2() {
    if !m.is_term || m.text.length() == 0 {
      continue
    }
    // 与 term_candidates 的 text.iter() 同路径取首字符(统一按 Unicode 标量,避免切片语义差异)
    let c = match m.text.iter().next() {
      Some(ch) => ch.to_string()
      None => continue
    }
    match idx.get(c) {
      Some(arr) => arr.push(mid)
      None => {
        let a : Array[String] = [mid]
        idx.set(c, a)
      }
    }
  }
  self.term_idx = idx
  self.term_idx_dirty = false
}

// 收集 text 首字符能命中的候选 term mid 集合(按首字符索引粗筛)。
// 完备性:term_hit(text,term)=true ⟹ term[0] 必作为字符出现在 text 中(CJK 子串 / 拉丁边界均成立),
// 故候选集合是超集;终判仍用 term_hit 精确复验,保证输出与全量扫描逐项等价。
fn ProphecyEngine::term_candidates(self : ProphecyEngine, text : String) -> Map[String, Bool] {
  let cand : Map[String, Bool] = Map::from_iter(([] : Array[(String, Bool)]).iter())
  let seen : Map[String, Bool] = Map::from_iter(([] : Array[(String, Bool)]).iter())
  for ch in text.iter() {
    let c = ch.to_string()
    if seen.contains(c) {
      continue
    }
    seen.set(c, true)
    match self.term_idx.get(c) {
      Some(mids) =>
        for mid in mids {
          cand.set(mid, true)
        }
      None => ()
    }
  }
  cand
}

// IDF 加权集合 Dice = 2·Σ_{t∈A∩B} idf(t) / (Σ_{t∈A} idf(t) + Σ_{t∈B} idf(t))。
// fuzzy_match 主评分项:关注共有词的权重(罕见术语权重高),对 query 独有词的
// 范数惩罚比 TF-IDF 余弦更宽容,短句/中文 bigram 场景更稳。
fn idf_dice(
  qvec : Map[String, Double],
  mvec : Map[String, Double],
  idf : Map[String, Double],
) -> Double {
  if qvec.length() == 0 || mvec.length() == 0 {
    return 0.0
  }
  let mut inter_w = 0.0
  let mut qw = 0.0
  let mut mw = 0.0
  for k, _ in qvec.iter2() {
    let id = match idf.get(k) {
      Some(x) => x
      None => 1.0
    }
    qw = qw + id
    if mvec.contains(k) {
      inter_w = inter_w + id
    }
  }
  for k, _ in mvec.iter2() {
    let id = match idf.get(k) {
      Some(x) => x
      None => 1.0
    }
    mw = mw + id
  }
  if qw == 0.0 || mw == 0.0 {
    return 0.0
  }
  2.0 * inter_w / (qw + mw)
}

// TF-IDF 加权余弦:qvec/mvec 存纯 TF,计算时实时乘 IDF(零依赖,不改变节点向量存储)。
// 作为 fuzzy_match 输出的信息性分项(sim_tfidf),供调用方对比。
fn tfidf_cosine(
  qvec : Map[String, Double],
  mvec : Map[String, Double],
  idf : Map[String, Double],
) -> Double {
  if qvec.length() == 0 || mvec.length() == 0 {
    return 0.0
  }
  let mut num = 0.0
  for k, qv in qvec.iter2() {
    let id = match idf.get(k) {
      Some(x) => x
      None => 1.0
    }
    match mvec.get(k) {
      Some(mv) => num = num + (qv * id) * (mv * id)
      None => ()
    }
  }
  if num == 0.0 {
    return 0.0
  }
  let mut na2 = 0.0
  for k, v in qvec.iter2() {
    let id = match idf.get(k) {
      Some(x) => x
      None => 1.0
    }
    na2 = na2 + (v * id) * (v * id)
  }
  let mut nb2 = 0.0
  for k, v in mvec.iter2() {
    let id = match idf.get(k) {
      Some(x) => x
      None => 1.0
    }
    nb2 = nb2 + (v * id) * (v * id)
  }
  let na = na2.sqrt()
  let nb = nb2.sqrt()
  if na == 0.0 || nb == 0.0 {
    return 0.0
  }
  num / (na * nb)
}