///|
/// Cross-model validation record with uncertainty and stability diagnostics.
pub struct ModelValidation {
  name : String
  folds : Int
  score : Double
  standard_error : Double
  coverage : Double
  stability : Double
  selected : Bool
}

///|
pub struct ValidationLeaderboard {
  models : Array[ModelValidation]
  winner : String
  margin : Double
  confidence : Double
}

///|
pub fn model_validation(
  name : String,
  scores : Array[Double],
  coverage : Double,
) -> ModelValidation {
  let center = mean(scores)
  let error = if scores.length() < 2 {
    0.0
  } else {
    sample_stddev(scores) / scores.length().to_double().sqrt()
  }
  let stability = if center == 0.0 {
    1.0
  } else {
    1.0 / (1.0 + error / abs_double(center))
  }
  {
    name,
    folds: scores.length(),
    score: center,
    standard_error: error,
    coverage: transform_clip(coverage, 0.0, 1.0),
    stability,
    selected: false,
  }
}

///|
pub fn model_validation_from_backtest(
  name : String,
  report : BacktestReport,
) -> ModelValidation {
  let scores = []
  for error in backtest_fold_errors(report) {
    scores.push(1.0 / (1.0 + abs_double(error)))
  }
  model_validation(name, scores, backtest_coverage(report))
}

///|
pub fn model_validation_rank(
  models : Array[ModelValidation],
) -> Array[ModelValidation] {
  let result = models.copy()
  result.sort_by((left, right) => {
    if left.score > right.score {
      -1
    } else if left.score < right.score {
      1
    } else {
      0
    }
  })
  for index = 0; index < result.length(); index = index + 1 {
    result[index] = { ..result[index], selected: index == 0 }
  }
  result
}

///|
pub fn model_validation_leaderboard(
  models : Array[ModelValidation],
) -> ValidationLeaderboard {
  let ranked = model_validation_rank(models)
  if ranked.length() == 0 {
    return { models: [], winner: "none", margin: 0.0, confidence: 0.0 }
  }
  let margin = if ranked.length() < 2 {
    ranked[0].score
  } else {
    ranked[0].score - ranked[1].score
  }
  let confidence = transform_clip(
    ranked[0].stability * (0.5 + margin),
    0.0,
    1.0,
  )
  { models: ranked, winner: ranked[0].name, margin, confidence }
}

///|
pub fn model_validation_scores(
  leaderboard : ValidationLeaderboard,
) -> Array[Double] {
  let result = []
  for model in leaderboard.models {
    result.push(model.score)
  }
  result
}

///|
pub fn model_validation_names(
  leaderboard : ValidationLeaderboard,
) -> Array[String] {
  let result = []
  for model in leaderboard.models {
    result.push(model.name)
  }
  result
}

///|
pub fn model_validation_margin(leaderboard : ValidationLeaderboard) -> Double {
  leaderboard.margin
}

///|
pub fn model_validation_reliable(
  model : ModelValidation,
  score_threshold : Double,
  coverage_threshold : Double,
) -> Bool {
  model.score >= score_threshold &&
  model.coverage >= coverage_threshold &&
  model.stability >= 0.5
}

///|
pub fn model_validation_compare(
  left : ModelValidation,
  right : ModelValidation,
) -> Array[Double] {
  [
    left.score,
    right.score,
    left.standard_error,
    right.standard_error,
    left.coverage,
    right.coverage,
    left.stability,
    right.stability,
  ]
}

///|
pub fn model_validation_lines(model : ModelValidation) -> Array[String] {
  [
    "name=" + model.name,
    "folds=" + model.folds.to_string(),
    "score=" + model.score.to_string(),
    "standard_error=" + model.standard_error.to_string(),
    "coverage=" + model.coverage.to_string(),
    "stability=" + model.stability.to_string(),
    "selected=" + model.selected.to_string(),
  ]
}

///|
pub fn model_validation_string(model : ModelValidation) -> String {
  model_validation_lines(model).join("\n")
}

///|
pub fn leaderboard_lines(leaderboard : ValidationLeaderboard) -> Array[String] {
  let lines = [
    "winner=" + leaderboard.winner,
    "margin=" + leaderboard.margin.to_string(),
    "confidence=" + leaderboard.confidence.to_string(),
  ]
  for index = 0; index < leaderboard.models.length(); index = index + 1 {
    let model = leaderboard.models[index]
    lines.push(
      index.to_string() +
      "|" +
      model.name +
      "|" +
      model.score.to_string() +
      "|" +
      model.coverage.to_string() +
      "|" +
      model.selected.to_string(),
    )
  }
  lines
}

///|
pub fn leaderboard_string(leaderboard : ValidationLeaderboard) -> String {
  leaderboard_lines(leaderboard).join("\n")
}

///|
pub fn model_validation_residual_score(
  actual : Array[Double],
  predicted : Array[Double],
) -> Double {
  if actual.length() != predicted.length() || actual.length() == 0 {
    return 0.0
  }
  let error = mean_absolute_error(actual, predicted)
  1.0 / (1.0 + error)
}

///|
pub fn model_validation_rmse_score(
  actual : Array[Double],
  predicted : Array[Double],
) -> Double {
  if actual.length() != predicted.length() || actual.length() == 0 {
    return 0.0
  }
  1.0 / (1.0 + mean_squared_error(actual, predicted).sqrt())
}

///|
pub fn model_validation_bias_score(
  actual : Array[Double],
  predicted : Array[Double],
) -> Double {
  if actual.length() != predicted.length() || actual.length() == 0 {
    return 0.0
  }
  let residuals = []
  for index = 0; index < actual.length(); index = index + 1 {
    residuals.push(actual[index] - predicted[index])
  }
  1.0 / (1.0 + abs_double(mean(residuals)))
}

///|
pub fn model_validation_score_vector(
  actual : Array[Double],
  predicted : Array[Double],
) -> Array[Double] {
  [
    model_validation_residual_score(actual, predicted),
    model_validation_rmse_score(actual, predicted),
    model_validation_bias_score(actual, predicted),
    coverage_of_interval(actual, [min_value(predicted), max_value(predicted)]),
  ]
}

///|
pub fn model_validation_ensemble(
  actual : Array[Double],
  predictions : Array[Array[Double]],
) -> Array[ModelValidation] {
  let result = []
  for index = 0; index < predictions.length(); index = index + 1 {
    let scores = model_validation_score_vector(actual, predictions[index])
    result.push(
      model_validation("model-" + index.to_string(), scores, scores[0]),
    )
  }
  result
}

///|
pub fn model_validation_select(
  actual : Array[Double],
  predictions : Array[Array[Double]],
) -> Int {
  let models = model_validation_ensemble(actual, predictions)
  if models.length() == 0 {
    return -1
  }
  let leaderboard = model_validation_leaderboard(models)
  if leaderboard.models.length() == 0 {
    return -1
  }
  for index = 0; index < models.length(); index = index + 1 {
    if models[index].name == leaderboard.winner {
      return index
    }
  }
  -1
}

///|
pub fn model_validation_learning_curve(
  actual : Array[Double],
  predictions : Array[Array[Double]],
) -> Array[Double] {
  let result = []
  for prediction in predictions {
    result.push(model_validation_residual_score(actual, prediction))
  }
  result
}

///|
pub fn model_validation_stable(
  scores : Array[Double],
  tolerance : Double,
) -> Bool {
  if scores.length() == 0 {
    return true
  }
  let center = mean(scores)
  for score in scores {
    if abs_double(score - center) > tolerance {
      return false
    }
  }
  true
}

///|
pub fn model_validation_summary(
  leaderboard : ValidationLeaderboard,
) -> Array[Double] {
  [
    leaderboard.models.length().to_double(),
    leaderboard.margin,
    leaderboard.confidence,
    if leaderboard.winner == "none" {
      0.0
    } else {
      1.0
    },
  ]
}