///|
fn ideal_dcg(
  relevances : Array[Int],
  cutoff : Int,
  gain_scheme : GainScheme,
) -> Double {
  if cutoff <= 0 {
    return 0.0
  }
  let copied = relevances.copy()
  copied.sort_by(fn(a, b) { b - a })
  let limit = Int::min(cutoff, copied.length())
  let mut score = 0.0
  for index in 0.. QueryEvaluation {
  let normalized_cutoffs = unique_positive_cutoffs(config.cutoffs)
  let relevance_map = build_relevance_map(qrels)
  let relevant_levels : Array[Int] = []
  let mut relevant_total = 0
  for _, relevance in relevance_map {
    relevant_levels.push(relevance)
    if relevance >= config.relevant_threshold {
      relevant_total += 1
    }
  }
  let metrics : Map[String, Double] = Map([])
  for cutoff in normalized_cutoffs {
    let top = Int::min(cutoff, run.length())
    let mut hits = 0
    let mut judged_hits = 0
    let mut average_precision = 0.0
    let mut first_rr = 0.0
    let mut dcg = 0.0
    for index in 0..= config.relevant_threshold {
        hits += 1
        if first_rr == 0.0 {
          first_rr = reciprocal_rank(index)
        }
        average_precision += to_ratio(hits, index + 1)
      }
    }
    let denom = if relevant_total <= 0 { 1 } else { relevant_total }
    let best_dcg = ideal_dcg(relevant_levels, cutoff, config.gain_scheme)
    metrics[metric_name("recall", cutoff)] = to_ratio(hits, denom)
    metrics[metric_name("precision", cutoff)] = to_ratio(hits, cutoff)
    metrics[metric_name("hit_rate", cutoff)] = if hits > 0 { 1.0 } else { 0.0 }
    metrics[metric_name("mrr", cutoff)] = first_rr
    metrics[metric_name("map", cutoff)] = if relevant_total <= 0 {
      0.0
    } else {
      average_precision / Double::from_int(relevant_total)
    }
    metrics[metric_name("ndcg", cutoff)] = if best_dcg == 0.0 {
      0.0
    } else {
      dcg / best_dcg
    }
    metrics[metric_name("judged", cutoff)] = to_ratio(judged_hits, cutoff)
  }
  for cutoff in normalized_cutoffs {
    let advanced = evaluate_metric_family(
      qrels,
      run,
      cutoff,
      config.relevant_threshold,
      config.gain_scheme,
    )
    for name, value in advanced {
      metrics[name] = value
    }
  }
  { query_id, relevant_total, retrieved_total: run.length(), metrics }
}

///|
fn summarize_queries(
  queries : Array[QueryEvaluation],
) -> Array[AggregateMetric] {
  let values : Map[String, Array[Double]] = Map([])
  for query in queries {
    for name, value in query.metrics {
      let bucket = values.get_or_init(name, fn() { [] })
      bucket.push(value)
    }
  }
  let summary : Array[AggregateMetric] = []
  for name, bucket in values {
    if bucket.is_empty() {
      continue
    }
    let mut total = 0.0
    let mut min_value = bucket[0]
    let mut max_value = bucket[0]
    for value in bucket {
      total += value
      min_value = Double::min(min_value, value)
      max_value = Double::max(max_value, value)
    }
    summary.push({
      name,
      mean: total / Double::from_int(bucket.length()),
      min: min_value,
      max: max_value,
    })
  }
  summary.sort_by(fn(a, b) { a.name.compare(b.name) })
  summary
}

///|
pub fn evaluate_benchmark(
  qrels : Array[JudgedDoc],
  run : Array[RetrievedDoc],
  config? : EvalConfig = EvalConfig::default(),
) -> BenchmarkReport {
  let qrels_by_query = group_qrels(qrels)
  let run_by_query = group_runs(run)
  let query_ids = sorted_query_ids(qrels_by_query, run_by_query)
  let queries : Array[QueryEvaluation] = []
  for query_id in query_ids {
    let query_qrels = qrels_by_query.get_or_default(query_id, [])
    let query_run = run_by_query.get_or_default(query_id, [])
    queries.push(evaluate_query(query_id, query_qrels, query_run, config~))
  }
  {
    cutoffs: unique_positive_cutoffs(config.cutoffs),
    relevant_threshold: config.relevant_threshold,
    query_count: queries.length(),
    queries,
    summary: summarize_queries(queries),
  }
}