///|
/// Robust design helpers for selecting operating points under scenarios.
pub(all) struct ScenarioEvaluation {
  scenario : String
  candidate : DesignCandidate
  objective : Double
  feasible : Bool
  constraint_margin : Double
} derive(Debug, Eq, ToJson)

///|
pub(all) struct RobustCandidate {
  candidate : DesignCandidate
  worst_objective : Double
  mean_objective : Double
  best_objective : Double
  worst_margin : Double
  feasible_scenarios : Int
  total_scenarios : Int
  robust : Bool
} derive(Debug, Eq, ToJson)

///|
pub(all) struct RobustRanking {
  candidates : Array[RobustCandidate]
  selected_index : Int
  risk_tolerance : Double
  confidence : Double
} derive(Debug, Eq, ToJson)

///|
pub fn evaluate_candidate_scenarios(
  candidate : DesignCandidate,
  scenarios : ArrayView[String],
  objective_values : ArrayView[Double],
  margins : ArrayView[Double],
  minimum_margin : Double,
) -> Array[ScenarioEvaluation] raise @domain.ExtractionError {
  if scenarios.length() != objective_values.length() ||
    scenarios.length() != margins.length() ||
    scenarios.length() == 0 {
    raise @domain.ExtractionError::InvalidArgument(
      message="scenario evaluation arrays are invalid",
    )
  }
  let result = []
  for index in 0..= minimum_margin,
      constraint_margin: margins[index],
    })
  }
  result
}

///|
pub fn summarize_robust_candidate(
  candidate : DesignCandidate,
  evaluations : ArrayView[ScenarioEvaluation],
  risk_tolerance : Double,
) -> RobustCandidate raise @domain.ExtractionError {
  if evaluations.length() == 0 || risk_tolerance < 0.0 || risk_tolerance > 1.0 {
    raise @domain.ExtractionError::InvalidArgument(
      message="robust-candidate inputs are invalid",
    )
  }
  let mut worst = evaluations[0].objective
  let mut best = evaluations[0].objective
  let mut total = 0.0
  let mut margin = evaluations[0].constraint_margin
  let mut feasible = 0
  for evaluation in evaluations {
    worst = worst.max(evaluation.objective)
    best = best.min(evaluation.objective)
    total += evaluation.objective
    margin = margin.min(evaluation.constraint_margin)
    if evaluation.feasible {
      feasible += 1
    }
  }
  let confidence = feasible.to_double() / evaluations.length().to_double()
  {
    candidate,
    worst_objective: worst,
    mean_objective: total / evaluations.length().to_double(),
    best_objective: best,
    worst_margin: margin,
    feasible_scenarios: feasible,
    total_scenarios: evaluations.length(),
    robust: confidence >= 1.0 - risk_tolerance,
  }
}

///|
pub fn robust_score(
  candidate : RobustCandidate,
  risk_weight : Double,
  feasibility_weight : Double,
) -> Double {
  let risk = candidate.worst_objective - candidate.best_objective
  let feasibility = 1.0 -
    candidate.feasible_scenarios.to_double() /
    candidate.total_scenarios.to_double()
  candidate.mean_objective +
  risk_weight * risk +
  feasibility_weight * feasibility
}

///|
pub fn rank_robust_candidates(
  candidates : ArrayView[RobustCandidate],
  risk_weight : Double,
  feasibility_weight : Double,
  risk_tolerance : Double,
) -> RobustRanking raise @domain.ExtractionError {
  if candidates.length() == 0 {
    raise @domain.ExtractionError::InsufficientData(required=1, actual=0)
  }
  let scores = [
    for candidate in candidates => {
      robust_score(candidate, risk_weight, feasibility_weight)
    }
  ]
  let mut selected = 0
  let mut best_score = scores[0]
  for index in 1.. Double {
  if objective_values.length() == 0 {
    return 0.0
  }
  let mut result = objective_values[0]
  for value in objective_values {
    result = result.max(value)
  }
  result
}

///|
pub fn best_case_objective(objective_values : ArrayView[Double]) -> Double {
  if objective_values.length() == 0 {
    return 0.0
  }
  let mut result = objective_values[0]
  for value in objective_values {
    result = result.min(value)
  }
  result
}

///|
pub fn scenario_regret(
  objective_values : ArrayView[Double],
  minimize : Bool,
) -> Array[Double] {
  if objective_values.length() == 0 {
    return []
  }
  let reference = if minimize {
    best_case_objective(objective_values)
  } else {
    worst_case_objective(objective_values)
  }
  [
    for value in objective_values => {
      if minimize {
        value - reference
      } else {
        reference - value
      }
    }
  ]
}

///|
pub fn regret_summary(
  objective_values : ArrayView[Double],
  minimize : Bool,
) -> (Double, Double, Double) {
  let regrets = scenario_regret(objective_values, minimize)
  if regrets.length() == 0 {
    return (0.0, 0.0, 0.0)
  }
  let mut total = 0.0
  let mut maximum = 0.0
  for regret in regrets {
    total += regret
    maximum = maximum.max(regret)
  }
  (total / regrets.length().to_double(), maximum, regrets[regrets.length() - 1])
}

///|
pub fn chance_constraint(
  values : ArrayView[Double],
  limit : Double,
  probability : Double,
  lower_is_better : Bool,
) -> Bool raise @domain.ExtractionError {
  if values.length() == 0 || probability < 0.0 || probability > 1.0 {
    raise @domain.ExtractionError::InvalidArgument(
      message="chance constraint inputs are invalid",
    )
  }
  let mut satisfied = 0
  for value in values {
    if (if lower_is_better { value <= limit } else { value >= limit }) {
      satisfied += 1
    }
  }
  satisfied.to_double() / values.length().to_double() >= probability
}

///|
pub fn safety_factor(
  nominal : Double,
  limit : Double,
  lower_is_better : Bool,
) -> Double {
  if nominal == 0.0 {
    return 0.0
  }
  if lower_is_better {
    limit / nominal
  } else {
    nominal / limit
  }
}

///|
pub fn robustness_index(candidate : RobustCandidate) -> Double {
  let feasibility = candidate.feasible_scenarios.to_double() /
    candidate.total_scenarios.to_double()
  let margin = candidate.worst_margin.max(0.0)
  feasibility * (1.0 + margin)
}

///|
pub fn robust_candidates(
  candidates : ArrayView[RobustCandidate],
) -> Array[RobustCandidate] {
  let selected = []
  for candidate in candidates {
    if candidate.robust {
      selected.push(candidate)
    }
  }
  selected
}

///|
pub fn robust_design_markdown(ranking : RobustRanking) -> String {
  let lines = [
    "| Rank | Objective mean | Worst | Best | Worst margin | Feasible scenarios | Robust |",
    "|---:|---:|---:|---:|---:|---:|---|",
  ]
  let indexes = [ for index in 0.. index ]
  for index in indexes {
    let candidate = ranking.candidates[index]
    lines.push(
      "| " +
      index.to_string() +
      " | " +
      candidate.mean_objective.to_string() +
      " | " +
      candidate.worst_objective.to_string() +
      " | " +
      candidate.best_objective.to_string() +
      " | " +
      candidate.worst_margin.to_string() +
      " | " +
      candidate.feasible_scenarios.to_string() +
      "/" +
      candidate.total_scenarios.to_string() +
      " | " +
      candidate.robust.to_string() +
      " |",
    )
  }
  lines.join("\n")
}

///|
pub fn robust_design_csv(ranking : RobustRanking) -> String {
  let lines = [
    "index,mean_objective,worst_objective,best_objective,worst_margin,feasible,total,robust",
  ]
  for index in 0.. Double raise @domain.ExtractionError {
  if values.length() != weights.length() || values.length() == 0 {
    raise @domain.ExtractionError::InvalidArgument(
      message="weighted mean arrays are invalid",
    )
  }
  let mut total = 0.0
  let mut weight_total = 0.0
  for index in 0.. Double raise @domain.ExtractionError {
  if values.length() != weights.length() ||
    values.length() == 0 ||
    probability < 0.0 ||
    probability > 1.0 {
    raise @domain.ExtractionError::InvalidArgument(
      message="weighted quantile inputs are invalid",
    )
  }
  let pairs = []
  for index in 0.. {
    if first.0 < second.0 {
      -1
    } else if first.0 > second.0 {
      1
    } else {
      0
    }
  })
  let total = pairs.fold(init=0.0, (sum, pair) => sum + pair.1)
  let target = total * probability
  let mut cumulative = 0.0
  for pair in pairs {
    cumulative += pair.1
    if cumulative >= target {
      return pair.0
    }
  }
  pairs[pairs.length() - 1].0
}