///|
/// Aggregated metric statistics across multiple runs.
pub struct MetricStats {
  priv key : String
  priv count : Int
  priv min_val : Double
  priv max_val : Double
  priv mean_val : Double
  priv values : Array[Double]
} derive(Debug)

///|
/// Return the metric key.
pub fn MetricStats::key(self : MetricStats) -> String {
  self.key
}

///|
/// Return the number of runs that have this metric.
pub fn MetricStats::count(self : MetricStats) -> Int {
  self.count
}

///|
/// Return the minimum metric value.
pub fn MetricStats::min_val(self : MetricStats) -> Double {
  self.min_val
}

///|
/// Return the maximum metric value.
pub fn MetricStats::max_val(self : MetricStats) -> Double {
  self.max_val
}

///|
/// Return the mean metric value.
pub fn MetricStats::mean_val(self : MetricStats) -> Double {
  self.mean_val
}

///|
/// Return a detached copy of all metric values.
pub fn MetricStats::values(self : MetricStats) -> Array[Double] {
  self.values.copy()
}

///|
/// Return the range (max - min) of metric values.
pub fn MetricStats::range(self : MetricStats) -> Double {
  self.max_val - self.min_val
}

///|
/// Return the variance of metric values.
pub fn MetricStats::variance(self : MetricStats) -> Double {
  if self.count < 2 {
    return 0.0
  }
  let mut sum_sq = 0.0
  for v in self.values {
    let diff = v - self.mean_val
    sum_sq += diff * diff
  }
  sum_sq / (self.count - 1).to_double()
}

///|
/// Return the standard deviation of metric values.
pub fn MetricStats::std_dev(self : MetricStats) -> Double {
  self.variance().sqrt()
}

///|
/// Aggregate metric statistics for a specific metric key across all runs
/// in an experiment.
///
/// Only the latest metric value for each run is considered. Runs without
/// the specified metric are skipped.
pub fn TrackingStore::aggregate_metric(
  self : TrackingStore,
  experiment_id : String,
  metric_key : String,
) -> Result[MetricStats, TrackingError] {
  // Verify experiment exists
  match self.get_experiment(experiment_id) {
    Err(err) => return Err(err)
    Ok(_) => ()
  }
  let runs = self.runs_for_experiment(experiment_id)
  let values : Array[Double] = []
  for run in runs {
    match run.latest_metric(metric_key) {
      Some(m) => values.push(m.value())
      None => ()
    }
  }
  if values.is_empty() {
    return Err(RunNotFound(metric_key))
  }
  let mut min_v = values[0]
  let mut max_v = values[0]
  let mut sum = 0.0
  for v in values {
    if v < min_v {
      min_v = v
    }
    if v > max_v {
      max_v = v
    }
    sum += v
  }
  let count = values.length()
  let mean = sum / count.to_double()
  Ok({
    key: metric_key,
    count,
    min_val: min_v,
    max_val: max_v,
    mean_val: mean,
    values,
  })
}

///|
/// Aggregate metric statistics for all metric keys across all runs in an
/// experiment.
///
/// Returns a map from metric key to `MetricStats`. Only the latest metric
/// value for each run is considered.
pub fn TrackingStore::aggregate_all_metrics(
  self : TrackingStore,
  experiment_id : String,
) -> Result[Array[MetricStats], TrackingError] {
  match self.get_experiment(experiment_id) {
    Err(err) => return Err(err)
    Ok(_) => ()
  }
  let runs = self.runs_for_experiment(experiment_id)
  // Collect all unique metric keys
  let all_keys : Array[String] = []
  for run in runs {
    for m in run.metrics() {
      if !array_contains_string(all_keys, m.key()) {
        all_keys.push(m.key())
      }
    }
  }
  // Aggregate each key
  let stats : Array[MetricStats] = []
  for key in all_keys {
    match self.aggregate_metric(experiment_id, key) {
      Ok(s) => stats.push(s)
      Err(_) => ()
    }
  }
  Ok(stats)
}

///|
/// Find the best run in an experiment by a specific metric.
///
/// "Best" is determined by the metric direction: for `HigherBetter`, the
/// run with the highest metric value; for `LowerBetter`, the run with the
/// lowest metric value. If direction is `None`, the run with the highest
/// value is returned.
pub fn TrackingStore::best_run(
  self : TrackingStore,
  experiment_id : String,
  metric_key : String,
) -> Result[Run, TrackingError] {
  match self.get_experiment(experiment_id) {
    Err(err) => return Err(err)
    Ok(_) => ()
  }
  let runs = self.runs_for_experiment(experiment_id)
  if runs.is_empty() {
    return Err(RunNotFound(experiment_id))
  }
  // Determine direction from the first run that has this metric
  let mut direction : MetricDirection = None_
  for run in runs {
    match run.latest_metric(metric_key) {
      Some(m) => {
        direction = m.direction()
        break
      }
      None => ()
    }
  }
  // Find the best run
  let mut best : Run? = None
  let mut best_val = 0.0
  for run in runs {
    match run.latest_metric(metric_key) {
      Some(m) =>
        match best {
          None => {
            best = Some(run)
            best_val = m.value()
          }
          Some(_) =>
            match direction {
              LowerBetter =>
                if m.value() < best_val {
                  best = Some(run)
                  best_val = m.value()
                }
              _ =>
                if m.value() > best_val {
                  best = Some(run)
                  best_val = m.value()
                }
            }
        }
      None => ()
    }
  }
  match best {
    Some(r) => Ok(r)
    None => Err(RunNotFound(metric_key))
  }
}