///|
pub(all) struct NumericSummary {
  count : Int
  minimum : Double
  maximum : Double
  mean : Double
  sample_standard_deviation : Double
} derive(Debug, Eq, ToJson)

///|
pub fn summarize(
  values : ArrayView[Double],
) -> NumericSummary raise @domain.ExtractionError {
  if values.length() == 0 {
    raise @domain.ExtractionError::InsufficientData(required=1, actual=0)
  }
  let mut minimum = values[0]
  let mut maximum = values[0]
  let mut mean = 0.0
  let mut sum_squared_deviation = 0.0
  for index, value in values {
    @domain.require_finite("value[\{index}]", value)
    minimum = minimum.min(value)
    maximum = maximum.max(value)
    let count = (index + 1).to_double()
    let delta = value - mean
    mean += delta / count
    let delta_after = value - mean
    sum_squared_deviation += delta * delta_after
  }
  {
    count: values.length(),
    minimum,
    maximum,
    mean,
    sample_standard_deviation: if values.length() > 1 {
      (sum_squared_deviation / (values.length() - 1).to_double()).sqrt()
    } else {
      0.0
    },
  }
}

///|
pub(all) struct ScenarioSummary {
  recovery : NumericSummary
  raffinate_concentration : NumericSummary
  extract_concentration : NumericSummary
  extraction_factor : NumericSummary
  maximum_absolute_balance_residual : Double
} derive(Debug, Eq, ToJson)

///|
pub fn summarize_scenarios(
  results : ArrayView[ScenarioResult],
) -> ScenarioSummary raise @domain.ExtractionError {
  if results.length() == 0 {
    raise @domain.ExtractionError::InsufficientData(required=1, actual=0)
  }
  let recoveries : Array[Double] = []
  let raffinates : Array[Double] = []
  let extracts : Array[Double] = []
  let factors : Array[Double] = []
  let mut max_residual = 0.0
  for item in results {
    recoveries.push(item.result.recovery)
    raffinates.push(item.result.raffinate_concentration)
    extracts.push(item.result.extract_concentration)
    factors.push(item.extraction_factor)
    max_residual = max_residual.max(item.result.balance_residual.abs())
  }
  {
    recovery: summarize(recoveries),
    raffinate_concentration: summarize(raffinates),
    extract_concentration: summarize(extracts),
    extraction_factor: summarize(factors),
    maximum_absolute_balance_residual: max_residual,
  }
}

///|
pub(all) struct BalanceAudit {
  passed : Bool
  tolerance : Double
  checked : Int
  failed_names : Array[String]
  maximum_absolute_residual : Double
} derive(Debug, Eq, ToJson)

///|
pub fn audit_balances(
  results : ArrayView[ScenarioResult],
  tolerance? : Double = 1.0e-10,
) -> BalanceAudit raise @domain.ExtractionError {
  @domain.require_positive("balance tolerance", tolerance)
  if results.length() == 0 {
    raise @domain.ExtractionError::InsufficientData(required=1, actual=0)
  }
  let failed_names : Array[String] = []
  let mut maximum = 0.0
  for item in results {
    let residual = item.result.balance_residual.abs()
    maximum = maximum.max(residual)
    if residual > tolerance {
      failed_names.push(item.name)
    }
  }
  {
    passed: failed_names.length() == 0,
    tolerance,
    checked: results.length(),
    failed_names,
    maximum_absolute_residual: maximum,
  }
}