///|
/// A repeatable simulation scenario for demonstrations and regression tests.
pub(all) struct ProcessScenario {
  name : String
  center : Double
  deviation : Double
  lower : Double?
  upper : Double?
  drift : Double
  outlier_probability : Double
  seed : Int
} derive(Debug, ToJson)

///|
/// One simulated record with its latent cause.
pub(all) struct SimulatedPoint {
  index : Int
  value : Double
  center : Double
  cause : String
  in_spec : Bool
} derive(Debug, ToJson)

///|
/// Summary of a simulation run.
pub(all) struct SimulationSummary {
  scenario : String
  points : Int
  mean : Double?
  deviation : Double?
  defects : Int
  alarms : Int
} derive(Debug, ToJson)

///|
/// Construct a stable process scenario.
pub fn ProcessScenario::new(
  name~ : String,
  center~ : Double,
  deviation~ : Double,
  lower~ : Double?,
  upper~ : Double?,
  drift~ : Double,
  outlier_probability~ : Double,
  seed~ : Int,
) -> ProcessScenario {
  {
    name,
    center,
    deviation: deviation.abs(),
    lower,
    upper,
    drift,
    outlier_probability: outlier_probability.max(0.0).min(1.0),
    seed,
  }
}

///|
/// Generate deterministic pseudo-normal noise by summing uniform draws.
pub fn simulated_noise(seed : Int) -> Double {
  let samples = sample_uniform(12, seed~, lower=-1.0, upper=1.0)
  if samples.length() == 0 {
    0.0
  } else {
    sum_of(samples) / 2.0
  }
}

///|
/// Generate a deterministic process sequence with optional drift and faults.
pub fn simulate_process(
  scenario : ProcessScenario,
  count : Int,
) -> Array[SimulatedPoint] {
  let result : Array[SimulatedPoint] = []
  if count <= 0 {
    result
  } else {
    for index in 0.. 0 &&
        fault_selector[0] < scenario.outlier_probability
      let value = if fault {
        center + 4.0 * scenario.deviation
      } else {
        center + noise
      }
      let in_spec = match (scenario.lower, scenario.upper) {
        (Some(lower), Some(upper)) => value >= lower && value <= upper
        _ => true
      }
      let cause = if fault {
        "special-cause"
      } else if scenario.drift != 0.0 {
        "drift"
      } else {
        "common-cause"
      }
      result.push({ index, value, center, cause, in_spec })
    }
    result
  }
}

///|
/// Extract values from simulated points.
pub fn simulated_values(points : Array[SimulatedPoint]) -> Array[Double] {
  let result : Array[Double] = []
  for point in points {
    result.push(point.value)
  }
  result
}

///|
/// Extract latent centers from simulated points.
pub fn simulated_centers(points : Array[SimulatedPoint]) -> Array[Double] {
  let result : Array[Double] = []
  for point in points {
    result.push(point.center)
  }
  result
}

///|
/// Count points by latent cause.
pub fn simulation_causes(
  points : Array[SimulatedPoint],
) -> Array[CategoryCount] {
  let causes : Array[String] = []
  for point in points {
    causes.push(point.cause)
  }
  category_counts(causes)
}

///|
/// Count simulated specification defects.
pub fn simulation_defects(points : Array[SimulatedPoint]) -> Int {
  let mut count = 0
  for point in points {
    if !point.in_spec {
      count += 1
    }
  }
  count
}

///|
/// Count simulated special causes.
pub fn simulation_special_causes(points : Array[SimulatedPoint]) -> Int {
  let mut count = 0
  for point in points {
    if point.cause == "special-cause" {
      count += 1
    }
  }
  count
}

///|
/// Count points beyond three standard deviations from their latent center.
pub fn simulation_alarms(
  points : Array[SimulatedPoint],
  deviation : Double,
) -> Int {
  let mut count = 0
  for point in points {
    if deviation > 0.0 && (point.value - point.center).abs() > 3.0 * deviation {
      count += 1
    }
  }
  count
}

///|
/// Summarize a simulation result.
pub fn summarize_simulation(
  scenario : ProcessScenario,
  points : Array[SimulatedPoint],
) -> SimulationSummary {
  let values = simulated_values(points)
  let mean = if values.length() == 0 { None } else { Some(mean_of(values)) }
  let deviation = if values.length() < 2 {
    None
  } else {
    Some(std_of(values, mean_of(values)))
  }
  {
    scenario: scenario.name,
    points: points.length(),
    mean,
    deviation,
    defects: simulation_defects(points),
    alarms: simulation_alarms(points, scenario.deviation),
  }
}

///|
/// Generate a paired baseline and drifted process for comparison.
pub fn simulate_comparison(
  scenario : ProcessScenario,
  count : Int,
) -> (Array[SimulatedPoint], Array[SimulatedPoint]) {
  let baseline = { ..scenario, drift: 0.0, name: "baseline" }
  let changed = { ..scenario, name: "changed" }
  (simulate_process(baseline, count), simulate_process(changed, count))
}

///|
/// Difference between two simulated sequences.
pub fn simulation_delta(
  first : Array[SimulatedPoint],
  second : Array[SimulatedPoint],
) -> Array[Double]? {
  if first.length() != second.length() {
    None
  } else {
    let result : Array[Double] = []
    for index in 0.. Double? {
  let centers = simulated_centers(points)
  match linear_trend(centers) {
    Some(trend) => Some(trend.slope)
    None => None
  }
}

///|
/// Run a simulation through the streaming monitor.
pub fn simulate_monitor(scenario : ProcessScenario, count : Int) -> Int {
  let points = simulate_process(scenario, count)
  let policy = StreamPolicy::standard(
    lower=scenario.lower,
    upper=scenario.upper,
  )
  replay_stream(
    [
      for point in points => Some(point.value)
    ],
    policy~,
    center=scenario.center,
  )
}

///|
/// Build a synthetic subgroup array for chart examples.
pub fn simulated_subgroups(
  scenario : ProcessScenario,
  count : Int,
  width : Int,
) -> Array[Array[Double]]? {
  if width <= 0 {
    None
  } else {
    match
      subgroup(
        simulated_values(simulate_process(scenario, count * width)),
        width,
      ) {
      Some(groups) => Some(groups)
      None => None
    }
  }
}

///|
/// Generate a deterministic category stream with a controlled defect burst.
pub fn simulated_categories(
  count : Int,
  burst_start : Int,
  burst_length : Int,
) -> Array[String] {
  let result : Array[String] = []
  for index in 0..= burst_start && index < burst_start + burst_length {
      result.push("special-cause")
    } else {
      result.push("common-cause")
    }
  }
  result
}

///|
/// Compare expected and observed defect counts.
pub fn defect_count_error(expected : Int, observed : Int) -> Int {
  (observed - expected).abs()
}

///|
/// Check whether a simulation met a defect-rate target.
pub fn meets_defect_target(
  points : Array[SimulatedPoint],
  target : Double,
) -> Bool {
  if points.length() == 0 {
    target >= 0.0
  } else {
    simulation_defects(points).to_double() / points.length().to_double() <=
    target
  }
}