///|
/// Search range for deterministic hyperparameter selection.
pub struct OptimizationRange {
  lower : Double
  upper : Double
  steps : Int
}

///|
pub struct OptimizationResult {
  parameter : Double
  score : Double
  baseline : Double
  evaluations : Int
  stable : Bool
}

///|
pub struct ModelSelectionResult {
  name : String
  score : Double
  rank : Int
  diagnostics : Array[Double]
}

///|
pub fn optimization_range(
  lower : Double,
  upper : Double,
  steps : Int,
) -> OptimizationRange {
  { lower, upper, steps: if steps < 1 { 1 } else { steps } }
}

///|
pub fn optimization_grid(range : OptimizationRange) -> Array[Double] {
  let result = []
  if range.steps == 1 {
    result.push(range.lower)
    return result
  }
  for index = 0; index < range.steps; index = index + 1 {
    let fraction = index.to_double() / (range.steps - 1).to_double()
    result.push(range.lower + fraction * (range.upper - range.lower))
  }
  result
}

///|
pub fn optimization_best(
  results : Array[OptimizationResult],
) -> OptimizationResult {
  if results.length() == 0 {
    return {
      parameter: 0.0,
      score: 0.0,
      baseline: 0.0,
      evaluations: 0,
      stable: false,
    }
  }
  let mut best = results[0]
  for result in results {
    if result.score > best.score {
      best = result
    }
  }
  best
}

///|
pub fn optimization_score_gain(result : OptimizationResult) -> Double {
  result.score - result.baseline
}

///|
pub fn optimization_threshold(
  data : Array[Double],
  range : OptimizationRange,
) -> OptimizationResult {
  let grid = optimization_grid(range)
  let baseline = robust_signal_quality(data)
  let results = []
  for threshold in grid {
    let cleaned = transform_outlier_replacement(data, threshold)
    let score = robust_signal_quality(cleaned)
    results.push({
      parameter: threshold,
      score,
      baseline,
      evaluations: 1,
      stable: score >= baseline,
    })
  }
  optimization_best(results)
}

///|
pub fn optimization_trim(
  data : Array[Double],
  range : OptimizationRange,
) -> OptimizationResult {
  let grid = optimization_grid(range)
  let baseline = abs_double(mean(data) - median(data))
  let results = []
  for probability in grid {
    let trimmed = transform_trim(data, probability)
    let score = 1.0 / (1.0 + abs_double(mean(trimmed) - median(trimmed)))
    results.push({
      parameter: probability,
      score,
      baseline,
      evaluations: 1,
      stable: trimmed.length() > 0,
    })
  }
  optimization_best(results)
}

///|
pub fn optimization_window(
  data : Array[Double],
  windows : Array[Int],
  threshold : Double,
) -> OptimizationResult {
  let baseline = streaming_quality_score(data, 2, threshold)
  let results = []
  for window in windows {
    let score = streaming_quality_score(data, window, threshold)
    results.push({
      parameter: window.to_double(),
      score,
      baseline,
      evaluations: 1,
      stable: score >= 0.5,
    })
  }
  optimization_best(results)
}

///|
pub fn optimization_alpha(
  data : Array[Double],
  range : OptimizationRange,
) -> OptimizationResult {
  let baseline = signal_noise_estimate(data)
  let results = []
  for alpha in optimization_grid(range) {
    let residual = streaming_ewma_residuals(data, alpha)
    let score = 1.0 / (1.0 + mad(residual))
    results.push({
      parameter: alpha,
      score,
      baseline: 1.0 / (1.0 + baseline),
      evaluations: 1,
      stable: residual.length() == data.length(),
    })
  }
  optimization_best(results)
}

///|
pub fn optimization_forecast_method(
  data : Array[Double],
  horizon : Int,
) -> Array[ModelSelectionResult] {
  let methods = ["naive", "mean", "median", "drift", "huber"]
  let result = []
  let split = if data.length() < horizon + 2 {
    data.length() / 2
  } else {
    data.length() - horizon
  }
  let train = []
  let holdout = []
  for index = 0; index < split; index = index + 1 {
    train.push(data[index])
  }
  for index = split; index < data.length(); index = index + 1 {
    holdout.push(data[index])
  }
  let predictions = [
    forecast_naive(train, holdout.length()),
    forecast_mean_level(train, holdout.length()),
    forecast_median_level(train, holdout.length()),
    forecast_drift(train, holdout.length()),
    forecast_huber_level(train, holdout.length()),
  ]
  for index = 0; index < methods.length(); index = index + 1 {
    let errors = []
    let prediction = predictions[index]
    let count = if prediction.length() < holdout.length() {
      prediction.length()
    } else {
      holdout.length()
    }
    for cursor = 0; cursor < count; cursor = cursor + 1 {
      errors.push(holdout[cursor] - prediction[cursor])
    }
    result.push({
      name: methods[index],
      score: -mean_absolute_error(holdout, prediction),
      rank: 0,
      diagnostics: [
        mean(errors),
        sample_stddev(errors),
        errors.length().to_double(),
      ],
    })
  }
  for left = 0; left < result.length(); left = left + 1 {
    let mut rank = 0
    for right = 0; right < result.length(); right = right + 1 {
      if result[right].score > result[left].score {
        rank += 1
      }
    }
    result[left] = { ..result[left], rank, }
  }
  result
}

///|
pub fn optimization_best_forecast_method(
  data : Array[Double],
  horizon : Int,
) -> String {
  let results = optimization_forecast_method(data, horizon)
  if results.length() == 0 {
    "none"
  } else {
    let mut best = results[0]
    for result in results {
      if result.score > best.score {
        best = result
      }
    }
    best.name
  }
}

///|
pub fn optimization_model_scores(
  data : Array[Double],
  horizon : Int,
) -> Array[Double] {
  let result = []
  for item in optimization_forecast_method(data, horizon) {
    result.push(item.score)
  }
  result
}

///|
pub fn optimization_ensemble_weights(scores : Array[Double]) -> Array[Double] {
  let result = []
  let mut total = 0.0
  for score in scores {
    let weight = if score < 0.0 { 0.0 } else { score }
    result.push(weight)
    total += weight
  }
  if total == 0.0 {
    return result
  }
  for index = 0; index < result.length(); index = index + 1 {
    result[index] = result[index] / total
  }
  result
}

///|
pub fn optimization_weighted_forecast(
  predictions : Array[Array[Double]],
  weights : Array[Double],
) -> Array[Double] {
  let result = []
  if predictions.length() == 0 {
    return result
  }
  let count = predictions[0].length()
  let normalized = optimization_ensemble_weights(weights)
  for index = 0; index < count; index = index + 1 {
    let mut value = 0.0
    for model = 0; model < predictions.length(); model = model + 1 {
      let weight = if model < normalized.length() {
        normalized[model]
      } else {
        0.0
      }
      if index < predictions[model].length() {
        value += weight * predictions[model][index]
      }
    }
    result.push(value)
  }
  result
}

///|
pub fn optimization_forecast_ensemble(
  data : Array[Double],
  horizon : Int,
) -> Array[Double] {
  let predictions = [
    forecast_naive(data, horizon),
    forecast_mean_level(data, horizon),
    forecast_median_level(data, horizon),
    forecast_drift(data, horizon),
    forecast_huber_level(data, horizon),
  ]
  optimization_weighted_forecast(predictions, [1.0, 1.0, 1.0, 1.0, 1.0])
}

///|
pub fn optimization_cluster_count(
  data : Array[Double],
  candidates : Array[Int],
) -> OptimizationResult {
  let baseline = if data.length() == 0 {
    0.0
  } else {
    cluster_stability(data, 2)
  }
  let results = []
  for count in candidates {
    let safe_count = if count < 2 { 2 } else { count }
    let score = cluster_stability(data, safe_count)
    results.push({
      parameter: safe_count.to_double(),
      score,
      baseline,
      evaluations: 1,
      stable: score >= 0.5,
    })
  }
  optimization_best(results)
}

///|
pub fn optimization_drift_threshold(
  baseline : Array[Double],
  current : Array[Double],
  range : OptimizationRange,
) -> OptimizationResult {
  let grid = optimization_grid(range)
  let base = drift_report(baseline, current, drift_default_rule()).drift_score
  let results = []
  for threshold in grid {
    let rule = drift_rule(threshold, threshold, threshold, threshold, 10)
    let report = drift_report(baseline, current, rule)
    let score = if report.drifted { 0.0 } else { 1.0 }
    results.push({
      parameter: threshold,
      score,
      baseline: 1.0 - base,
      evaluations: 1,
      stable: !report.drifted,
    })
  }
  optimization_best(results)
}

///|
pub fn optimization_risk_confidence(
  returns : Array[Double],
  range : OptimizationRange,
) -> OptimizationResult {
  let baseline = risk_snapshot(returns, risk_default_rule()).sharpe
  let results = []
  for confidence in optimization_grid(range) {
    let rule = risk_rule(confidence, 0.0, 0.0, 1.0)
    let snapshot = risk_snapshot(returns, rule)
    results.push({
      parameter: confidence,
      score: snapshot.sharpe,
      baseline,
      evaluations: 1,
      stable: snapshot.count == returns.length(),
    })
  }
  optimization_best(results)
}

///|
pub fn optimization_stability(
  results : Array[OptimizationResult],
  tolerance : Double,
) -> Bool {
  if results.length() < 2 {
    return true
  }
  let best = optimization_best(results)
  for result in results {
    if abs_double(result.score - best.score) > tolerance {
      return false
    }
  }
  true
}

///|
pub fn optimization_result_vector(result : OptimizationResult) -> Array[Double] {
  [
    result.parameter,
    result.score,
    result.baseline,
    result.evaluations.to_double(),
    if result.stable {
      1.0
    } else {
      0.0
    },
    optimization_score_gain(result),
  ]
}

///|
pub fn optimization_result_lines(result : OptimizationResult) -> Array[String] {
  [
    "parameter=" + result.parameter.to_string(),
    "score=" + result.score.to_string(),
    "baseline=" + result.baseline.to_string(),
    "evaluations=" + result.evaluations.to_string(),
    "stable=" + result.stable.to_string(),
    "gain=" + optimization_score_gain(result).to_string(),
  ]
}

///|
pub fn optimization_result_string(result : OptimizationResult) -> String {
  optimization_result_lines(result).join("\n")
}