///|
pub fn FoldEvaluation::index(self : FoldEvaluation) -> Int {
  self.fold_number
}

///|
pub fn FoldEvaluation::train_indices(self : FoldEvaluation) -> Array[Int] {
  self.training_rows.copy()
}

///|
pub fn FoldEvaluation::test_indices(self : FoldEvaluation) -> Array[Int] {
  self.testing_rows.copy()
}

///|
pub fn FoldEvaluation::actual(self : FoldEvaluation) -> Array[Int] {
  self.actual_labels.copy()
}

///|
pub fn FoldEvaluation::predicted(self : FoldEvaluation) -> Array[Int] {
  self.predicted_labels.copy()
}

///|
pub fn FoldEvaluation::metrics(self : FoldEvaluation) -> ClassificationMetrics {
  self.fold_metrics
}

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

///|
pub fn ValidationReport::fold_count(self : ValidationReport) -> Int {
  self.fold_evaluations.length()
}

///|
pub fn ValidationReport::folds(
  self : ValidationReport,
) -> Array[FoldEvaluation] {
  self.fold_evaluations.copy()
}

///|
pub fn ValidationReport::metrics(
  self : ValidationReport,
) -> ClassificationMetrics {
  self.aggregate_metrics
}

///|
pub fn ValidationReport::observation_count(self : ValidationReport) -> Int {
  self.evaluated_observations
}

///|
pub fn ValidationReport::scaling(self : ValidationReport) -> ScalingPlan {
  self.scaling_plan
}

///|
fn scaled_fold_data(
  training_data : Dataset,
  testing_data : Dataset,
  scaling : ScalingPlan,
) -> Result[(Dataset, Dataset, FeatureScaler?), SvmError] {
  match scaling {
    NoScaling => Ok((training_data, testing_data, None))
    StandardScaling => {
      let scaler = match fit_scaler(training_data, StandardScale) {
        Err(error) => return Err(error)
        Ok(value) => value
      }
      let scaled_training = match
        scaler.transform_dataset(training_data, "scaled training fold") {
        Err(error) => return Err(error)
        Ok(value) => value
      }
      let scaled_testing = match
        scaler.transform_dataset(testing_data, "scaled testing fold") {
        Err(error) => return Err(error)
        Ok(value) => value
      }
      Ok((scaled_training, scaled_testing, Some(scaler)))
    }
    MinMaxScaling(lower, upper) => {
      let scaler = match fit_scaler(training_data, MinMaxScale(lower, upper)) {
        Err(error) => return Err(error)
        Ok(value) => value
      }
      let scaled_training = match
        scaler.transform_dataset(training_data, "scaled training fold") {
        Err(error) => return Err(error)
        Ok(value) => value
      }
      let scaled_testing = match
        scaler.transform_dataset(testing_data, "scaled testing fold") {
        Err(error) => return Err(error)
        Ok(value) => value
      }
      Ok((scaled_training, scaled_testing, Some(scaler)))
    }
  }
}

///|
/// Evaluates held-out predictions with preprocessing fitted inside each fold.
pub fn cross_validate(
  data : Dataset,
  config : BinaryConfig,
  fold_count : Int,
  scaling : ScalingPlan,
) -> Result[ValidationReport, SvmError] {
  let partitions = match stratified_folds(data, fold_count) {
    Err(error) => return Err(error)
    Ok(value) => value
  }
  let evaluations : Array[FoldEvaluation] = []
  let aggregate_actual : Array[Int] = []
  let aggregate_predicted : Array[Int] = []
  for partition in partitions {
    let training_data = match
      data.subset(
        partition.train_indices(),
        "training fold \{partition.index()}",
      ) {
      Err(error) => return Err(error)
      Ok(value) => value
    }
    let testing_data = match
      data.subset(partition.test_indices(), "testing fold \{partition.index()}") {
      Err(error) => return Err(error)
      Ok(value) => value
    }
    let (fit_data, predict_data, scaler) = match
      scaled_fold_data(training_data, testing_data, scaling) {
      Err(error) => return Err(error)
      Ok(value) => value
    }
    let model = match train_multiclass(fit_data, config) {
      Err(error) => return Err(error)
      Ok(value) => value
    }
    let actual = predict_data.labels()
    let predicted = match model.predict_batch(predict_data.features()) {
      Err(error) => return Err(error)
      Ok(value) => value
    }
    let metrics = match classification_metrics(actual, predicted) {
      Err(error) => return Err(error)
      Ok(value) => value
    }
    for label in actual {
      aggregate_actual.push(label)
    }
    for label in predicted {
      aggregate_predicted.push(label)
    }
    evaluations.push({
      fold_number: partition.index(),
      training_rows: partition.train_indices(),
      testing_rows: partition.test_indices(),
      actual_labels: actual,
      predicted_labels: predicted,
      fold_metrics: metrics,
      fitted_scaler: scaler,
    })
  }
  let aggregate = match
    classification_metrics(aggregate_actual, aggregate_predicted) {
    Err(error) => return Err(error)
    Ok(value) => value
  }
  Ok({
    fold_evaluations: evaluations,
    aggregate_metrics: aggregate,
    evaluated_observations: data.row_count(),
    scaling_plan: scaling,
  })
}