///|
/// Probability-of-success and risk reporting primitives for design studies.
pub(all) struct RiskObservation {
  scenario : String
  outcome : Double
  threshold : Double
  weight : Double
  pass : Bool
} derive(Debug, Eq, ToJson)

///|
pub(all) struct RiskSummary {
  observations : Int
  weighted_pass_probability : Double
  mean_outcome : Double
  lower_outcome : Double
  upper_outcome : Double
  expected_shortfall : Double
  worst_scenario : String
  risk_level : String
} derive(Debug, Eq, ToJson)

///|
pub(all) struct ReliabilityCurvePoint {
  threshold : Double
  pass_probability : Double
  failure_probability : Double
  sample_count : Int
} derive(Debug, Eq, ToJson)

///|
pub fn make_risk_observation(
  scenario : String,
  outcome : Double,
  threshold : Double,
  weight : Double,
  higher_is_better : Bool,
) -> RiskObservation raise @domain.ExtractionError {
  if weight < 0.0 {
    raise @domain.ExtractionError::InvalidArgument(
      message="risk weight cannot be negative",
    )
  }
  {
    scenario,
    outcome,
    threshold,
    weight,
    pass: if higher_is_better {
      outcome >= threshold
    } else {
      outcome <= threshold
    },
  }
}

///|
pub fn summarize_risk(
  observations : ArrayView[RiskObservation],
  lower_probability : Double,
  upper_probability : Double,
) -> RiskSummary raise @domain.ExtractionError {
  if observations.length() == 0 ||
    lower_probability < 0.0 ||
    upper_probability > 1.0 ||
    lower_probability > upper_probability {
    raise @domain.ExtractionError::InvalidArgument(
      message="risk summary inputs are invalid",
    )
  }
  let outcomes = [ for observation in observations => observation.outcome ]
  outcomes.sort()
  let mut total_weight = 0.0
  let mut pass_weight = 0.0
  let mut mean = 0.0
  let mut worst = observations[0].outcome
  let mut worst_scenario = observations[0].scenario
  for observation in observations {
    total_weight += observation.weight
    mean += observation.weight * observation.outcome
    if observation.pass {
      pass_weight += observation.weight
    }
    if observation.outcome < worst {
      worst = observation.outcome
      worst_scenario = observation.scenario
    }
  }
  let mean_outcome = if total_weight == 0.0 { 0.0 } else { mean / total_weight }
  let lower_index = (lower_probability * (outcomes.length() - 1).to_double()).to_int()
  let upper_index = (upper_probability * (outcomes.length() - 1).to_double()).to_int()
  let mut shortfall = 0.0
  let mut shortfall_count = 0
  for observation in observations {
    if !observation.pass {
      shortfall += (observation.threshold - observation.outcome).abs()
      shortfall_count += 1
    }
  }
  let pass_probability = if total_weight == 0.0 {
    0.0
  } else {
    pass_weight / total_weight
  }
  {
    observations: observations.length(),
    weighted_pass_probability: pass_probability,
    mean_outcome,
    lower_outcome: outcomes[lower_index],
    upper_outcome: outcomes[upper_index],
    expected_shortfall: if shortfall_count == 0 {
      0.0
    } else {
      shortfall / shortfall_count.to_double()
    },
    worst_scenario,
    risk_level: if pass_probability >= 0.95 {
      "low"
    } else if pass_probability >= 0.8 {
      "moderate"
    } else {
      "high"
    },
  }
}

///|
pub fn reliability_curve(
  outcomes : ArrayView[Double],
  thresholds : ArrayView[Double],
  higher_is_better : Bool,
) -> Array[ReliabilityCurvePoint] {
  let curve = []
  for threshold in thresholds {
    let mut pass = 0
    for outcome in outcomes {
      if (if higher_is_better {
          outcome >= threshold
        } else {
          outcome <= threshold
        }) {
        pass += 1
      }
    }
    let probability = if outcomes.length() == 0 {
      0.0
    } else {
      pass.to_double() / outcomes.length().to_double()
    }
    curve.push({
      threshold,
      pass_probability: probability,
      failure_probability: 1.0 - probability,
      sample_count: outcomes.length(),
    })
  }
  curve
}

///|
pub fn probability_above(
  outcomes : ArrayView[Double],
  threshold : Double,
) -> Double {
  if outcomes.length() == 0 {
    return 0.0
  }
  let mut count = 0
  for outcome in outcomes {
    if outcome >= threshold {
      count += 1
    }
  }
  count.to_double() / outcomes.length().to_double()
}

///|
pub fn probability_below(
  outcomes : ArrayView[Double],
  threshold : Double,
) -> Double {
  1.0 - probability_above(outcomes, threshold)
}

///|
pub fn conditional_mean(
  outcomes : ArrayView[Double],
  threshold : Double,
  above : Bool,
) -> Double {
  let mut total = 0.0
  let mut count = 0
  for outcome in outcomes {
    if (if above { outcome >= threshold } else { outcome < threshold }) {
      total += outcome
      count += 1
    }
  }
  if count == 0 {
    0.0
  } else {
    total / count.to_double()
  }
}

///|
pub fn value_at_risk(
  outcomes : ArrayView[Double],
  probability : Double,
) -> Double raise @domain.ExtractionError {
  if outcomes.length() == 0 || probability < 0.0 || probability > 1.0 {
    raise @domain.ExtractionError::InvalidArgument(
      message="value-at-risk inputs are invalid",
    )
  }
  let ordered = outcomes.to_owned()
  ordered.sort()
  ordered[(probability * (ordered.length() - 1).to_double()).to_int()]
}

///|
pub fn conditional_value_at_risk(
  outcomes : ArrayView[Double],
  probability : Double,
) -> Double raise @domain.ExtractionError {
  let threshold = value_at_risk(outcomes, probability)
  let mut total = 0.0
  let mut count = 0
  for outcome in outcomes {
    if outcome <= threshold {
      total += outcome
      count += 1
    }
  }
  if count == 0 {
    threshold
  } else {
    total / count.to_double()
  }
}

///|
pub fn risk_summary_markdown(summary : RiskSummary) -> String {
  "| Observations | Pass probability | Mean | Lower | Upper | Expected shortfall | Worst scenario | Risk |\n" +
  "|---:|---:|---:|---:|---:|---:|---|---|\n" +
  "| " +
  summary.observations.to_string() +
  " | " +
  summary.weighted_pass_probability.to_string() +
  " | " +
  summary.mean_outcome.to_string() +
  " | " +
  summary.lower_outcome.to_string() +
  " | " +
  summary.upper_outcome.to_string() +
  " | " +
  summary.expected_shortfall.to_string() +
  " | " +
  summary.worst_scenario +
  " | " +
  summary.risk_level +
  " |"
}

///|
pub fn reliability_curve_csv(
  curve : ArrayView[ReliabilityCurvePoint],
) -> String {
  let lines = ["threshold,pass_probability,failure_probability,sample_count"]
  for point in curve {
    lines.push(
      point.threshold.to_string() +
      "," +
      point.pass_probability.to_string() +
      "," +
      point.failure_probability.to_string() +
      "," +
      point.sample_count.to_string(),
    )
  }
  lines.join("\n")
}

///|
pub fn scenario_risk_order(
  observations : ArrayView[RiskObservation],
) -> Array[String] {
  let names = []
  for observation in observations {
    if !observation.pass {
      names.push(observation.scenario)
    }
  }
  names
}

///|
pub fn risk_margin(observation : RiskObservation) -> Double {
  observation.outcome - observation.threshold
}

///|
pub fn risk_margin_vector(
  observations : ArrayView[RiskObservation],
) -> Array[Double] {
  [
    for observation in observations => risk_margin(observation)
  ]
}

///|
pub fn risk_is_acceptable(
  summary : RiskSummary,
  minimum_probability : Double,
) -> Bool {
  summary.weighted_pass_probability >= minimum_probability &&
  summary.risk_level != "high"
}

///|
pub fn risk_level_from_probability(probability : Double) -> String {
  if probability >= 0.95 {
    "low"
  } else if probability >= 0.8 {
    "moderate"
  } else {
    "high"
  }
}