///|
/// Ensemble weighting, stacking, and disagreement diagnostics.
pub enum EnsembleMode {
  Mean
  Median
  Weighted
  Trimmed
  Voting
} derive(Debug, Eq, ToJson, FromJson)

///|
pub struct EnsembleMember {
  name : String
  predictions : Array[Double]
  weight : Double
  validation_loss : Double
  active : Bool
} derive(Debug, Eq, ToJson, FromJson)

///|
pub fn EnsembleMember::new(
  name : String,
  predictions : Array[Double],
) -> EnsembleMember {
  { name, predictions, weight: 1.0, validation_loss: 0.0, active: true }
}

///|
pub fn EnsembleMember::with_loss(
  self : EnsembleMember,
  loss : Double,
) -> EnsembleMember {
  { ..self, validation_loss: loss.max(0.0) }
}

///|
pub fn EnsembleMember::with_weight(
  self : EnsembleMember,
  weight : Double,
) -> EnsembleMember {
  if weight < 0.0 {
    abort("EnsembleMember: weight must be non-negative")
  }
  { ..self, weight, }
}

///|
pub fn EnsembleMember::disabled(self : EnsembleMember) -> EnsembleMember {
  { ..self, active: false }
}

///|
pub struct EnsemblePrediction {
  values : Array[Double]
  lower : Array[Double]
  upper : Array[Double]
  member_count : Int
  disagreement : Array[Double]
} derive(Debug, Eq, ToJson, FromJson)

///|
pub fn combine_predictions(
  members : Array[EnsembleMember],
  mode : EnsembleMode,
) -> EnsemblePrediction {
  let active = members.filter(fn(model_member) { model_member.active })
  if active.length() == 0 {
    abort("combine_predictions: no active members")
  }
  let points = active[0].predictions.length()
  for model_member in active.iter() {
    if model_member.predictions.length() != points {
      abort("combine_predictions: prediction lengths differ")
    }
  }
  let values : Array[Double] = []
  let lower : Array[Double] = []
  let upper : Array[Double] = []
  let disagreement : Array[Double] = []
  for point in 0..
        weighted_point(active, point)
      EnsembleMode::Median => empirical_quantile(point_values, 0.5)
      EnsembleMode::Trimmed => trimmed_mean(point_values, 0.2)
      EnsembleMode::Voting =>
        if point_values.filter(fn(value) { value >= 0.5 }).length() * 2 >=
          point_values.length() {
          1.0
        } else {
          0.0
        }
    }
    values.push(center)
    lower.push(empirical_quantile(point_values, 0.1))
    upper.push(empirical_quantile(point_values, 0.9))
    disagreement.push(variance(point_values).sqrt())
  }
  { values, lower, upper, member_count: active.length(), disagreement }
}

///|
fn weighted_point(members : Array[EnsembleMember], point : Int) -> Double {
  let mut total = 0.0
  let mut weight = 0.0
  for model_member in members.iter() {
    total = total + model_member.predictions[point] * model_member.weight
    weight = weight + model_member.weight
  }
  if weight == 0.0 {
    0.0
  } else {
    total / weight
  }
}

///|
fn trimmed_mean(values : Array[Double], fraction : Double) -> Double {
  if values.length() == 0 {
    return 0.0
  }
  let sorted = values.copy()
  for left in 0.. WeightFit {
  if features.length() != target.length() ||
    features.length() == 0 ||
    iterations <= 0 ||
    learning_rate <= 0.0 {
    abort("fit_linear_stacker: invalid inputs")
  }
  let columns = features[0].length()
  let weights : Array[Double] = []
  for _ in 0.. Double {
  let active = members.filter(fn(model_member) { model_member.active })
  let total_weight = active.fold(init=0.0, (total, model_member) => {
    total + model_member.weight
  })
  if total_weight == 0.0 {
    0.0
  } else {
    active.fold(init=0.0, (total, model_member) => {
      total + model_member.weight * model_member.validation_loss
    }) /
    total_weight
  }
}

///|
pub fn inverse_loss_weights(
  members : Array[EnsembleMember],
  floor : Double,
) -> Array[EnsembleMember] {
  if floor <= 0.0 {
    abort("inverse_loss_weights: floor must be positive")
  }
  let mut total = 0.0
  let raw = members.map(fn(model_member) {
    1.0 / model_member.validation_loss.max(floor)
  })
  for weight in raw.iter() {
    total = total + weight
  }
  members.map(fn(model_member) {
    let mut index = 0
    for candidate in members.iter() {
      if candidate.name == model_member.name {
        break
      }
      index = index + 1
    }
    { ..model_member, weight: raw[index] / total }
  })
}

///|
pub fn ensemble_bias(
  predicted : Array[Double],
  actual : Array[Double],
) -> Double {
  if predicted.length() != actual.length() || predicted.length() == 0 {
    abort("ensemble_bias: lengths must match")
  }
  let mut total = 0.0
  for index in 0.. Double {
  if predicted.length() != actual.length() || predicted.length() == 0 {
    abort("ensemble_rmse: lengths must match")
  }
  let mut total = 0.0
  for index in 0.. DisagreementReport {
  let high : Array[Int] = []
  for index in 0.. threshold {
      high.push(index)
    }
  }
  {
    mean_disagreement: if disagreement.length() == 0 {
      0.0
    } else {
      mean(disagreement)
    },
    maximum_disagreement: if disagreement.length() == 0 {
      0.0
    } else {
      disagreement.fold(init=disagreement[0], (current, value) => {
        current.max(value)
      })
    },
    high_disagreement_points: high,
    threshold,
  }
}

///|
pub fn ensemble_rank(members : Array[EnsembleMember]) -> Array[String] {
  let ordered = members.copy()
  for left in 0.. Array[Array[Double]] {
  let result : Array[Array[Double]] = []
  for left in members.iter() {
    let row : Array[Double] = []
    for right in members.iter() {
      row.push(mean_squared_difference(left.predictions, right.predictions))
    }
    result.push(row)
  }
  result
}

///|
fn mean_squared_difference(
  left : Array[Double],
  right : Array[Double],
) -> Double {
  if left.length() != right.length() || left.length() == 0 {
    return 0.0
  }
  let mut total = 0.0
  for index in 0.. Array[EnsembleMember] {
  if limit <= 0 {
    return []
  }
  let ordered = ensemble_rank(members)
  let result : Array[EnsembleMember] = []
  for name in ordered.iter() {
    if result.length() < limit {
      for model_member in members.iter() {
        if model_member.name == name {
          result.push(model_member)
        }
      }
    }
  }
  result
}

///|
pub struct StackingReport {
  member_count : Int
  training_loss : Double
  weighted_loss : Double
  diversity : Double
  selected : Array[String]
} derive(Debug, Eq, ToJson, FromJson)

///|
pub fn stacking_report(
  members : Array[EnsembleMember],
  prediction : EnsemblePrediction,
) -> StackingReport {
  let diversity = mean(prediction.disagreement)
  {
    member_count: members.length(),
    training_loss: weighted_loss(members),
    weighted_loss: weighted_loss(members),
    diversity,
    selected: ensemble_rank(members),
  }
}