///|
fn validation_issue(
  code : String,
  level : ValidationLevel,
  message : String,
  query_id? : String = "",
  doc_id? : String = "",
) -> ValidationIssue {
  { code, level, message, query_id, doc_id }
}

///|
fn add_validation_issue(
  issues : Array[ValidationIssue],
  code : String,
  level : ValidationLevel,
  message : String,
  query_id? : String = "",
  doc_id? : String = "",
) -> Unit {
  issues.push(validation_issue(code, level, message, query_id~, doc_id~))
}

///|
fn has_query(
  qrels_by_query : Map[String, Array[JudgedDoc]],
  query_id : String,
) -> Bool {
  qrels_by_query.contains(query_id)
}

///|
pub fn validate_dataset(
  qrels : Array[JudgedDoc],
  run : Array[RetrievedDoc],
) -> ValidationSummary {
  let qrels_by_query = group_qrels(qrels)
  let runs_by_query = group_runs(run)
  let issues : Array[ValidationIssue] = []
  let judged_keys : Map[String, Unit] = Map([])
  let duplicate_judgments : Map[String, Unit] = Map([])
  for item in qrels {
    if item.query_id.is_empty() {
      add_validation_issue(
        issues,
        "empty_query_id",
        Error,
        "qrels query_id must not be empty",
        doc_id=item.doc_id,
      )
    }
    if item.doc_id.is_empty() {
      add_validation_issue(
        issues,
        "empty_doc_id",
        Error,
        "qrels doc_id must not be empty",
        query_id=item.query_id,
      )
    }
    if item.relevance < 0 {
      add_validation_issue(
        issues,
        "negative_relevance",
        Error,
        "relevance must be non-negative",
        query_id=item.query_id,
        doc_id=item.doc_id,
      )
    }
    let key = "\{item.query_id}:\{item.doc_id}"
    if judged_keys.contains(key) {
      if !duplicate_judgments.contains(key) {
        duplicate_judgments[key] = ()
        add_validation_issue(
          issues,
          "duplicate_judgment",
          Warning,
          "qrels contains a duplicate query/document judgment",
          query_id=item.query_id,
          doc_id=item.doc_id,
        )
      }
    } else {
      judged_keys[key] = ()
    }
  }
  let run_keys : Map[String, Unit] = Map([])
  let duplicate_runs : Map[String, Unit] = Map([])
  for item in run {
    if item.query_id.is_empty() {
      add_validation_issue(
        issues,
        "empty_query_id",
        Error,
        "run query_id must not be empty",
        doc_id=item.doc_id,
      )
    }
    if item.doc_id.is_empty() {
      add_validation_issue(
        issues,
        "empty_doc_id",
        Error,
        "run doc_id must not be empty",
        query_id=item.query_id,
      )
    }
    if item.score.is_nan() || item.score.is_inf() {
      add_validation_issue(
        issues,
        "non_finite_score",
        Error,
        "run score must be finite",
        query_id=item.query_id,
        doc_id=item.doc_id,
      )
    }
    let key = "\{item.query_id}:\{item.doc_id}"
    if run_keys.contains(key) {
      if !duplicate_runs.contains(key) {
        duplicate_runs[key] = ()
        add_validation_issue(
          issues,
          "duplicate_run_doc",
          Warning,
          "run contains a duplicate query/document row",
          query_id=item.query_id,
          doc_id=item.doc_id,
        )
      }
    } else {
      run_keys[key] = ()
    }
    if !has_query(qrels_by_query, item.query_id) {
      add_validation_issue(
        issues,
        "unknown_query",
        Warning,
        "run query_id does not occur in qrels",
        query_id=item.query_id,
        doc_id=item.doc_id,
      )
    }
  }
  for query_id, _ in qrels_by_query {
    if !runs_by_query.contains(query_id) {
      add_validation_issue(
        issues,
        "missing_run_query",
        Warning,
        "qrels query_id has no retrieved results",
        query_id~,
      )
    }
  }
  if qrels.is_empty() {
    add_validation_issue(
      issues,
      "empty_qrels",
      Error,
      "qrels must contain at least one row",
    )
  }
  if run.is_empty() {
    add_validation_issue(issues, "empty_run", Warning, "run contains no rows")
  }
  let mut error_count = 0
  let mut warning_count = 0
  for issue in issues {
    match issue.level {
      Error => error_count += 1
      Warning => warning_count += 1
    }
  }
  {
    error_count,
    warning_count,
    query_count: sorted_query_ids(qrels_by_query, runs_by_query).length(),
    judged_count: qrels.length(),
    retrieved_count: run.length(),
    issues,
  }
}

///|
pub fn normalize_run(run : Array[RetrievedDoc]) -> Array[RetrievedDoc] {
  let grouped = group_runs(run)
  let normalized : Array[RetrievedDoc] = []
  let query_ids : Array[String] = []
  for query_id, _ in grouped {
    query_ids.push(query_id)
  }
  query_ids.sort()
  for query_id in query_ids {
    let seen : Map[String, Unit] = Map([])
    for item in grouped[query_id] {
      if !seen.contains(item.doc_id) {
        seen[item.doc_id] = ()
        normalized.push(item)
      }
    }
  }
  normalized
}

///|
pub fn normalize_cutoffs(cutoffs : Array[Int]) -> Array[Int] {
  unique_positive_cutoffs(cutoffs)
}