///|
pub fn judged_recall_at(
  qrels : Array[JudgedDoc],
  run : Array[RetrievedDoc],
  cutoff : Int,
) -> Double {
  let relevance_map = build_relevance_map(qrels)
  let ranked = sorted_run_items(run)
  let limit = Int::min(Int::max(cutoff, 0), ranked.length())
  let mut judged = 0
  for index in 0.. Double {
  if run.is_empty() {
    return 0.0
  }
  let mut total = 0.0
  for item in run {
    total += item.score
  }
  total / Double::from_int(run.length())
}

///|
pub fn score_stddev(run : Array[RetrievedDoc]) -> Double {
  if run.is_empty() {
    return 0.0
  }
  let mean = score_mean(run)
  let mut squared = 0.0
  for item in run {
    let delta = item.score - mean
    squared += delta * delta
  }
  Double::sqrt(squared / Double::from_int(run.length()))
}

///|
fn bpref_denominator(relevant_total : Int, non_relevant_total : Int) -> Int {
  Int::max(1, Int::min(relevant_total, Int::max(non_relevant_total, 1)))
}

///|
pub fn bpref_at(
  qrels : Array[JudgedDoc],
  run : Array[RetrievedDoc],
  cutoff : Int,
  threshold : Int,
) -> Double {
  let relevance_map = build_relevance_map(qrels)
  let mut relevant_total = 0
  let mut non_relevant_total = 0
  for _, relevance in relevance_map {
    if relevance >= threshold {
      relevant_total += 1
    } else {
      non_relevant_total += 1
    }
  }
  if relevant_total <= 0 {
    return 0.0
  }
  let ranked = sorted_run_items(run)
  let limit = Int::min(Int::max(cutoff, 0), ranked.length())
  let denominator = bpref_denominator(relevant_total, non_relevant_total)
  let mut seen_non_relevant = 0
  let mut total = 0.0
  for index in 0..= threshold {
      let penalty = Int::min(seen_non_relevant, denominator)
      total += 1.0 - to_ratio(penalty, denominator)
    } else if relevance_map.contains(ranked[index].doc_id) {
      seen_non_relevant += 1
    }
  }
  total / Double::from_int(relevant_total)
}