///|
pub fn svm_candidate(
  name : String,
  config : BinaryConfig,
  scaling : ScalingPlan,
) -> SvmCandidate {
  {
    candidate_name: name,
    binary_configuration: config,
    candidate_scaling: scaling,
  }
}

///|
pub fn SvmCandidate::name(self : SvmCandidate) -> String {
  self.candidate_name
}

///|
pub fn SvmCandidate::config(self : SvmCandidate) -> BinaryConfig {
  self.binary_configuration
}

///|
pub fn SvmCandidate::scaling(self : SvmCandidate) -> ScalingPlan {
  self.candidate_scaling
}

///|
pub fn CandidateEvaluation::index(self : CandidateEvaluation) -> Int {
  self.original_index
}

///|
pub fn CandidateEvaluation::name(self : CandidateEvaluation) -> String {
  self.evaluated_name
}

///|
pub fn CandidateEvaluation::score(self : CandidateEvaluation) -> Double {
  self.metric_score
}

///|
pub fn CandidateEvaluation::validation(
  self : CandidateEvaluation,
) -> ValidationReport {
  self.validation_report
}

///|
pub fn CandidateFailure::index(self : CandidateFailure) -> Int {
  self.original_index
}

///|
pub fn CandidateFailure::name(self : CandidateFailure) -> String {
  self.failed_name
}

///|
pub fn CandidateFailure::error(self : CandidateFailure) -> SvmError {
  self.failure_error
}

///|
pub fn SearchResult::candidates(
  self : SearchResult,
) -> Array[CandidateEvaluation] {
  self.successful_candidates.copy()
}

///|
pub fn SearchResult::failures(self : SearchResult) -> Array[CandidateFailure] {
  self.failed_candidates.copy()
}

///|
pub fn SearchResult::best_index(self : SearchResult) -> Int {
  self.selected_original_index
}

///|
pub fn SearchResult::best_name(self : SearchResult) -> String {
  self.selected_name
}

///|
pub fn SearchResult::best_score(self : SearchResult) -> Double {
  self.selected_score
}

///|
pub fn SearchResult::metric(self : SearchResult) -> SelectionMetric {
  self.selected_metric
}

///|
pub fn SearchResult::validation(self : SearchResult) -> ValidationReport {
  self.selected_validation
}

///|
pub fn SearchResult::model(self : SearchResult) -> MulticlassModel {
  self.selected_model
}

///|
pub fn SearchResult::scaler(self : SearchResult) -> FeatureScaler? {
  self.selected_scaler
}

///|
pub fn SearchResult::predict(
  self : SearchResult,
  row : Array[Double],
) -> Result[Int, SvmError] {
  let prepared = match self.selected_scaler {
    None => row
    Some(scaler) =>
      match scaler.transform_row(row) {
        Err(error) => return Err(error)
        Ok(value) => value
      }
  }
  self.selected_model.predict(prepared)
}

///|
fn selection_score(
  metrics : ClassificationMetrics,
  selection : SelectionMetric,
) -> Double {
  match selection {
    SelectAccuracy => metrics.accuracy()
    SelectBalancedAccuracy => metrics.balanced_accuracy()
    SelectMacroF1 => metrics.macro_f1()
    SelectWeightedF1 => metrics.weighted_f1()
  }
}

///|
fn fit_selected_data(
  data : Dataset,
  scaling : ScalingPlan,
) -> Result[(Dataset, FeatureScaler?), SvmError] {
  match scaling {
    NoScaling => Ok((data, None))
    StandardScaling => {
      let scaler = match fit_scaler(data, StandardScale) {
        Err(error) => return Err(error)
        Ok(value) => value
      }
      let transformed = match
        scaler.transform_dataset(data, "selected full data") {
        Err(error) => return Err(error)
        Ok(value) => value
      }
      Ok((transformed, Some(scaler)))
    }
    MinMaxScaling(lower, upper) => {
      let scaler = match fit_scaler(data, MinMaxScale(lower, upper)) {
        Err(error) => return Err(error)
        Ok(value) => value
      }
      let transformed = match
        scaler.transform_dataset(data, "selected full data") {
        Err(error) => return Err(error)
        Ok(value) => value
      }
      Ok((transformed, Some(scaler)))
    }
  }
}

///|
/// Evaluates candidates in order and retains the earliest candidate on ties.
pub fn select_svm(
  data : Dataset,
  candidates : Array[SvmCandidate],
  fold_count : Int,
  metric : SelectionMetric,
) -> Result[SearchResult, SvmError] {
  if candidates.is_empty() {
    return Err(InvalidCandidateCount(0))
  }
  let successful : Array[CandidateEvaluation] = []
  let failures : Array[CandidateFailure] = []
  let mut best_success_index = -1
  let mut best_score = 0.0
  for index, candidate in candidates {
    match
      cross_validate(
        data,
        candidate.binary_configuration,
        fold_count,
        candidate.candidate_scaling,
      ) {
      Err(error) =>
        failures.push({
          original_index: index,
          failed_name: candidate.candidate_name,
          failure_error: error,
        })
      Ok(validation) => {
        let score = selection_score(validation.metrics(), metric)
        successful.push({
          original_index: index,
          evaluated_name: candidate.candidate_name,
          metric_score: score,
          validation_report: validation,
        })
        let current_success = successful.length() - 1
        if best_success_index < 0 || score > best_score {
          best_success_index = current_success
          best_score = score
        }
      }
    }
  }
  if best_success_index < 0 {
    return Err(NoValidCandidate)
  }
  let best_evaluation = successful[best_success_index]
  let best_candidate = candidates[best_evaluation.original_index]
  let (fit_data, scaler) = match
    fit_selected_data(data, best_candidate.candidate_scaling) {
    Err(error) => return Err(error)
    Ok(value) => value
  }
  let model = match
    train_multiclass(fit_data, best_candidate.binary_configuration) {
    Err(error) => return Err(error)
    Ok(value) => value
  }
  Ok({
    successful_candidates: successful,
    failed_candidates: failures,
    selected_original_index: best_evaluation.original_index,
    selected_name: best_evaluation.evaluated_name,
    selected_score: best_evaluation.metric_score,
    selected_metric: metric,
    selected_validation: best_evaluation.validation_report,
    selected_model: model,
    selected_scaler: scaler,
  })
}