///|
pub fn profile_candidate_pools(
  pools : Array[CandidatePool],
) -> CandidatePoolProfile {
  let unique : Map[String, Unit] = Map([])
  let mut total = 0
  let mut duplicates = 0
  let mut minimum = 0
  let mut maximum = 0
  for pool_index, pool in pools {
    let seen_in_pool : Map[String, Unit] = Map([])
    total += pool.doc_ids.length()
    if pool_index == 0 || pool.doc_ids.length() < minimum {
      minimum = pool.doc_ids.length()
    }
    if pool.doc_ids.length() > maximum {
      maximum = pool.doc_ids.length()
    }
    for doc_id in pool.doc_ids {
      if seen_in_pool.contains(doc_id) {
        duplicates += 1
      } else {
        seen_in_pool[doc_id] = ()
      }
      unique["\{pool.query_id}:\{doc_id}"] = ()
    }
  }
  {
    pool_count: pools.length(),
    total_candidate_count: total,
    unique_candidate_count: unique.length(),
    duplicate_candidate_count: duplicates,
    mean_pool_size: if pools.is_empty() {
      0.0
    } else {
      Double::from_int(total) / Double::from_int(pools.length())
    },
    min_pool_size: minimum,
    max_pool_size: maximum,
  }
}

///|
pub fn candidate_pool_coverage(
  qrels : Array[JudgedDoc],
  pools : Array[CandidatePool],
) -> Double {
  let relevant : Map[String, Unit] = Map([])
  for item in qrels {
    if item.relevance > 0 {
      relevant["\{item.query_id}:\{item.doc_id}"] = ()
    }
  }
  if relevant.is_empty() {
    return 0.0
  }
  let covered : Map[String, Unit] = Map([])
  for pool in pools {
    for doc_id in pool.doc_ids {
      let key = "\{pool.query_id}:\{doc_id}"
      if relevant.contains(key) {
        covered[key] = ()
      }
    }
  }
  to_ratio(covered.length(), relevant.length())
}

///|
pub fn merge_candidate_pools(
  left : Array[CandidatePool],
  right : Array[CandidatePool],
) -> Array[CandidatePool] {
  let merged : Map[String, Array[String]] = Map([])
  for pool in left {
    let docs = merged.get_or_init(pool.query_id, fn() { [] })
    for doc_id in pool.doc_ids {
      if !docs.contains(doc_id) {
        docs.push(doc_id)
      }
    }
  }
  for pool in right {
    let docs = merged.get_or_init(pool.query_id, fn() { [] })
    for doc_id in pool.doc_ids {
      if !docs.contains(doc_id) {
        docs.push(doc_id)
      }
    }
  }
  let result : Array[CandidatePool] = []
  for query_id, doc_ids in merged {
    doc_ids.sort()
    result.push({ query_id, doc_ids })
  }
  result.sort_by(fn(a, b) { a.query_id.compare(b.query_id) })
  result
}