// MemoryStore::add。仍是同一个聚合根上的方法。

// ---------------------------------------------------------------------------
// add:编排薄壳,阶段助手仍挂在同一个聚合根上
// ---------------------------------------------------------------------------

///|
/// 提取阶段结果:facts + Err 路径降级标记(notes 由助手写入)。
priv struct AddExtractOutcome {
  facts : Array[ExtractedFact]
  extraction_failed : Bool
  degraded : Bool
}

///|
/// 提取(失败降级:原文 unstructured;连续 MAX_EXTRACTION_FAILURES 次报错)。
fn MemoryStore::extract_for_add(
  self : MemoryStore,
  user_id : String,
  text : String,
  notes : Array[String],
) -> Result[AddExtractOutcome, MoomemError] {
  match self.extractor.extract(user_id, text) {
    Ok(fs) =>
      Ok(AddExtractOutcome::{
        facts: fs,
        extraction_failed: false,
        degraded: false,
      })
    Err(e) => {
      self.extraction_failures = self.extraction_failures + 1
      let failures = self.extraction_failures
      let err_text = e.message()
      if failures >= MAX_EXTRACTION_FAILURES {
        return Err(
          MoomemError::ExtractionFailure(
            "extractor failed \{failures} times in a row; last error: \{err_text}",
          ),
        )
      }
      notes.push(
        "extraction degraded to raw unstructured (failure \{failures}/\{MAX_EXTRACTION_FAILURES}): \{err_text}",
      )
      Ok(AddExtractOutcome::{
        facts: [
          ExtractedFact::{
            content: text,
            kind: Unstructured,
            source_span: None,
            degraded: true,
            degrade_reason: Some(err_text),
          },
        ],
        extraction_failed: true,
        degraded: true,
      })
    }
  }
}

///|
/// 成功且无自报降级时清零连续失败计数(ReturnRaw 自报降级不得清零)。
fn MemoryStore::clear_extraction_streak_if_clean(
  self : MemoryStore,
  facts : Array[ExtractedFact],
  extraction_failed : Bool,
) -> Unit {
  if extraction_failed {
    return
  }
  let mut any_self_degrade = false
  for f in facts {
    if f.degraded {
      any_self_degrade = true
    }
  }
  if !any_self_degrade {
    self.extraction_failures = 0
  }
}

///|
/// 记录提取器自报降级(ReturnRaw);Err 路径已计数,避免双重递增。
/// 返回是否因本 fact 标记 degraded。
fn MemoryStore::note_fact_self_degrade(
  self : MemoryStore,
  fact : ExtractedFact,
  extraction_failed : Bool,
  notes : Array[String],
) -> Bool {
  if !(fact.degraded && !extraction_failed) {
    return false
  }
  self.extraction_failures = self.extraction_failures + 1
  let why = match fact.degrade_reason {
    Some(r) => r
    None => "reason not reported"
  }
  let streak = self.extraction_failures
  notes.push(
    "extraction degraded to raw (streak \{streak}/\{MAX_EXTRACTION_FAILURES}): \{why}",
  )
  true
}

///|
/// 精确去重:同 user 内容指纹命中则记 note,返回 true 表示应跳过。
fn MemoryStore::skip_if_duplicate(
  self : MemoryStore,
  user_id : String,
  content : String,
  notes : Array[String],
) -> Bool {
  match self.dedup.lookup(user_id, content) {
    Some(existing_id) => {
      notes.push("duplicate of \{existing_id}; skipped")
      true
    }
    None => false
  }
}

///|
/// 嵌入(失败降级:空向量 → 仅 BM25)。返回 (vec, embedding_failed, degraded)。
fn MemoryStore::embed_for_add(
  self : MemoryStore,
  content : String,
  notes : Array[String],
) -> (Array[Double], Bool, Bool) {
  let vec : Array[Double] = match self.embedder.embed([content]) {
    Ok(vs) => if vs.length() > 0 { vs[0] } else { [] }
    Err(e) => {
      notes.push("embedding degraded to keyword-only: \{e.message()}")
      return ([], true, true)
    }
  }
  let embedding_failed = vec.length() != self.dim
  (vec, embedding_failed, false)
}

///|
/// 冲突候选:向量 + 关键词各取 conflict_candidates,仅本 user 可检索条目。
fn MemoryStore::collect_conflict_candidates(
  self : MemoryStore,
  user_id : String,
  content : String,
  vec : Array[Double],
) -> Array[MemoryEntry] {
  let candidate_ids : Array[String] = []
  if vec.length() == self.dim {
    for pair in self.vector_index.search(user_id, vec, self.conflict_candidates) {
      if !candidate_ids.contains(pair.0) {
        candidate_ids.push(pair.0)
      }
    }
  }
  for
    pair in self.keyword_index.search(
      user_id,
      content,
      self.conflict_candidates,
    ) {
    if !candidate_ids.contains(pair.0) {
      candidate_ids.push(pair.0)
    }
  }
  let candidates : Array[MemoryEntry] = []
  for cid in candidate_ids {
    match self.entries.get(cid) {
      Some(e) =>
        if e.user_id == user_id && e.is_recallable() {
          candidates.push(e)
        }
      None => ()
    }
  }
  candidates
}

///|
/// 冲突判定(失败降级:空裁决 → 两条并存)。返回 (verdicts, degraded)。
fn MemoryStore::judge_for_add(
  self : MemoryStore,
  fact : ExtractedFact,
  candidates : Array[MemoryEntry],
  notes : Array[String],
) -> (Array[ConflictVerdict], Bool) {
  match self.judge.judge(fact, candidates) {
    Ok(vs) => (vs, false)
    Err(e) => {
      notes.push("conflict judge degraded to coexist: \{e.message()}")
      ([], true)
    }
  }
}

///|
/// 构造条目、入库、双索引;返回新条目。
fn MemoryStore::insert_fact_entry(
  self : MemoryStore,
  user_id : String,
  fact : ExtractedFact,
  vec : Array[Double],
  extraction_degraded_flag : Bool,
  embedding_failed : Bool,
) -> MemoryEntry {
  let seq = self.next_seq
  self.next_seq = self.next_seq + 1
  let created_at = self.clock.now_ms()
  if created_at > self.clock_high {
    self.clock_high = created_at
  }
  let id = generate_entry_id(seq, fact.content, created_at)
  let keywords = tokenize(fact.content)
  let entry = MemoryEntry::{
    id,
    user_id,
    content: fact.content,
    kind: fact.kind,
    embedding: vec,
    keywords,
    status: Active,
    created_at,
    superseded_by: None,
    seq,
    metadata: Map([]),
  }
  entry.metadata.set("extractor", Json::string(self.extractor.mode()))
  if extraction_degraded_flag || fact.degraded {
    entry.metadata.set("extraction_degraded", Json::string("true"))
  }
  if embedding_failed {
    entry.metadata.set("embedding_degraded", Json::string("true"))
  }
  self.entries.set(id, entry)
  match self.by_user.get(user_id) {
    Some(arr) => arr.push(id)
    None => self.by_user.set(user_id, [id])
  }
  self.index_entry(entry)
  entry
}

///|
/// 应用冲突裁决(Active→Superseded);返回 supersede 次数。
fn MemoryStore::apply_conflict_verdicts(
  self : MemoryStore,
  entry : MemoryEntry,
  verdicts : Array[ConflictVerdict],
  notes : Array[String],
) -> Int32 {
  let mut superseded = 0
  let id = entry.id
  for v in verdicts {
    match v.decision {
      Replace => {
        let old = v.existing
        let threshold = self.supersede_threshold
        old.status = Superseded
        old.superseded_by = Some(id)
        self.deindex_entry(old)
        superseded = superseded + 1
        notes.push(
          "superseded \{old.id} (\"\{old.content}\") by similarity >= \{threshold}",
        )
        entry.metadata.set("conflict_decision", Json::string("replace"))
        entry.metadata.set("conflict_with", Json::string(old.id))
      }
      Coexist => {
        entry.metadata.set("conflict_decision", Json::string("coexist"))
        entry.metadata.set("conflict_with", Json::string(v.existing.id))
        notes.push("coexists with \{v.existing.id} (\"\{v.existing.content}\")")
      }
      Ignore => ()
    }
  }
  superseded
}

///|
/// 有状态变更才落盘。
fn MemoryStore::flush_after_add_mutation(
  self : MemoryStore,
  inserted : Int32,
  superseded : Int32,
) -> Result[Unit, MoomemError] {
  if inserted > 0 || superseded > 0 {
    self.flush()
  } else {
    Ok(())
  }
}

///|
/// 写入记忆:校验 → 提取 → 去重 → 冲突 → 嵌入 → 双索引 → supersede → 落盘。
pub fn MemoryStore::add(
  self : MemoryStore,
  user_id : String,
  text : String,
) -> Result[AddSummary, MoomemError] {
  if self.closed {
    return Err(MoomemError::InvalidOperation("store is closed"))
  }
  match validate_user_id(user_id) {
    Ok(_) => ()
    Err(e) => return Err(e)
  }
  let notes : Array[String] = []
  let outcome = match self.extract_for_add(user_id, text, notes) {
    Ok(o) => o
    Err(e) => return Err(e)
  }
  let facts = outcome.facts
  let extraction_failed = outcome.extraction_failed
  let mut degraded = outcome.degraded
  if facts.length() == 0 {
    // 提取成功但无事实(纯闲聊)同样属于成功,须清零连续失败计数
    self.extraction_failures = 0
    return Ok(AddSummary::{
      extracted: 0,
      inserted: 0,
      duplicates: 0,
      superseded: 0,
      degraded: false,
      notes: ["extracted 0 facts; nothing stored (chitchat filtered)"],
    })
  }
  self.clear_extraction_streak_if_clean(facts, extraction_failed)
  let extraction_degraded_flag = extraction_failed
  let mut inserted = 0
  let mut duplicates = 0
  let mut superseded = 0
  for fact in facts {
    if self.note_fact_self_degrade(fact, extraction_failed, notes) {
      degraded = true
    }
    if self.skip_if_duplicate(user_id, fact.content, notes) {
      duplicates = duplicates + 1
      continue
    }
    let (vec, embedding_failed, embed_degraded) = self.embed_for_add(
      fact.content,
      notes,
    )
    if embed_degraded {
      degraded = true
    }
    let candidates = self.collect_conflict_candidates(
      user_id,
      fact.content,
      vec,
    )
    let (verdicts, judge_degraded) = self.judge_for_add(fact, candidates, notes)
    if judge_degraded {
      degraded = true
    }
    let entry = self.insert_fact_entry(
      user_id, fact, vec, extraction_degraded_flag, embedding_failed,
    )
    inserted = inserted + 1
    superseded = superseded +
      self.apply_conflict_verdicts(entry, verdicts, notes)
  }
  match self.flush_after_add_mutation(inserted, superseded) {
    Ok(_) => ()
    Err(e) => return Err(e)
  }
  Ok(AddSummary::{
    extracted: facts.length(),
    inserted,
    duplicates,
    superseded,
    degraded,
    notes,
  })
}