///|
/// Decision-oriented summaries for uncertainty and scenario studies.
pub(all) struct RiskBand {
  name : String
  lower : Double
  central : Double
  upper : Double
  probability : Double
  severity : String
} derive(Debug, Eq, ToJson)

///|
pub(all) struct ScenarioMetric {
  name : String
  value : Double
  weight : Double
  feasible : Bool
} derive(Debug, Eq, ToJson)

///|
pub(all) struct SensitivityContribution {
  parameter : String
  elasticity : Double
  absolute_effect : Double
  rank : Int
  direction : String
} derive(Debug, Eq, ToJson)

///|
pub(all) struct DecisionSummary {
  target : String
  central_value : Double
  lower_bound : Double
  upper_bound : Double
  probability_of_success : Double
  confidence : Double
  decision : String
  limiting_factor : String
} derive(Debug, Eq, ToJson)

///|
pub fn percentile(
  values : ArrayView[Double],
  probability : Double,
) -> Double raise @domain.ExtractionError {
  if values.length() == 0 || probability < 0.0 || probability > 1.0 {
    raise @domain.ExtractionError::InvalidArgument(
      message="percentile input is invalid",
    )
  }
  let ordered = values.to_owned()
  ordered.sort()
  let position = probability * (ordered.length() - 1).to_double()
  let lower = position.to_int()
  let upper = (lower + 1).min(ordered.length() - 1)
  let fraction = position - lower.to_double()
  ordered[lower] + fraction * (ordered[upper] - ordered[lower])
}

///|
pub fn risk_band(
  name : String,
  values : ArrayView[Double],
  lower_probability : Double,
  upper_probability : Double,
  target : Double,
) -> RiskBand raise @domain.ExtractionError {
  let lower = percentile(values, lower_probability)
  let central = percentile(values, 0.5)
  let upper = percentile(values, upper_probability)
  let mut success = 0
  for value in values {
    if value >= target {
      success += 1
    }
  }
  let probability = success.to_double() / values.length().to_double()
  {
    name,
    lower,
    central,
    upper,
    probability,
    severity: if probability >= 0.95 {
      "low"
    } else if probability >= 0.8 {
      "moderate"
    } else {
      "high"
    },
  }
}

///|
pub fn weighted_scenario_mean(
  scenarios : ArrayView[ScenarioMetric],
) -> Double raise @domain.ExtractionError {
  if scenarios.length() == 0 {
    raise @domain.ExtractionError::InsufficientData(required=1, actual=0)
  }
  let mut total = 0.0
  let mut weights = 0.0
  for scenario in scenarios {
    if scenario.weight < 0.0 {
      raise @domain.ExtractionError::InvalidArgument(
        message="scenario weight is negative",
      )
    }
    total += scenario.value * scenario.weight
    weights += scenario.weight
  }
  if weights == 0.0 {
    raise @domain.ExtractionError::InvalidArgument(
      message="scenario weights are zero",
    )
  }
  total / weights
}

///|
pub fn feasible_probability(scenarios : ArrayView[ScenarioMetric]) -> Double {
  if scenarios.length() == 0 {
    return 0.0
  }
  let mut total = 0.0
  let mut feasible = 0.0
  for scenario in scenarios {
    total += scenario.weight
    if scenario.feasible {
      feasible += scenario.weight
    }
  }
  if total == 0.0 {
    0.0
  } else {
    feasible / total
  }
}

///|
pub fn scenario_value_range(
  scenarios : ArrayView[ScenarioMetric],
) -> (Double, Double) {
  if scenarios.length() == 0 {
    return (0.0, 0.0)
  }
  let mut lower = scenarios[0].value
  let mut upper = lower
  for scenario in scenarios {
    lower = lower.min(scenario.value)
    upper = upper.max(scenario.value)
  }
  (lower, upper)
}

///|
pub fn sensitivity_contributions(
  parameters : ArrayView[String],
  elasticities : ArrayView[Double],
  baseline : Double,
) -> Array[SensitivityContribution] {
  let count = parameters.length().min(elasticities.length())
  let result = []
  for index in 0.. 0.0 {
        "positive"
      } else if elasticity < 0.0 {
        "negative"
      } else {
        "neutral"
      },
    })
  }
  for first in 0.. result[first].absolute_effect {
        rank += 1
      }
    }
    result[first] = {
      parameter: result[first].parameter,
      elasticity: result[first].elasticity,
      absolute_effect: result[first].absolute_effect,
      rank,
      direction: result[first].direction,
    }
  }
  result
}

///|
pub fn limiting_parameter(
  contributions : ArrayView[SensitivityContribution],
) -> String {
  if contributions.length() == 0 {
    return ""
  }
  let mut selected = contributions[0]
  for contribution in contributions {
    if contribution.absolute_effect > selected.absolute_effect {
      selected = contribution
    }
  }
  selected.parameter
}

///|
pub fn decide(
  target : String,
  values : ArrayView[Double],
  target_value : Double,
  minimum_probability : Double,
  confidence : Double,
) -> DecisionSummary raise @domain.ExtractionError {
  let band = risk_band(target, values, 0.05, 0.95, target_value)
  let probability = band.probability
  {
    target,
    central_value: band.central,
    lower_bound: band.lower,
    upper_bound: band.upper,
    probability_of_success: probability,
    confidence,
    decision: if probability >= minimum_probability {
      "accept"
    } else {
      "review"
    },
    limiting_factor: if band.severity == "high" {
      "uncertainty"
    } else {
      "none"
    },
  }
}

///|
pub fn decision_is_robust(
  summary : DecisionSummary,
  minimum_probability : Double,
  minimum_confidence : Double,
) -> Bool {
  summary.probability_of_success >= minimum_probability &&
  summary.confidence >= minimum_confidence &&
  summary.decision == "accept"
}

///|
pub fn risk_band_markdown(bands : ArrayView[RiskBand]) -> String {
  let lines = [
    "| Metric | Lower | Central | Upper | Success probability | Severity |", "|---|---:|---:|---:|---:|---|",
  ]
  for band in bands {
    lines.push(
      "| " +
      band.name +
      " | " +
      band.lower.to_string() +
      " | " +
      band.central.to_string() +
      " | " +
      band.upper.to_string() +
      " | " +
      band.probability.to_string() +
      " | " +
      band.severity +
      " |",
    )
  }
  lines.join("\n")
}

///|
pub fn sensitivity_markdown(
  contributions : ArrayView[SensitivityContribution],
) -> String {
  let lines = [
    "| Rank | Parameter | Elasticity | Absolute effect | Direction |", "|---:|---|---:|---:|---|",
  ]
  for contribution in contributions {
    lines.push(
      "| " +
      contribution.rank.to_string() +
      " | " +
      contribution.parameter +
      " | " +
      contribution.elasticity.to_string() +
      " | " +
      contribution.absolute_effect.to_string() +
      " | " +
      contribution.direction +
      " |",
    )
  }
  lines.join("\n")
}

///|
pub fn decision_markdown(summary : DecisionSummary) -> String {
  "| Target | Central | Lower | Upper | Probability | Confidence | Decision |\n" +
  "|---|---:|---:|---:|---:|---:|---|\n" +
  "| " +
  summary.target +
  " | " +
  summary.central_value.to_string() +
  " | " +
  summary.lower_bound.to_string() +
  " | " +
  summary.upper_bound.to_string() +
  " | " +
  summary.probability_of_success.to_string() +
  " | " +
  summary.confidence.to_string() +
  " | " +
  summary.decision +
  " |"
}

///|
pub fn scenario_metrics_csv(scenarios : ArrayView[ScenarioMetric]) -> String {
  let lines = ["name,value,weight,feasible"]
  for scenario in scenarios {
    lines.push(
      scenario.name +
      "," +
      scenario.value.to_string() +
      "," +
      scenario.weight.to_string() +
      "," +
      scenario.feasible.to_string(),
    )
  }
  lines.join("\n")
}

///|
pub fn bootstrap_mean(values : ArrayView[Double]) -> Double {
  if values.length() == 0 {
    return 0.0
  }
  let mut total = 0.0
  for value in values {
    total += value
  }
  total / values.length().to_double()
}

///|
pub fn bootstrap_standard_error(values : ArrayView[Double]) -> Double {
  if values.length() < 2 {
    return 0.0
  }
  let mean = bootstrap_mean(values)
  let mut total = 0.0
  for value in values {
    let difference = value - mean
    total += difference * difference
  }
  (total / (values.length() - 1).to_double()).sqrt() /
  values.length().to_double().sqrt()
}

///|
pub fn uncertainty_width(summary : DecisionSummary) -> Double {
  summary.upper_bound - summary.lower_bound
}

///|
pub fn uncertainty_width_relative(summary : DecisionSummary) -> Double {
  if summary.central_value == 0.0 {
    0.0
  } else {
    uncertainty_width(summary) / summary.central_value.abs()
  }
}