///|
/// One rolling-origin forecast evaluation record.
pub struct BacktestFold {
  origin : Int
  horizon : Int
  actual : Array[Double]
  predicted : Array[Double]
  mae : Double
  rmse : Double
  bias : Double
  coverage : Double
}

///|
/// Summary of a backtest run.
pub struct BacktestReport {
  folds : Array[BacktestFold]
  mean_mae : Double
  mean_rmse : Double
  mean_bias : Double
  median_rmse : Double
  worst_rmse : Double
  stability : Double
}

///|
/// Configuration for rolling-origin validation.
pub struct BacktestRule {
  minimum_train : Int
  horizon : Int
  step : Int
  confidence : Double
}

///|
pub fn backtest_default_rule() -> BacktestRule {
  { minimum_train: 10, horizon: 3, step: 1, confidence: 0.95 }
}

///|
pub fn backtest_rule(
  minimum_train : Int,
  horizon : Int,
  step : Int,
  confidence : Double,
) -> BacktestRule {
  {
    minimum_train: if minimum_train < 1 {
      1
    } else {
      minimum_train
    },
    horizon: if horizon < 1 {
      1
    } else {
      horizon
    },
    step: if step < 1 {
      1
    } else {
      step
    },
    confidence: if confidence <= 0.0 {
      0.5
    } else if confidence >= 1.0 {
      0.999
    } else {
      confidence
    },
  }
}

///|
pub fn backtest_holdout(
  data : Array[Double],
  origin : Int,
  horizon : Int,
) -> Array[Double] {
  let result = []
  let start = if origin < 0 { 0 } else { origin }
  let end = if start + horizon > data.length() {
    data.length()
  } else {
    start + horizon
  }
  for index = start; index < end; index = index + 1 {
    result.push(data[index])
  }
  result
}

///|
pub fn backtest_prediction(
  data : Array[Double],
  origin : Int,
  strategy : ForecastMethod,
  horizon : Int,
) -> Array[Double] {
  forecast_with_method(forecast_prefix(data, origin), strategy, horizon)
}

///|
pub fn backtest_fold(
  data : Array[Double],
  origin : Int,
  strategy : ForecastMethod,
  horizon : Int,
  confidence : Double,
) -> BacktestFold {
  let actual = backtest_holdout(data, origin, horizon)
  let predicted = backtest_prediction(data, origin, strategy, actual.length())
  let points = forecast_points(
    forecast_prefix(data, origin),
    strategy,
    actual.length(),
    confidence~,
  )
  let lower = []
  let upper = []
  for point in points {
    lower.push(point.lower)
    upper.push(point.upper)
  }
  {
    origin,
    horizon: actual.length(),
    actual,
    predicted,
    mae: forecast_mae(actual, predicted),
    rmse: forecast_rmse(actual, predicted),
    bias: forecast_bias(actual, predicted),
    coverage: forecast_coverage(actual, lower, upper),
  }
}

///|
pub fn backtest(
  data : Array[Double],
  strategy : ForecastMethod,
  rule : BacktestRule,
) -> BacktestReport {
  let folds = []
  let mut origin = rule.minimum_train
  while origin < data.length() {
    let fold = backtest_fold(
      data,
      origin,
      strategy,
      rule.horizon,
      rule.confidence,
    )
    if fold.horizon > 0 {
      folds.push(fold)
    }
    origin += rule.step
  }
  backtest_report(folds)
}

///|
pub fn backtest_report(folds : Array[BacktestFold]) -> BacktestReport {
  let maes = []
  let rmses = []
  let biases = []
  for fold in folds {
    maes.push(fold.mae)
    rmses.push(fold.rmse)
    biases.push(fold.bias)
  }
  let worst = if rmses.length() == 0 { 0.0 } else { max_value(rmses) }
  let average = if rmses.length() == 0 { 0.0 } else { mean(rmses) }
  {
    folds,
    mean_mae: mean(maes),
    mean_rmse: average,
    mean_bias: mean(biases),
    median_rmse: median(rmses),
    worst_rmse: worst,
    stability: if worst == 0.0 {
      1.0
    } else {
      median(rmses) / worst
    },
  }
}

///|
pub fn backtest_report_vector(report : BacktestReport) -> Array[Double] {
  [
    report.folds.length().to_double(),
    report.mean_mae,
    report.mean_rmse,
    report.mean_bias,
    report.median_rmse,
    report.worst_rmse,
    report.stability,
  ]
}

///|
pub fn backtest_report_lines(report : BacktestReport) -> Array[String] {
  [
    "folds=" + report.folds.length().to_string(),
    "mean_mae=" + report.mean_mae.to_string(),
    "mean_rmse=" + report.mean_rmse.to_string(),
    "mean_bias=" + report.mean_bias.to_string(),
    "median_rmse=" + report.median_rmse.to_string(),
    "worst_rmse=" + report.worst_rmse.to_string(),
    "stability=" + report.stability.to_string(),
  ]
}

///|
pub fn backtest_report_string(report : BacktestReport) -> String {
  backtest_report_lines(report).join("\n")
}

///|
pub fn backtest_fold_errors(report : BacktestReport) -> Array[Double] {
  let result = []
  for fold in report.folds {
    result.push(fold.rmse)
  }
  result
}

///|
pub fn backtest_fold_origins(report : BacktestReport) -> Array[Int] {
  let result = []
  for fold in report.folds {
    result.push(fold.origin)
  }
  result
}

///|
pub fn backtest_fold_biases(report : BacktestReport) -> Array[Double] {
  let result = []
  for fold in report.folds {
    result.push(fold.bias)
  }
  result
}

///|
pub fn backtest_coverage(report : BacktestReport) -> Double {
  let values = []
  for fold in report.folds {
    values.push(fold.coverage)
  }
  mean(values)
}

///|
pub fn backtest_method_report(
  data : Array[Double],
  methods : Array[ForecastMethod],
  rule : BacktestRule,
) -> Array[BacktestReport] {
  let result = []
  for strategy in methods {
    result.push(backtest(data, strategy, rule))
  }
  result
}

///|
pub fn backtest_method_errors(
  data : Array[Double],
  methods : Array[ForecastMethod],
  rule : BacktestRule,
) -> Array[Double] {
  let result = []
  for strategy in methods {
    result.push(backtest(data, strategy, rule).mean_rmse)
  }
  result
}

///|
pub fn backtest_best_method(
  data : Array[Double],
  methods : Array[ForecastMethod],
  rule : BacktestRule,
) -> ForecastMethod {
  if methods.length() == 0 {
    return Naive
  }
  let mut best = methods[0]
  let mut score = backtest(data, best, rule).mean_rmse
  for strategy in methods {
    let current = backtest(data, strategy, rule).mean_rmse
    if current < score {
      best = strategy
      score = current
    }
  }
  best
}

///|
pub fn backtest_gain(
  data : Array[Double],
  candidate : ForecastMethod,
  baseline : ForecastMethod,
  rule : BacktestRule,
) -> Double {
  let candidate_score = backtest(data, candidate, rule).mean_rmse
  let baseline_score = backtest(data, baseline, rule).mean_rmse
  if baseline_score == 0.0 {
    0.0
  } else {
    (baseline_score - candidate_score) / baseline_score
  }
}

///|
pub fn backtest_residuals(report : BacktestReport) -> Array[Double] {
  let result = []
  for fold in report.folds {
    for index = 0
        index < fold.actual.length() && index < fold.predicted.length()
        index = index + 1 {
      result.push(fold.actual[index] - fold.predicted[index])
    }
  }
  result
}

///|
pub fn backtest_residual_quality(report : BacktestReport) -> Double {
  robust_signal_quality(backtest_residuals(report))
}

///|
pub fn backtest_residual_bias(report : BacktestReport) -> Double {
  mean(backtest_residuals(report))
}

///|
pub fn backtest_residual_scale(report : BacktestReport) -> Double {
  mad(backtest_residuals(report))
}

///|
pub fn backtest_residual_autocorrelation(
  report : BacktestReport,
  lag : Int,
) -> Double {
  robust_autocorrelation(backtest_residuals(report), lag)
}

///|
pub fn backtest_error_quantiles(report : BacktestReport) -> Array[Double] {
  let errors = backtest_fold_errors(report)
  [quantile(errors, 0.05), quantile(errors, 0.5), quantile(errors, 0.95)]
}

///|
pub fn backtest_error_outliers(
  report : BacktestReport,
  threshold : Double,
) -> Array[Int] {
  outlier_indices_z(backtest_fold_errors(report), threshold~)
}

///|
pub fn backtest_stable(report : BacktestReport, tolerance : Double) -> Bool {
  report.stability >= tolerance
}

///|
pub fn backtest_improvement(
  candidate : BacktestReport,
  baseline : BacktestReport,
) -> Double {
  if baseline.mean_rmse == 0.0 {
    0.0
  } else {
    (baseline.mean_rmse - candidate.mean_rmse) / baseline.mean_rmse
  }
}

///|
pub fn backtest_walk_forward_scores(
  data : Array[Double],
  strategy : ForecastMethod,
  minimum_train : Int,
  horizon : Int,
  step : Int,
) -> Array[Double] {
  backtest_fold_errors(
    backtest(data, strategy, backtest_rule(minimum_train, horizon, step, 0.95)),
  )
}

///|
pub fn backtest_learning_curve(
  data : Array[Double],
  strategy : ForecastMethod,
  rule : BacktestRule,
  points : Int,
) -> Array[Double] {
  let result = []
  let count = if points < 1 { 1 } else { points }
  for index = 1; index <= count; index = index + 1 {
    let size = data.length() * index / count
    if size >= rule.minimum_train {
      result.push(
        backtest(forecast_prefix(data, size), strategy, rule).mean_rmse,
      )
    } else {
      result.push(0.0)
    }
  }
  result
}

///|
pub fn backtest_regime_scores(
  data : Array[Double],
  segments : Int,
  strategy : ForecastMethod,
  rule : BacktestRule,
) -> Array[Double] {
  let result = []
  for values in segment_values(data, segments) {
    result.push(backtest(values, strategy, rule).mean_rmse)
  }
  result
}

///|
pub fn backtest_regime_stability(
  data : Array[Double],
  segments : Int,
  strategy : ForecastMethod,
  rule : BacktestRule,
) -> Double {
  let scores = backtest_regime_scores(data, segments, strategy, rule)
  if scores.length() == 0 {
    0.0
  } else {
    1.0 / (1.0 + mad(scores))
  }
}

///|
pub fn backtest_forecast_quality(
  data : Array[Double],
  strategy : ForecastMethod,
  rule : BacktestRule,
) -> Double {
  let report = backtest(data, strategy, rule)
  1.0 /
  (1.0 + report.mean_rmse + abs_double(report.mean_bias)) *
  report.stability
}