///|
priv struct LinkageCandidateBatch {
  candidates : Array[LinkCandidate]
  blocked_candidate_count : Int
  evaluated_pair_count : Int
}

///|
fn linkage_candidate_order(left : LinkCandidate, right : LinkCandidate) -> Int {
  if left.score > right.score {
    -1
  } else if left.score < right.score {
    1
  } else if left.left_id != right.left_id {
    left.left_id.lexical_compare(right.left_id)
  } else if left.right_id != right.right_id {
    left.right_id.lexical_compare(right.right_id)
  } else {
    0
  }
}

///|
fn linkage_evaluation_limit(
  candidate_count : Int,
  configured_limit : Int,
) -> Int {
  if candidate_count < configured_limit {
    candidate_count
  } else {
    configured_limit
  }
}

///|
fn evaluate_linkage_pair(
  left : LinkRecord,
  right : NameRecord,
  config : LinkageConfig,
) -> Result[LinkCandidate, LinkageError] {
  let evidence = match match_names(left.name, right.name, config.match_config) {
    Err(error) => return Err(LinkageMatchFailed(left.id, right.id, error))
    Ok(value) => value
  }
  let accepted = evidence.matched && evidence.score >= config.minimum_score
  Ok({
    left_id: left.id,
    right_id: right.id,
    score: evidence.score,
    accepted,
    evidence,
  })
}

///|
fn collect_left_linkage_candidates(
  prepared : PreparedLinkageInputs,
  left : LinkRecord,
  config : LinkageConfig,
) -> Result[(Array[LinkCandidate], Int, Int), LinkageError] {
  let candidate_set = match prepared.right_index.candidate_records(left.name) {
    Err(error) => return Err(LinkageIndexFailed(LinkageLeft, left.id, error))
    Ok(value) => value
  }
  let unscored = candidate_set.records.copy()
  unscored.sort_by(fn(first, second) {
    first.record.id.lexical_compare(second.record.id)
  })
  let blocked_count = unscored.length()
  let evaluation_count = linkage_evaluation_limit(
    blocked_count,
    config.max_candidates_per_left,
  )
  let retained : Array[LinkCandidate] = []
  for index = 0; index < evaluation_count; index = index + 1 {
    let candidate = match
      evaluate_linkage_pair(left, unscored[index].record, config) {
      Err(error) => return Err(error)
      Ok(value) => value
    }
    if candidate.accepted || config.retain_rejected {
      retained.push(candidate)
    }
  }
  Ok((retained, blocked_count, evaluation_count))
}

///|
fn collect_linkage_candidates(
  prepared : PreparedLinkageInputs,
  config : LinkageConfig,
) -> Result[LinkageCandidateBatch, LinkageError] {
  let candidates : Array[LinkCandidate] = []
  let mut blocked_candidate_count = 0
  let mut evaluated_pair_count = 0
  for left in prepared.left {
    let result = match collect_left_linkage_candidates(prepared, left, config) {
      Err(error) => return Err(error)
      Ok(value) => value
    }
    for candidate in result.0 {
      candidates.push(candidate)
    }
    blocked_candidate_count = blocked_candidate_count + result.1
    evaluated_pair_count = evaluated_pair_count + result.2
  }
  candidates.sort_by(linkage_candidate_order)
  Ok({ candidates, blocked_candidate_count, evaluated_pair_count })
}