///|
/// 倒排索引中的一条 posting:文档编号 + 词频。
pub struct Posting {
  doc_id : Int
  tf : Int
} derive(ToJson, FromJson, Eq, Debug)

///|
/// BM25 倒排索引。
///
/// 支持增量添加文档、Top-K 检索,以及 JSON 序列化/反序列化。
/// 设计为不可变更新风格:`add` 返回添加后的新索引。
pub struct SearchIndex {
  mut ids : Array[String]
  mut texts : Array[String]
  mut lens : Array[Int]
  mut postings : Map[String, Array[Posting]]
  mut total_docs : Int
  mut sum_len : Int
  mut avg_dl : Double
  k1 : Double
  b : Double
  tokenizer : Tokenizer
} derive(ToJson, FromJson)

///|
pub fn SearchIndex::new(
  k1? : Double = 1.2,
  b? : Double = 0.75,
  tokenizer? : Tokenizer = Tokenizer::new(),
) -> SearchIndex {
  {
    ids: [],
    texts: [],
    lens: [],
    postings: Map([]),
    total_docs: 0,
    sum_len: 0,
    avg_dl: 0.0,
    k1,
    b,
    tokenizer,
  }
}

///|
/// 向索引添加一篇文档,返回新索引。`id` 可以是文档路径或分块 ID。
pub fn SearchIndex::add(self : SearchIndex, id : String, text : String) -> Unit {
  let tokens = self.tokenizer.tokenize(text)
  let doc_id = self.total_docs
  self.ids.push(id)
  self.texts.push(text)
  self.lens.push(tokens.length())
  let tf : Map[String, Int] = Map([])
  for t in tokens {
    tf[t] = tf.get_or_default(t, 0) + 1
  }
  for kv in tf.iter() {
    let (term, count) = kv
    let postings = self.postings.get_or_default(term, [])
    postings.push({ doc_id, tf: count, })
    self.postings[term] = postings
  }
  self.total_docs = self.total_docs + 1
  self.sum_len = self.sum_len + tokens.length()
  self.avg_dl = self.sum_len.to_double() / self.total_docs.to_double()
}

///|
/// 对查询执行 BM25 检索,返回按分数降序排列的 Top-K 结果。
pub fn SearchIndex::search(
  self : SearchIndex,
  query : String,
  top_k? : Int = 10,
) -> Array[SearchResult] {
  self.rank_postings(unique_tokens(self.tokenizer.tokenize(query)), top_k)
}

///|
/// 短语检索:查询短语必须按原文顺序连续出现,结果仍按 BM25 分数排序。
pub fn SearchIndex::search_phrase(
  self : SearchIndex,
  query : String,
  top_k? : Int = 10,
) -> Array[SearchResult] {
  let q_tokens = self.tokenizer.tokenize(query)
  if q_tokens.is_empty() || self.total_docs == 0 {
    return []
  }
  let unique = unique_tokens(q_tokens)
  let scored : Array[(Int, Double)] = []
  for i = 0; i < self.total_docs; i = i + 1 {
    if self.texts[i].contains(query) {
      scored.push((i, self.score_doc(unique, i)))
    }
  }
  scored.sort_by(fn(a, b) {
    let cmp = b.1.compare(a.1)
    if cmp != 0 {
      cmp
    } else {
      a.0.compare(b.0)
    }
  })
  let out : Array[SearchResult] = []
  for i = 0; i < scored.length() && i < top_k; i = i + 1 {
    let (doc, score) = scored[i]
    out.push({ doc_id: self.ids[doc], score, text: self.texts[doc], })
  }
  out
}

///|
/// 布尔检索:支持 `AND` / `OR` 连接词与 `-term` 排除。
///
/// 示例:
/// - `"moonbit AND rag"`:同时包含 moonbit 与 rag
/// - `"moonbit OR rag"`:包含任一即可
/// - `"moonbit -old"`:包含 moonbit 且不含 old
pub fn SearchIndex::search_boolean(
  self : SearchIndex,
  query : String,
  top_k? : Int = 10,
) -> Array[SearchResult] {
  let parts : Array[String] = []
  for p in query.split(" ") {
    let s = p.trim().to_owned()
    if !s.is_empty() {
      parts.push(s)
    }
  }
  let must : Array[String] = []
  let any : Array[String] = []
  let not : Array[String] = []
  let has_and = parts.contains("AND")
  let has_or = parts.contains("OR")
  for p in parts {
    if p == "AND" || p == "OR" {
      continue
    }
    if p.has_prefix("-") {
      let t = p.exact_view(start=1, end=p.length()).to_owned()
      if !t.is_empty() {
        not.push(t)
      }
    } else if has_and {
      must.push(p)
    } else if has_or {
      any.push(p)
    } else {
      any.push(p)
    }
  }
  let must_tokens = self.expand_terms(must)
  let any_tokens = self.expand_terms(any)
  let not_tokens = self.expand_terms(not)
  if must_tokens.is_empty() && any_tokens.is_empty() {
    return []
  }
  let positive = unique_tokens(must_tokens + any_tokens)
  let scored : Array[(Int, Double)] = []
  for i = 0; i < self.total_docs; i = i + 1 {
    if self.match_boolean(i, must_tokens, any_tokens, not_tokens) {
      scored.push((i, self.score_doc(positive, i)))
    }
  }
  scored.sort_by(fn(a, b) {
    let cmp = b.1.compare(a.1)
    if cmp != 0 {
      cmp
    } else {
      a.0.compare(b.0)
    }
  })
  let out : Array[SearchResult] = []
  for i = 0; i < scored.length() && i < top_k; i = i + 1 {
    let (doc, score) = scored[i]
    out.push({ doc_id: self.ids[doc], score, text: self.texts[doc], })
  }
  out
}

///|
/// 前缀检索:匹配以给定前缀开头的词条,结果按 BM25 分数排序。
pub fn SearchIndex::search_prefix(
  self : SearchIndex,
  prefix : String,
  top_k? : Int = 10,
) -> Array[SearchResult] {
  if prefix.is_empty() || top_k <= 0 || self.total_docs == 0 {
    return []
  }
  let terms : Array[String] = []
  for kv in self.postings.iter() {
    let (term, _) = kv
    if term.has_prefix(prefix) {
      terms.push(term)
    }
  }
  if terms.is_empty() {
    return []
  }
  self.rank_postings(terms, top_k)
}

///|
/// Accumulate BM25 contributions only for documents present in query postings.
fn SearchIndex::rank_postings(
  self : SearchIndex,
  terms : Array[String],
  top_k : Int,
) -> Array[SearchResult] {
  if top_k <= 0 || terms.is_empty() || self.total_docs == 0 {
    return []
  }
  let scores : Map[Int, Double] = Map([])
  let avg = if self.avg_dl > 0.0 { self.avg_dl } else { 1.0 }
  for term in terms {
    match self.postings.get(term) {
      None => continue
      Some(postings) => {
        let df = postings.length()
        let idf = @math.ln(
          1.0 +
          (self.total_docs.to_double() - df.to_double() + 0.5) /
          (df.to_double() + 0.5),
        )
        for posting in postings {
          let doc = posting.doc_id
          let tf = posting.tf.to_double()
          let denom = tf +
            self.k1 * (1.0 - self.b + self.b * self.lens[doc].to_double() / avg)
          let contribution = idf * (tf * (self.k1 + 1.0)) / denom
          scores[doc] = scores.get_or_default(doc, 0.0) + contribution
        }
      }
    }
  }
  let scored : Array[(Int, Double)] = []
  for entry in scores.iter() {
    let (doc, score) = entry
    if score > 0.0 {
      scored.push((doc, score))
    }
  }
  scored.sort_by(fn(a, b) {
    let cmp = b.1.compare(a.1)
    if cmp != 0 {
      cmp
    } else {
      a.0.compare(b.0)
    }
  })
  let out : Array[SearchResult] = []
  for i = 0; i < scored.length() && i < top_k; i = i + 1 {
    let (doc, score) = scored[i]
    out.push({ doc_id: self.ids[doc], score, text: self.texts[doc], })
  }
  out
}

///|
/// 序列化为 JSON 字符串。
pub fn SearchIndex::save(self : SearchIndex) -> String {
  @json.to_json(self).stringify()
}

///|
/// 从 JSON 字符串恢复索引。
pub fn SearchIndex::try_load(json : String) -> Result[SearchIndex, String] {
  try {
    let j = @json.parse(json)
    Ok(@json.from_json(j))
  } catch {
    err => Err(err.to_string())
  }
}

///|
/// 当前索引中的文档/分块数量。
pub fn SearchIndex::count(self : SearchIndex) -> Int {
  self.total_docs
}

///|
/// 按文档 ID 删除一篇文档,返回是否删除成功。
pub fn SearchIndex::remove(self : SearchIndex, id : String) -> Bool {
  self.remove_if(fn(x) { x == id }) > 0
}

///|
/// 按条件删除文档,返回删除数量。删除后内部编号重建,复杂度 O(N)。
pub fn SearchIndex::remove_if(
  self : SearchIndex,
  pred : (String) -> Bool,
) -> Int {
  let positions : Array[Int] = []
  for i = 0; i < self.ids.length(); i = i + 1 {
    if pred(self.ids[i]) {
      positions.push(i)
    }
  }
  if positions.is_empty() {
    return 0
  }
  let new_ids : Array[String] = []
  let new_texts : Array[String] = []
  let new_lens : Array[Int] = []
  let mut new_sum = 0
  for i = 0; i < self.ids.length(); i = i + 1 {
    if !positions.contains(i) {
      new_ids.push(self.ids[i])
      new_texts.push(self.texts[i])
      new_lens.push(self.lens[i])
      new_sum = new_sum + self.lens[i]
    }
  }
  self.ids = new_ids
  self.texts = new_texts
  self.lens = new_lens
  self.total_docs = self.ids.length()
  self.sum_len = new_sum
  self.avg_dl = if self.total_docs > 0 {
    new_sum.to_double() / self.total_docs.to_double()
  } else {
    0.0
  }
  self.rebuild_postings()
  positions.length()
}

///|
/// 返回索引统计信息。
pub fn SearchIndex::stats(self : SearchIndex) -> IndexStats {
  {
    doc_count: self.total_docs,
    term_count: self.postings.length(),
    total_tokens: self.sum_len,
    avg_doc_len: self.avg_dl,
  }
}

///|
fn SearchIndex::rebuild_postings(self : SearchIndex) -> Unit {
  self.postings = Map([])
  for i = 0; i < self.texts.length(); i = i + 1 {
    let tokens = self.tokenizer.tokenize(self.texts[i])
    let tf : Map[String, Int] = Map([])
    for t in tokens {
      tf[t] = tf.get_or_default(t, 0) + 1
    }
    for kv in tf.iter() {
      let (term, count) = kv
      let postings = self.postings.get_or_default(term, [])
      postings.push({ doc_id: i, tf: count, })
      self.postings[term] = postings
    }
  }
}

///|
fn unique_tokens(tokens : Array[String]) -> Array[String] {
  let out : Array[String] = []
  for t in tokens {
    if !out.contains(t) {
      out.push(t)
    }
  }
  out
}

///|
fn SearchIndex::expand_terms(
  self : SearchIndex,
  terms : Array[String],
) -> Array[String] {
  let out : Array[String] = []
  for t in terms {
    for tok in self.tokenizer.tokenize(t) {
      out.push(tok)
    }
  }
  out
}

///|
fn SearchIndex::doc_has_term(
  self : SearchIndex,
  doc : Int,
  term : String,
) -> Bool {
  match self.postings.get(term) {
    None => false
    Some(postings) => {
      for p in postings {
        if p.doc_id == doc {
          return true
        }
      }
      false
    }
  }
}

///|
fn SearchIndex::match_boolean(
  self : SearchIndex,
  doc : Int,
  must : Array[String],
  any : Array[String],
  not : Array[String],
) -> Bool {
  for t in must {
    if !self.doc_has_term(doc, t) {
      return false
    }
  }
  if !any.is_empty() {
    let mut ok = false
    for t in any {
      if self.doc_has_term(doc, t) {
        ok = true
        break
      }
    }
    if !ok {
      return false
    }
  }
  for t in not {
    if self.doc_has_term(doc, t) {
      return false
    }
  }
  true
}

///|
fn SearchIndex::score_doc(
  self : SearchIndex,
  query_tokens : Array[String],
  doc : Int,
) -> Double {
  let mut score = 0.0
  let dl = self.lens[doc]
  for t in query_tokens {
    match self.postings.get(t) {
      None => continue
      Some(postings) => {
        let mut tf = 0
        for p in postings {
          if p.doc_id == doc {
            tf = p.tf
            break
          }
        }
        if tf == 0 {
          continue
        }
        let n = self.total_docs
        let df = postings.length()
        let idf = @math.ln(
          1.0 + (n.to_double() - df.to_double() + 0.5) / (df.to_double() + 0.5),
        )
        let avg = if self.avg_dl > 0.0 { self.avg_dl } else { 1.0 }
        let denom = tf.to_double() +
          self.k1 * (1.0 - self.b + self.b * dl.to_double() / avg)
        score = score + idf * (tf.to_double() * (self.k1 + 1.0)) / denom
      }
    }
  }
  score
}