///|
/// Robust ensemble prediction with validation-aware model weights.
pub struct EnsembleMember {
  name : String
  prediction : Array[Double]
  score : Double
  weight : Double
}

///|
pub struct EnsembleResult {
  prediction : Array[Double]
  members : Array[EnsembleMember]
  diversity : Double
  confidence : Double
}

///|
pub fn ensemble_member(
  name : String,
  prediction : Array[Double],
  score : Double,
) -> EnsembleMember {
  { name, prediction, score, weight: 0.0 }
}

///|
pub fn ensemble_members(
  predictions : Array[Array[Double]],
  scores : Array[Double],
) -> Array[EnsembleMember] {
  let result = []
  for index = 0; index < predictions.length(); index = index + 1 {
    let score = if index < scores.length() { scores[index] } else { 0.0 }
    result.push(
      ensemble_member("model-" + index.to_string(), predictions[index], score),
    )
  }
  let mut total = 0.0
  for entry in result {
    total += if entry.score < 0.0 { 0.0 } else { entry.score }
  }
  for index = 0; index < result.length(); index = index + 1 {
    let score = if result[index].score < 0.0 {
      0.0
    } else {
      result[index].score
    }
    result[index] = {
      ..result[index],
      weight: if total == 0.0 {
        1.0 / result.length().to_double()
      } else {
        score / total
      },
    }
  }
  result
}

///|
pub fn ensemble_predict(members : Array[EnsembleMember]) -> Array[Double] {
  let result = []
  if members.length() == 0 {
    return result
  }
  let count = members[0].prediction.length()
  for index = 0; index < count; index = index + 1 {
    let mut value = 0.0
    for entry in members {
      if index < entry.prediction.length() {
        value += entry.weight * entry.prediction[index]
      }
    }
    result.push(value)
  }
  result
}

///|
pub fn ensemble_diversity(members : Array[EnsembleMember]) -> Double {
  if members.length() < 2 {
    return 0.0
  }
  let mut total = 0.0
  let mut pairs = 0
  for left = 0; left < members.length(); left = left + 1 {
    for right = left + 1; right < members.length(); right = right + 1 {
      let count = if members[left].prediction.length() <
        members[right].prediction.length() {
        members[left].prediction.length()
      } else {
        members[right].prediction.length()
      }
      let mut distance = 0.0
      for index = 0; index < count; index = index + 1 {
        distance += abs_double(
          members[left].prediction[index] - members[right].prediction[index],
        )
      }
      if count > 0 {
        total += distance / count.to_double()
        pairs += 1
      }
    }
  }
  if pairs == 0 {
    0.0
  } else {
    total / pairs.to_double()
  }
}

///|
pub fn ensemble_confidence(members : Array[EnsembleMember]) -> Double {
  if members.length() == 0 {
    0.0
  } else {
    transform_clip(1.0 - ensemble_diversity(members), 0.0, 1.0)
  }
}

///|
pub fn ensemble_fit(
  actual : Array[Double],
  predictions : Array[Array[Double]],
) -> EnsembleResult {
  let scores = []
  for prediction in predictions {
    scores.push(1.0 / (1.0 + mean_absolute_error(actual, prediction)))
  }
  let members = ensemble_members(predictions, scores)
  {
    prediction: ensemble_predict(members),
    members,
    diversity: ensemble_diversity(members),
    confidence: ensemble_confidence(members),
  }
}

///|
pub fn ensemble_fit_forecasts(
  data : Array[Double],
  horizon : Int,
) -> EnsembleResult {
  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),
  ]
  let actual = if data.length() < horizon {
    data.copy()
  } else {
    forecast_prefix(data, data.length() - horizon)
  }
  ensemble_fit(actual, predictions)
}

///|
pub fn ensemble_weights(members : Array[EnsembleMember]) -> Array[Double] {
  let result = []
  for entry in members {
    result.push(entry.weight)
  }
  result
}

///|
pub fn ensemble_names(members : Array[EnsembleMember]) -> Array[String] {
  let result = []
  for entry in members {
    result.push(entry.name)
  }
  result
}

///|
pub fn ensemble_score(members : Array[EnsembleMember]) -> Double {
  let mut total = 0.0
  for entry in members {
    total += entry.score * entry.weight
  }
  total
}

///|
pub fn ensemble_result_vector(result : EnsembleResult) -> Array[Double] {
  [
    result.prediction.length().to_double(),
    result.members.length().to_double(),
    result.diversity,
    result.confidence,
    ensemble_score(result.members),
  ]
}

///|
pub fn ensemble_result_lines(result : EnsembleResult) -> Array[String] {
  let lines = [
    "prediction_count=" + result.prediction.length().to_string(),
    "members=" + result.members.length().to_string(),
    "diversity=" + result.diversity.to_string(),
    "confidence=" + result.confidence.to_string(),
  ]
  for entry in result.members {
    lines.push(
      entry.name +
      "|score=" +
      entry.score.to_string() +
      "|weight=" +
      entry.weight.to_string(),
    )
  }
  lines
}

///|
pub fn ensemble_result_string(result : EnsembleResult) -> String {
  ensemble_result_lines(result).join("\n")
}

///|
pub fn ensemble_stable(
  left : EnsembleResult,
  right : EnsembleResult,
  tolerance : Double,
) -> Bool {
  if left.prediction.length() != right.prediction.length() {
    return false
  }
  for index = 0; index < left.prediction.length(); index = index + 1 {
    if abs_double(left.prediction[index] - right.prediction[index]) > tolerance {
      return false
    }
  }
  true
}

///|
pub fn ensemble_residual_quality(
  actual : Array[Double],
  result : EnsembleResult,
) -> Double {
  1.0 / (1.0 + mean_absolute_error(actual, result.prediction))
}

///|
pub fn ensemble_compare(
  left : EnsembleResult,
  right : EnsembleResult,
) -> Array[Double] {
  [
    left.diversity,
    right.diversity,
    left.confidence,
    right.confidence,
    left.prediction.length().to_double(),
    right.prediction.length().to_double(),
  ]
}

///|
pub fn ensemble_summary(
  actual : Array[Double],
  predictions : Array[Array[Double]],
) -> Array[Double] {
  let result = ensemble_fit(actual, predictions)
  [
    ensemble_residual_quality(actual, result),
    result.diversity,
    result.confidence,
    ensemble_score(result.members),
  ]
}