/// evolve.mbt —— DGM 档案库核心(域无关实现)
///
/// 移植自 dna_forge/archive.py(原 Python,见评估文档),用 MoonBit 重写核心机制,
/// 去掉 DNA 领域绑定(SEED/TASK_BANK/编码层),只保留通用的档案库算法:
///
///   - Artifact:档案库中的一个产物(= 语言/代码/设计的一个版本)
///   - Archive:保留【所有】通过门槛的产物(含低分,故意不淘汰)+ 谱系
///   - sample_parent:加权采样(性能加权 × 子代越少越该探索,等价 p ∝ s·h)
///   - is_duplicate:粗粒度查重(防 agent 无限重做同一事)
///   - novelty:相对档案库的新颖度(防收敛到单一路线)
///   - lineage:谱系回溯(垫脚石链,最优 agent 常经过性能下降的中间节点)
///
/// 评分采用注入式函数(域无关,可对接 Omega gate / run_check),绝不让 LLM 自评。

///|
/// 档案库中的一个产物。
pub struct Artifact {
  id : String
  parent_id : String?
  goal : String // 这一轮想解决的问题
  note : String // 设计意图(检索 key / novelty 用)
  code : String // 实现
  score : Double // 可计算评分 [0,1]
  parts : Map[String, Double] // 评分分项明细(域选择)
  children : Int // 已产生的可用子代数(采样新颖性)
  created_at : String // 时间戳
}

///|
/// 档案库(进程内 Map 存储,持久化由调用方决定)。
pub struct Archive {
  items : Map[String, Artifact]
  path : String
}

///|
/// 新建档案库。
pub fn Archive::new(path : String) -> Archive {
  { items: Map([]), path, }
}

///|
/// 产物总数。
pub fn Archive::len(self : Archive) -> Int {
  self.items.size()
}

///|
/// 是否为空档案库。
pub fn Archive::is_empty(self : Archive) -> Bool {
  self.items.is_empty()
}

///|
/// 按 id 取产物。
pub fn Archive::get(self : Archive, id : String) -> Artifact? {
  self.items.get(id)
}

///|
/// 全部产物(值数组)。
pub fn Archive::all(self : Archive) -> Array[Artifact] {
  let out : Array[Artifact] = []
  for (_, a) in self.items {
    out.push(a)
  }
  out
}

///|
/// 添加产物。若 parent 已在库中,累加其 children 计数。
/// 重复添加同一 id(覆盖已有记录)时不再重复累加父节点 children,避免计数虚高。
pub fn Archive::add(self : Archive, a : Artifact) -> Unit {
  // 先查重:若 id 已存在则视为更新既有记录,不重复增加父节点子代数
  let is_new = self.items.get(a.id) is None
  if is_new {
    match a.parent_id {
      Some(pid) =>
        match self.items.get(pid) {
          Some(p) => {
            let bumped = Artifact::with_children(p, p.children + 1)
            self.items.set(pid, bumped)
          }
          None => ()
        }
      None => ()
    }
  }
  self.items.set(a.id, a)
}

///|
/// 返回 children 字段更新后的副本(MoonBit struct 不可变,用于累加子代数)。
pub fn Artifact::with_children(a : Artifact, n : Int) -> Artifact {
  {
    id: a.id,
    parent_id: a.parent_id,
    goal: a.goal,
    note: a.note,
    code: a.code,
    score: a.score,
    parts: a.parts,
    children: n,
    created_at: a.created_at,
  }
}

///|
/// 构造一个产物(供外部包创建后加入档案库)。
pub fn Artifact::create(
  id~ : String,
  parent_id~ : String?,
  goal~ : String,
  note~ : String,
  code~ : String,
  score~ : Double,
  parts~ : Map[String, Double],
  created_at~ : String,
) -> Artifact {
  { id, parent_id, goal, note, code, score, parts, children: 0, created_at, }
}

// ---------------------------------------------------------------------------
// 采样(等价 DGM p ∝ s_i · h_i)
//   性能权重 s(父代分数,越高越可能被选作父代,但非垄断)
//   新颖性权重 h = 1/(1+children)(子代越少越该被探索)
// 所有产物保留非零权重 —— 任何路径都不会被永久封死。
// ---------------------------------------------------------------------------

///|
/// 加权滚轮采样:p_i ∝ s_i · h_i,返回选中产物(空库 None)。
/// rand 注入 `() -> Double`([0,1) 均匀),保证可测试。
pub fn Archive::sample_parent(
  self : Archive,
  lam? : Double = 8.0,
  a0? : Double = 0.4,
  rand~ : () -> Double,
) -> Artifact? {
  let cands = self.all()
  if cands.is_empty() {
    return None
  }
  let weights : Array[Double] = []
  for a in cands {
    // 性能分量:用 saturating 映射 `score / (score + a0)` -> [0,1),单调且无需 exp。
    // score 越大 -> 权重越高;score=0 时权重为 0(该产物仅靠 novelty 分量也不会为零,
    // 但为保留“零分也有非零概率”,下限 0.01)。
    let s_raw = if a.score > 0.0 { a.score / (a.score + a0) } else { 0.0 }
    let s = s_raw.max(0.001)
    let h = 1.0 / (1.0 + a.children.to_double())
    weights.push(s * h)
  }
  let total = weights.fold_left(init=0.0, fn(acc, w) { acc + w })
  if total <= 0.0 {
    return Some(cands[0])
  }
  // 轮盘选择:r 在 [0,total);r -= 权重,命中即返回。
  // 用 r < 0(不含等号)保证 rand 返回 0(贴在左端)时不会错误命中第一个元素之前。
  let mut r = rand() * total
  for i = 0; i < cands.length(); i = i + 1 {
    r = r - weights[i]
    if r < 0.0 {
      return Some(cands[i])
    }
  }
  Some(cands[cands.length() - 1])
}

///|
/// 最高分产物(空库 None)。
pub fn Archive::best(self : Archive) -> Artifact? {
  if self.is_empty() {
    return None
  }
  let mut b = self.all()[0]
  for a in self.all() {
    if a.score > b.score {
      b = a
    }
  }
  Some(b)
}

// ---------------------------------------------------------------------------
// 查重与新颖性(粗粒度 token 重合,Voyager 必修课)
// ---------------------------------------------------------------------------

///|
/// 切词单真源(BUG-25 标定后重写):ASCII 词 + CJK 单字 + CJK 相邻二字组,去重。
///
/// 旧口径是"可见字符集"——对中文等价于逐字拆散,`熔断器三态` 与 `三态熔断器` 同分,
/// 而真正的病灶是**改写型子任务**:子任务合法地换一套词("依赖图恒空"→"增量编译链修复"),
/// 字符集重合因此趋 0,绝对阈值下 15/15 全判漂移(实测见 scripts/calibrate_goal_drift.py)。
/// 新口径加 ASCII 词与 CJK 二字组,让"同词/同词组"才计分:
/// 全量父子对(1222 条)假阳性 16.9%→1.9%,随机错配对照仍 81.3% 判可疑。
/// 口径必须与 scripts/calibrate_goal_drift.py 逐字对齐——数字不是抄来的,是那条命令跑出来的。
pub fn tokens(s : String) -> Array[String] {
  let seen : Map[String, Bool] = Map([])
  let out : Array[String] = []
  let chars = s.to_lower().to_array()
  let n = chars.length()
  let mut i = 0
  while i < n {
    let c = chars[i]
    if is_cjk(c) {
      // CJK 段:先扫到段边界,再发单字与相邻二字组("熔断器"→ 熔/断/器/熔断/断器)
      let mut j = i
      while j < n && is_cjk(chars[j]) {
        j = j + 1
      }
      let mut k = i
      while k < j {
        tok_add(seen, out, chars[k].to_string())
        if k + 1 < j {
          tok_add(seen, out, chars[k].to_string() + chars[k + 1].to_string())
        }
        k = k + 1
      }
      i = j
    } else if is_word_char(c) {
      let mut j = i
      let mut w = ""
      while j < n && is_word_char(chars[j]) {
        w = w + chars[j].to_string()
        j = j + 1
      }
      tok_add(seen, out, w)
      i = j
    } else {
      i = i + 1
    }
  }
  out
}

///|
/// 去重后入表(Map/Array 是可变值类型,无需 mut 绑定;见 cb_json 同形写法)。
fn tok_add(seen : Map[String, Bool], out : Array[String], k : String) -> Unit {
  if k != "" && seen.get(k).is_none() {
    seen.set(k, true)
    out.push(k)
  }
}

///|
/// CJK 统一表意文字(含扩展 A)——"按字成词"的前提只对这些字符成立。
fn is_cjk(c : Char) -> Bool {
  let cp = c.to_int()
  (cp >= 0x4e00 && cp <= 0x9fff) || (cp >= 0x3400 && cp <= 0x4dbf)
}

///|
/// ASCII 词字符:字母/数字/下划线/连字符(与标定脚本的 `[a-z0-9_-]+` 同集)。
fn is_word_char(c : Char) -> Bool {
  let cp = c.to_int()
  let alpha = cp >= 'a'.to_int() && cp <= 'z'.to_int()
  let digit = cp >= '0'.to_int() && cp <= '9'.to_int()
  alpha || digit || cp == '_'.to_int() || cp == '-'.to_int()
}

///|
/// 可见字符去重集(旧 `tokens` 口径,保留给**兄弟冗余**用)。
/// 兄弟重复的典型形状是"词序换位"(`数据采集模块` ↔ `采集数据模块`),
/// 二字组口径会把它们放行(jaccard 0.556),字符集恰好对词序不敏感(1.0);
/// 实测这个分量在真实非模板家族上不额外增加误报(两种口径同为 34.8%,见标定脚本)。
pub fn unigrams(s : String) -> Array[String] {
  let seen : Map[String, Bool] = Map([])
  let out : Array[String] = []
  for c in s.to_lower() {
    let ch = c.to_string()
    if ch.trim() != "" && seen.get(ch).is_none() {
      seen.set(ch, true)
      out.push(ch)
    }
  }
  out
}

///|
/// 覆盖率 |a∩b| / |a|——**非对称**,a 是基准(根目标),b 是待测文本。
/// Jaccard 用并集做分母,长文本天然被稀释:子任务多写 200 字模板就"更像漂移",
/// 那是长度惩罚不是目标偏离。coverage 只问"根目标里的词还在不在",多写不罚。
pub fn coverage(a : Array[String], b : Array[String]) -> Double {
  if a.is_empty() {
    return 0.0
  }
  let in_b : Map[String, Bool] = Map([])
  for w in b {
    in_b.set(w, true)
  }
  let mut inter = 0
  // a 已由 tokens 去重,这里不再二次去重(重复元素会把分母外的分子抬高)
  for w in a {
    if in_b.contains(w) {
      inter = inter + 1
    }
  }
  inter.to_double() / a.length().to_double()
}

///|
/// 两个 token 数组的 Jaccard 相似度 |a∩b| / |a∪b|。
pub fn jaccard(a : Array[String], b : Array[String]) -> Double {
  if a.is_empty() && b.is_empty() {
    return 0.0
  }
  let in_a : Map[String, Bool] = Map([])
  for w in a {
    in_a.set(w, true)
  }
  let in_b : Map[String, Bool] = Map([])
  for w in b {
    in_b.set(w, true)
  }
  let mut inter = 0
  for (w, _) in in_a {
    if in_b.contains(w) {
      inter = inter + 1
    }
  }
  let union = in_a.size() + in_b.size() - inter
  if union == 0 {
    0.0
  } else {
    inter.to_double() / union.to_double()
  }
}

///|
/// 查重:某个 goal 是否与库中已有产物高度相似(Jaccard >= threshold)。
pub fn Archive::is_duplicate(
  self : Archive,
  goal : String,
  threshold? : Double = 0.88,
) -> Bool {
  let g = tokens(goal)
  if g.is_empty() {
    return false
  }
  for (_, a) in self.items {
    let t = tokens(a.goal)
    if not(t.is_empty()) {
      if jaccard(g, t) >= threshold {
        return true
      }
    }
  }
  false
}

///|
/// 新颖性:1 - 与库中 note 的最大 Jaccard 重合。值越高越没见过。
pub fn Archive::novelty(self : Archive, note : String) -> Double {
  let n = tokens(note)
  if n.is_empty() || self.is_empty() {
    return 1.0
  }
  let mut max_sim = 0.0
  for (_, a) in self.items {
    let t = tokens(a.note)
    if not(t.is_empty()) {
      let j = jaccard(n, t)
      if j > max_sim {
        max_sim = j
      }
    }
  }
  (1.0 - max_sim).clamp(min=0.0, max=1.0)
}

// ---------------------------------------------------------------------------
// 摘要 / 低分地图 / 谱系
// ---------------------------------------------------------------------------

///|
/// 摘要(高分优先):给课程生成器看的 top-N。
pub fn Archive::summaries(self : Archive, limit? : Int = 12) -> Array[String] {
  let sorted = self.all()
  sorted.sort_by(fn(a, b) {
    if a.score != b.score {
      if a.score > b.score {
        -1
      } else {
        1
      }
    } else {
      b.created_at.compare(a.created_at)
    }
  })
  let n = if sorted.length() < limit { sorted.length() } else { limit }
  let out : Array[String] = []
  for i = 0; i < n; i = i + 1 {
    out.push(evolve_summary_line(sorted[i]))
  }
  out
}

///|
/// 低分产物(“此路不通”地图):喂回给课程生成器绕开。
pub fn Archive::dead_ends(self : Archive, limit? : Int = 8) -> Array[String] {
  let sorted = self.all()
  sorted.sort_by(fn(a, b) {
    if a.score != b.score {
      if a.score < b.score {
        -1
      } else {
        1
      }
    } else {
      0
    }
  })
  let n = if sorted.length() < limit { sorted.length() } else { limit }
  let out : Array[String] = []
  for i = 0; i < n; i = i + 1 {
    out.push(evolve_summary_line(sorted[i]))
  }
  out
}

///|
/// 单行摘要(id 前 8 位 + score + goal)。
fn evolve_summary_line(a : Artifact) -> String {
  let short = if a.id.length() > 8 {
    a.id.substring(start=0, end=8)
  } else {
    a.id
  }
  let sc = (a.score * 1000.0).to_int().to_double() / 1000.0
  "[\{short} score=\{sc} ] \{a.goal}"
}

///|
/// 从某产物沿 parent 链回溯谱系(最多 depth 层),返回 (id, score, goal) 数组。
pub fn Archive::lineage(
  self : Archive,
  from : String,
  depth? : Int = 12,
) -> Array[(String, Double, String)] {
  let out : Array[(String, Double, String)] = []
  let mut cur : String? = Some(from)
  let mut d = 0
  while d < depth {
    match cur {
      None => break
      Some(id) =>
        match self.get(id) {
          Some(a) => {
            out.push((a.id, a.score, a.goal))
            cur = a.parent_id
            d = d + 1
          }
          None => break
        }
    }
  }
  out
}

///|
/// 产出 JSON(供 MCP / 展示)。
pub fn Artifact::to_json(self : Artifact) -> Json {
  let m : Map[String, Json] = Map([])
  m.set("id", Json::string(self.id))
  m.set(
    "parent_id",
    match self.parent_id {
      Some(p) => Json::string(p)
      None => Json::null()
    },
  )
  m.set("goal", Json::string(self.goal))
  m.set("note", Json::string(self.note))
  m.set("code", Json::string(self.code))
  m.set("score", Json::number(self.score))
  m.set("children", Json::number(self.children.to_double()))
  m.set("created_at", Json::string(self.created_at))
  let pm : Map[String, Json] = Map([])
  for (k, v) in self.parts {
    pm.set(k, Json::number(v))
  }
  m.set("parts", Json::object(pm))
  Json::object(m)
}

///|
/// 从 JSON 反序列化 Artifact。
pub fn Artifact::from_json(j : Json) -> Artifact {
  let obj = match j {
    Object(m) => m
    _ => Map([])
  }
  let str = fn(k : String) -> String {
    match obj.get(k) {
      Some(String(s)) => s
      _ => ""
    }
  }
  let score = match obj.get("score") {
    Some(Number(n, ..)) => n
    _ => 0.0
  }
  let parent = match obj.get("parent_id") {
    Some(String(p)) => if p == "" { None } else { Some(p) }
    Some(Null) => None
    _ => None
  }
  // 反序列化 children(采样新颖性权重),缺失回退 0
  let children = match obj.get("children") {
    Some(Number(n, ..)) => n.to_int()
    _ => 0
  }
  // 反序列化 parts(评分分项),缺失回退空 Map
  let parts : Map[String, Double] = Map([])
  match obj.get("parts") {
    Some(Object(pm)) =>
      for (k, v) in pm.iter2() {
        match v {
          Number(n, ..) => parts.set(k, n)
          _ => ()
        }
      }
    _ => ()
  }
  {
    id: str("id"),
    parent_id: parent,
    goal: str("goal"),
    note: str("note"),
    code: str("code"),
    score,
    parts,
    children,
    created_at: str("created_at"),
  }
}