// engine_tm.mbt
// =====================================================================
// P10: 从 engine.mbt 拆出 — 同包(@lib)多文件,调用点零修改。
// 主题: add_tm + fuzzy_match 家族 + concordance + load_tbx + enforce/check_terms (#22 TM/TB 一等公民)

// ---- #22 TM/TB 一等公民:翻译记忆检索 + 术语库强制对齐 ----
// 设计:TM 条目 mtype="tm"(text=源, translation=译文);TB 术语 mtype="term"(text=源术语, translation=目标术语)。
// 二者分离:fuzzy_match/concordance 只检索 tm;enforce_terms/check_terms 只扫 term。

// 加入一条翻译记忆(源 -> 译文),复用 tokenize/tf_vector 建索引
pub fn ProphecyEngine::add_tm(self : ProphecyEngine, src : String, tgt : String) -> String {
  let mid = self._new_id()
  // P0 性能:分词一次,vec / tm_toks / tm_toks_set 共用(原版 vec 与 tm_toks 各分词一次)
  let toks = yimai_tokenize(src)
  let vec = tf_vector(toks)
  let node : MemoryNode = {
    id: mid,
    text: src,
    mtype: "tm",
    vec,
    created: self.clock,
    last_used: self.clock,
    last_active: self.clock,
    use_count: 1,
    feedback: 0.0,
    edges: Map::from_iter(([] : Array[(String, Double)]).iter()),
    predictive_value: 0.0,
    hit_count: 0,
    predict_count: 0,
    is_term: false,
    translation: tgt,
    tm_toks: toks,
    tm_toks_set: to_set(toks),
    tm_ngrams: char_ngram_set(src, 2),
  }
  self.memories.set(mid, node)
  self.invalidate_pred_cache()
  self.fed_add = self.fed_add + 1
  self.mark_tm_idf_dirty()
  mid
}

// TM 模糊匹配:Top-K(按匹配率降序)。升级版评分(S1,对标 XTM Weighted Token Levenshtein 思路):
//   score = 0.55·idf_dice(IDF 加权共有词权重,罕见术语占优)
//         + 0.20·char_2gram_dice(字符 2-gram,捕捉形态变体,中文 bigram 与分词器一致)
//         + 0.15·token_set_dice(词序容忍:去重集合 Dice,重排场景不惩罚)
//         + 0.10·position(首锚 token 命中,句首对齐权重)
// 低于 threshold(建议阈值 0.70)视为无匹配;返回匹配率%与分项相似度。
// 旧公式保留为 fuzzy_match_legacy(0.7·token-cosine + 0.3·char-ratio)供 A/B 对照。
//
// P1 性能:先按 query 高频 token(IDF 排序 top-3)反查候选 TM(用 tm_postings),
// 仅在候选集上跑四分量;候选集为空(极冷启动:query 词都不在 TM 中)时退回全图。
// 候选集是「高 IDF 词命中」的超集——任何与 query 真匹配的 TM 至少与其中一个高频词有交集
// (否则无法构成 idf_dice 正值),所以 Top-K 完备性保留;不等性来自排序稳定性(同分顺序由 cands 顺序继承),
// 实际中无可见差异。
//
// P1 准确性:weights 可参数化(默认 0.55/0.20/0.15/0.10),配合 ardot 工作台做 A/B 调参。

// 计数 TM 节点(只用于 fuzzy_match 决定是否走剪枝,1K+ 才需要)
// P1 性能:折入 tm_idf 脏标记生命周期——rebuild_tm_idf 重建时顺带刷新缓存,此处 O(1) 读;
//   dirty 时 ensure 触发重建后读(最坏与原 O(N) 扫描同级,稳态零扫描)。
//   所有 TM 集变更点(add_tm/remember/consolidate-prune/restore/from_json)均置脏或全量重建,缓存不失真。
fn ProphecyEngine::count_tm(self : ProphecyEngine) -> Int {
  self.ensure_tm_idf()
  self.tm_count
}

// P4 重构:抽出 fuzzy_match / fuzzy_match_full 公共打分+pack 逻辑到私有 helper
//   - score_one_tm: 算单个 TM 的 sim_tok/sim_ng/sim_ts/sim_pos 加权和; 低于 threshold 返 None
//   - pack_fuzzy_top: 对已排序 cands 抽 Top-K 并 pack JSON(带 sim_tfidf/sim_char 重算)
//   两个 public fn (fuzzy_match / fuzzy_match_full) 只差「是否走 IDF 倒排剪枝」
//   核心打分+pack 共享,确保 R15 排序与 score 分量一致。

// 单 TM 打分:低于 threshold 返 None,否则返 (mid, score, sim_tok, sim_ng, sim_ts)
// P1:q2m/qtokm 为调用方预建的查询侧集合(每 query 一次),节点侧 tm_ngrams/tm_toks_set
//   已是 Map 缓存 → dice_mm 全程零 Map 分配。
fn ProphecyEngine::fuzzy_score_one(
  self : ProphecyEngine,
  mid : String,
  m : MemoryNode,
  qv : Map[String, Double],
  q2m : Map[String, Bool],
  qtokm : Map[String, Bool],
  anchor : String,
  weights : (Double, Double, Double, Double),
  threshold : Double,
) -> Option[(String, Double, Double, Double, Double)] {
  if m.mtype != "tm" { return None }
  let (w_tok, w_ng, w_ts, w_pos) = weights
  let sim_tok = idf_dice(qv, m.vec, self.tm_idf)
  let sim_ng = dice_mm(q2m, m.tm_ngrams)
  let sim_ts = dice_mm(qtokm, m.tm_toks_set)
  let sim_pos = if anchor != "" && term_hit(m.text, anchor) { 1.0 } else { 0.0 }
  let score = w_tok * sim_tok + w_ng * sim_ng + w_ts * sim_ts + w_pos * sim_pos
  if score >= threshold {
    Some((mid, score, sim_tok, sim_ng, sim_ts))
  } else {
    None
  }
}

// 对排序后的 cands 抽 Top-K 并 pack JSON(白盒四分量)
fn ProphecyEngine::fuzzy_pack_top(
  self : ProphecyEngine,
  cands : Array[(String, Double, Double, Double, Double)],
  k : Int,
  query : String,
  qv : Map[String, Double],
) -> Array[Json] {
  pack_topk_json(cands, k, fn(c) {
    let (mid, score, st, sng, sts) = c
    match self.memories.get(mid) {
      Some(m) =>
        Some(obj([
          ("id", str_json(mid)),
          ("source", str_json(m.text)),
          ("target", str_json(m.translation)),
          ("score", num_json(r4(score))),
          ("match_pct", num_json(r4(score * 100.0))),
          ("sim_token", num_json(r4(st))),
          ("sim_tfidf", num_json(r4(tfidf_cosine(qv, m.vec, self.tm_idf)))),
          ("sim_char", num_json(r4(char_ratio(query, m.text)))),
          ("sim_ngram", num_json(r4(sng))),
          ("sim_tokenset", num_json(r4(sts))),
        ]))
      None => None
    }
  })
}

pub fn ProphecyEngine::fuzzy_match(
  self : ProphecyEngine,
  query : String,
  k : Int,
  threshold : Double,
  weights? : (Double, Double, Double, Double) = (0.55, 0.20, 0.15, 0.10),
) -> Json {
  self.ensure_tm_idf()
  // P0 性能:query 只分词一次(原版 tf_vector / unique / first_anchor 各分词一次,共 3 次)
  // P1 性能:查询侧集合(q2m/qtokm)预建一次,打分循环内 dice_mm 零分配
  let qtoks = yimai_tokenize(query)
  let qv = tf_vector(qtoks)
  let q2m = char_ngram_set(query, 2)
  let qtokm = to_set(qtoks)
  let anchor = first_anchor(qtoks)
  let cands : Array[(String, Double, Double, Double, Double)] = []
  // P1 倒排剪枝策略:
  //   1) TM 总量 < PRUNE_MIN_TM:直接全图(IDF 在小语料上区分度低,top-K 容易把全图缩成一个 TM)
  //   2) 候选集为空(qtok 为空 / 无 token 在 postings 中):退回全图
  //   3) 正常:取 query 唯一 tokens 中 IDF top-3,反查 postings 并集得到候选集
  let tm_total = self.count_tm()
  let use_prune = tm_total >= PRUNE_MIN_TM
  let cand_set = if use_prune {
    fuzzy_candidate_set(self, qtokm, self.tm_idf, 3)
  } else {
    Map::from_iter(([] : Array[(String, Bool)]).iter())
  }
  let pool = if use_prune && cand_set.length() > 0 {
    Some(cand_set)
  } else {
    None
  }
  match pool {
    Some(set) =>
      for mid, _ in set.iter2() {
        match self.memories.get(mid) {
          Some(m) =>
            match self.fuzzy_score_one(mid, m, qv, q2m, qtokm, anchor, weights, threshold) {
              Some(c) => cands.push(c)
              None => ()
            }
          None => ()
        }
      }
    None =>
      for mid, m in self.memories.iter2() {
        match self.fuzzy_score_one(mid, m, qv, q2m, qtokm, anchor, weights, threshold) {
          Some(c) => cands.push(c)
          None => ()
        }
      }
  }
  sort_desc_by_key(cands, fn(c) { c.1 })
  arr_json(self.fuzzy_pack_top(cands, k, query, qv))
}

// P1:全图 fuzzy_match(不剪枝),作为倒排剪枝的「对照基线」,供 A/B 与回归测试用。
// P4 重构:改调公共 helper fuzzy_score_one + fuzzy_pack_top;与 fuzzy_match 共享打分+pack 逻辑,
// 唯一差异是不走 IDF 倒排剪枝路径。
pub fn ProphecyEngine::fuzzy_match_full(self : ProphecyEngine, query : String, k : Int, threshold : Double) -> Json {
  self.ensure_tm_idf()
  // P0 性能:query 只分词一次(原版 tf_vector / unique / first_anchor 各分词一次,共 3 次)
  // P1 性能:查询侧集合(q2m/qtokm)预建一次,打分循环内 dice_mm 零分配
  let qtoks = yimai_tokenize(query)
  let qv = tf_vector(qtoks)
  let q2m = char_ngram_set(query, 2)
  let qtokm = to_set(qtoks)
  let anchor = first_anchor(qtoks)
  let cands : Array[(String, Double, Double, Double, Double)] = []
  for mid, m in self.memories.iter2() {
    match self.fuzzy_score_one(mid, m, qv, q2m, qtokm, anchor, (0.55, 0.20, 0.15, 0.10), threshold) {
      Some(c) => cands.push(c)
      None => ()
    }
  }
  sort_desc_by_key(cands, fn(c) { c.1 })
  arr_json(self.fuzzy_pack_top(cands, k, query, qv))
}

// P1 候选集构造:取 query 唯一 token 按 IDF 降序的前 top_k 个,合并其 postings 得候选 mid 集合。
// 返回 Map[mid, true] 形式,便于 O(1) 查;去重由 map 自然完成。
// 退化路径:qtok 为空 / 所有 IDF 缺失 / postings 全空 → 返回空 Map(caller 检测后走全图)。
// P1:qtok 改传 Map 集合(Map 插入序 = 首现序,与原 unique 数组序一致,选择结果逐位等价)。
fn fuzzy_candidate_set(
  e : ProphecyEngine,
  qtok : Map[String, Bool],
  idf : Map[String, Double],
  top_k : Int,
) -> Map[String, Bool] {
  if qtok.length() == 0 {
    return Map::from_iter(([] : Array[(String, Bool)]).iter())
  }
  // 取 IDF top-k(部分选择法;k 远小于 |qtok|,O(n·k) 已足)
  let picked : Array[String] = []
  for t, _ in qtok.iter2() {
    // 仅考虑 postings 中存在的 token(无 postings 的 token 投出去也无效)
    if e.tm_postings.contains(t) {
      let id = match idf.get(t) {
        Some(x) => x
        None => 1.0
      }
      // 选择:插入 picked 使其按 idf 降序
      let mut inserted = false
      let mut i = 0
      while i < picked.length() {
        let pid = match idf.get(picked[i]) {
          Some(x) => x
          None => 1.0
        }
        if id > pid {
          // 在 i 位置插入
          let mut j = picked.length()
          picked.push("")
          while j > i {
            picked[j] = picked[j - 1]
            j = j - 1
          }
          picked[i] = t
          inserted = true
          break
        }
        i = i + 1
      }
      if !inserted && picked.length() < top_k {
        picked.push(t)
      }
      // 维护 picked 长度 ≤ top_k
      while picked.length() > top_k {
        let _ = picked.pop()
      }
    }
  }
  // 合并 postings
  let cand : Map[String, Bool] = Map::from_iter(([] : Array[(String, Bool)]).iter())
  for t in picked {
    match e.tm_postings.get(t) {
      Some(mids) =>
        for mid in mids {
          cand.set(mid, true)
        }
      None => ()
    }
  }
  cand
}

// 旧版模糊匹配(S1 之前的公式):0.7·token-cosine + 0.3·char-ratio。
//
// ⚠️ **A/B 对照基线,长期保留不删除**(2026-08 决策):
//   - 新代码请用 `fuzzy_match`(P1 增量:S1 公式 + IDF 倒排剪枝)
//   - 本函数保留理由:
//     1) fuzzy_match_full(无剪枝)和 fuzzy_match_legacy(无 IDF 但有剪枝-前一版公式)的回归基线
//     2) 历史 TM/TB 测试集 (R1-R25 部分) 仍引用它作 expected score 对照
//     3) 删除会破坏 4 个引用点(engine.mbt + tests/core/{tm,api_coverage} + README)的平滑迁移
//   - 移除窗口:仅在 fuzzy_match / fuzzy_match_full 完全替代且引用点全部迁移后(≥0.3.0)讨论
pub fn ProphecyEngine::fuzzy_match_legacy(self : ProphecyEngine, query : String, k : Int, threshold : Double) -> Json {
  let qv = tf_vector(yimai_tokenize(query))
  let cands : Array[(String, Double, Double, Double)] = []
  for mid, m in self.memories.iter2() {
    if m.mtype != "tm" {
      continue
    }
    let sim_tok = cosine(qv, m.vec)
    let sim_ch = char_ratio(query, m.text)
    let score = 0.7 * sim_tok + 0.3 * sim_ch
    if score >= threshold {
      cands.push((mid, score, sim_tok, sim_ch))
    }
  }
  sort_desc_by_key(cands, fn(c){ c.1 })
  let out = pack_topk_json(cands, k, fn(c) {
    let (mid, score, st, sc) = c
    match self.memories.get(mid) {
      Some(m) =>
        Some(obj([
          ("id", str_json(mid)),
          ("source", str_json(m.text)),
          ("target", str_json(m.translation)),
          ("score", num_json(r4(score))),
          ("match_pct", num_json(r4(score * 100.0))),
          ("sim_token", num_json(r4(st))),
          ("sim_char", num_json(r4(sc))),
        ]))
      None => None
    }
  })
  arr_json(out)
}

// Concordance 检索:返回含查询词的所有 TM 句段(按词/串出现次数打分)。
// 注意:concordance 的命中度是"词出现度",与 fuzzy_match 的相似度算法不同(见调研结论)。
pub fn ProphecyEngine::concordance(self : ProphecyEngine, term : String, k : Int) -> Json {
  let q = yimai_tokenize(term)
  let qkey = if q.length() > 0 { q[0] } else { term }
  let cands : Array[(String, Int)] = []
  for mid, m in self.memories.iter2() {
    if m.mtype != "tm" { continue }
    let toks = m.tm_toks
    let mut hits = 0
    for t in toks {
      if t == qkey { hits = hits + 1 }
    }
    if hits == 0 && str_contains(m.text, term) { hits = 1 }
    if hits > 0 { cands.push((mid, hits)) }
  }
  sort_desc_by_key(cands, fn(c){ c.1.to_double() })
  let sorted = cands
  let lim = if k > sorted.length() { sorted.length() } else { k }
  let out : Array[Json] = []
  let mut i = 0
  while i < lim {
    let (mid, hits) = sorted[i]
    match self.memories.get(mid) {
      Some(m) =>
        out.push(obj([
          ("id", str_json(mid)),
          ("source", str_json(m.text)),
          ("target", str_json(m.translation)),
          ("hits", num_json(hits.to_double())),
        ]))
      None => ()
    }
    i = i + 1
  }
  arr_json(out)
}

// 解析 TBX(ISO 30042) 术语库,建立术语节点(text=源术语, translation=目标术语)。
// 支持 TBX 2.0  与简化 。
// 语言感知:按各 langSet 的 xml:lang 取值(默认源=en-US、目标=zh-CN);
// 无 xml:lang 时按文档顺序兜底(首 langSet 为源,次为译文)。返回加载的概念条目数。
pub fn ProphecyEngine::load_tbx(
  self : ProphecyEngine,
  xml : String,
  src_lang~ : String = "en-US",
  tgt_lang~ : String = "zh-CN"
) -> Int {
  let mut count = 0
  let entries = split_on(xml, " Json {
  self.ensure_term_index()
  let cand = self.term_candidates(text)
  let out : Array[Json] = []
  for mid, m in self.memories.iter2() {
    if !m.is_term { continue }
    if !cand.contains(mid) { continue }
    if term_hit(text, m.text) {
      out.push(obj([
        ("term", str_json(m.text)),
        ("translation", str_json(m.translation)),
        ("mid", str_json(mid)),
      ]))
    }
  }
  arr_json(out)
}

// 术语一致性校验:源文识别到的术语,其译文是否出现在译文中;返回违规项。
// 源/译文均用 term_hit 做边界判定,保证拉丁文术语不会因子串包含而误判。
pub fn ProphecyEngine::check_terms(self : ProphecyEngine, source : String, target : String) -> Json {
  self.ensure_term_index()
  let cand = self.term_candidates(source)
  let violations : Array[Json] = []
  for mid, m in self.memories.iter2() {
    if !m.is_term { continue }
    if !cand.contains(mid) { continue }
    if term_hit(source, m.text) && m.translation != "" {
      if !term_hit(target, m.translation) {
        violations.push(obj([
          ("term", str_json(m.text)),
          ("expected", str_json(m.translation)),
          ("mid", str_json(mid)),
        ]))
      }
    }
  }
  arr_json(violations)
}