///|
/// Deterministic synthetic data scenario.
pub struct SyntheticScenario {
  values : Array[Double]
  clean : Array[Double]
  anomaly_indices : Array[Int]
  seed : Int
  name : String
}

///|
/// Parameters for production-like fixture generation.
pub struct SyntheticRule {
  length : Int
  level : Double
  trend : Double
  seasonal_period : Int
  seasonal_amplitude : Double
  noise_scale : Double
  outlier_rate : Double
  outlier_scale : Double
  seed : Int
}

///|
pub fn synthetic_default_rule() -> SyntheticRule {
  {
    length: 256,
    level: 100.0,
    trend: 0.02,
    seasonal_period: 7,
    seasonal_amplitude: 2.0,
    noise_scale: 0.5,
    outlier_rate: 0.02,
    outlier_scale: 20.0,
    seed: 20260819,
  }
}

///|
pub fn synthetic_rule(
  length : Int,
  level : Double,
  trend : Double,
  seasonal_period : Int,
  seasonal_amplitude : Double,
  noise_scale : Double,
  outlier_rate : Double,
  outlier_scale : Double,
  seed : Int,
) -> SyntheticRule {
  {
    length: if length < 0 {
      0
    } else {
      length
    },
    level,
    trend,
    seasonal_period: if seasonal_period < 2 {
      2
    } else {
      seasonal_period
    },
    seasonal_amplitude: if seasonal_amplitude < 0.0 {
      -seasonal_amplitude
    } else {
      seasonal_amplitude
    },
    noise_scale: if noise_scale < 0.0 {
      -noise_scale
    } else {
      noise_scale
    },
    outlier_rate: if outlier_rate < 0.0 {
      0.0
    } else if outlier_rate > 1.0 {
      1.0
    } else {
      outlier_rate
    },
    outlier_scale: if outlier_scale < 0.0 {
      -outlier_scale
    } else {
      outlier_scale
    },
    seed,
  }
}

///|
fn synthetic_safe_length(length : Int) -> Int {
  if length < 0 {
    0
  } else {
    length
  }
}

///|
pub fn synthetic_uniform(length : Int, seed : Int) -> Array[Double] {
  let result : Array[Double] = []
  let rng = DeterministicRng::new(seed)
  let count = synthetic_safe_length(length)
  for _ in 0.. Array[Double] {
  let values = synthetic_uniform(length, seed)
  let result = []
  let amplitude = if scale < 0.0 { -scale } else { scale }
  for value in values {
    result.push((value * 2.0 - 1.0) * amplitude)
  }
  result
}

///|
pub fn synthetic_level(length : Int, level : Double) -> Array[Double] {
  let result = []
  let count = synthetic_safe_length(length)
  for _ in 0.. Array[Double] {
  let result = []
  let count = synthetic_safe_length(length)
  for index = 0; index < count; index = index + 1 {
    result.push(level + slope * index.to_double())
  }
  result
}

///|
pub fn synthetic_seasonal(
  length : Int,
  period : Int,
  amplitude : Double,
) -> Array[Double] {
  let safe_period = if period < 2 { 2 } else { period }
  let result = []
  let count = synthetic_safe_length(length)
  for index = 0; index < count; index = index + 1 {
    let phase = (index % safe_period).to_double() / safe_period.to_double()
    result.push(amplitude * (phase * 2.0 - 1.0))
  }
  result
}

///|
pub fn synthetic_clean(rule : SyntheticRule) -> Array[Double] {
  let trend = synthetic_trend(rule.length, rule.level, rule.trend)
  let seasonal = synthetic_seasonal(
    rule.length,
    rule.seasonal_period,
    rule.seasonal_amplitude,
  )
  let noise = synthetic_noise(rule.length, rule.noise_scale, rule.seed)
  let result = []
  for index = 0; index < rule.length; index = index + 1 {
    result.push(trend[index] + seasonal[index] + noise[index])
  }
  result
}

///|
pub fn synthetic_anomaly_indices(
  length : Int,
  rate : Double,
  seed : Int,
) -> Array[Int] {
  let result = []
  if length <= 0 {
    return result
  }
  let rng = DeterministicRng::new(seed)
  for index = 0; index < length; index = index + 1 {
    if rng.next_unit() < rate {
      result.push(index)
    }
  }
  result
}

///|
pub fn synthetic_inject_spikes(
  clean : Array[Double],
  rate : Double,
  scale : Double,
  seed : Int,
) -> SyntheticScenario {
  let values = clean.copy()
  let indices = synthetic_anomaly_indices(clean.length(), rate, seed)
  let rng = DeterministicRng::new(seed + 17)
  for index in indices {
    let sign = if rng.next_unit() < 0.5 { -1.0 } else { 1.0 }
    values[index] += sign * scale * (0.5 + rng.next_unit())
  }
  { values, clean, anomaly_indices: indices, seed, name: "spikes" }
}

///|
pub fn synthetic_inject_level_shift(
  clean : Array[Double],
  start : Int,
  shift : Double,
) -> SyntheticScenario {
  let values = clean.copy()
  let index = if start < 0 { 0 } else { start }
  for cursor = index; cursor < values.length(); cursor = cursor + 1 {
    values[cursor] += shift
  }
  { values, clean, anomaly_indices: [], seed: 0, name: "level-shift" }
}

///|
pub fn synthetic_inject_variance_shift(
  clean : Array[Double],
  start : Int,
  scale : Double,
  seed : Int,
) -> SyntheticScenario {
  let values = clean.copy()
  let rng = DeterministicRng::new(seed)
  let index = if start < 0 { 0 } else { start }
  for cursor = index; cursor < values.length(); cursor = cursor + 1 {
    values[cursor] += (rng.next_unit() * 2.0 - 1.0) * scale
  }
  { values, clean, anomaly_indices: [], seed, name: "variance-shift" }
}

///|
pub fn synthetic_scenario(rule : SyntheticRule) -> SyntheticScenario {
  synthetic_inject_spikes(
    synthetic_clean(rule),
    rule.outlier_rate,
    rule.outlier_scale,
    rule.seed,
  )
}

///|
pub fn synthetic_scenario_values(scenario : SyntheticScenario) -> Array[Double] {
  scenario.values.copy()
}

///|
pub fn synthetic_scenario_clean(scenario : SyntheticScenario) -> Array[Double] {
  scenario.clean.copy()
}

///|
pub fn synthetic_scenario_labels(scenario : SyntheticScenario) -> Array[Bool] {
  let labels = []
  for _ in scenario.values {
    labels.push(false)
  }
  for index in scenario.anomaly_indices {
    if index >= 0 && index < labels.length() {
      labels[index] = true
    }
  }
  labels
}

///|
pub fn synthetic_scenario_detection_score(
  scenario : SyntheticScenario,
  threshold : Double,
) -> Double {
  let predicted = outlier_indices_z(scenario.values, threshold~)
  let truth = synthetic_scenario_labels(scenario)
  let predicted_labels = []
  for _ in scenario.values {
    predicted_labels.push(false)
  }
  for index in predicted {
    if index >= 0 && index < predicted_labels.length() {
      predicted_labels[index] = true
    }
  }
  f1_score(confusion_matrix(truth, predicted_labels))
}

///|
pub fn synthetic_mixture(
  left : Array[Double],
  right : Array[Double],
  weight : Double,
) -> Array[Double] {
  let result = []
  let probability = if weight < 0.0 {
    0.0
  } else if weight > 1.0 {
    1.0
  } else {
    weight
  }
  for value in left {
    result.push(value * probability)
  }
  for value in right {
    result.push(value * (1.0 - probability))
  }
  result
}

///|
pub fn synthetic_correlated(
  length : Int,
  correlation : Double,
  seed : Int,
) -> Array[Array[Double]] {
  let first = synthetic_noise(length, 1.0, seed)
  let second_noise = synthetic_noise(length, 1.0, seed + 1)
  let coefficient = if correlation < -1.0 {
    -1.0
  } else if correlation > 1.0 {
    1.0
  } else {
    correlation
  }
  let second = []
  for index = 0; index < first.length(); index = index + 1 {
    second.push(
      coefficient * first[index] +
      (1.0 - abs_double(coefficient)) * second_noise[index],
    )
  }
  [first, second]
}

///|
pub fn synthetic_missing(
  data : Array[Double],
  rate : Double,
  seed : Int,
) -> Array[Double] {
  let result = data.copy()
  let rng = DeterministicRng::new(seed)
  for index = 0; index < result.length(); index = index + 1 {
    if rng.next_unit() < rate {
      result[index] = result[index] / 0.0
    }
  }
  result
}

///|
pub fn synthetic_duplicate_blocks(
  data : Array[Double],
  block_size : Int,
  copies : Int,
) -> Array[Double] {
  let result = []
  let width = if block_size < 1 { 1 } else { block_size }
  let count = if copies < 1 { 1 } else { copies }
  for start = 0; start < data.length(); start = start + width {
    let end = if start + width > data.length() {
      data.length()
    } else {
      start + width
    }
    for _ in 0.. Array[Double] {
  let result = []
  let safe_period = if period < 1 { 1 } else { period }
  for index = 0; index < data.length(); index = index + 1 {
    let scale = if index / safe_period % 2 == 0 {
      first_scale
    } else {
      second_scale
    }
    result.push(data[index] * scale)
  }
  result
}

///|
pub fn synthetic_step_signal(
  length : Int,
  start : Int,
  before : Double,
  after : Double,
) -> Array[Double] {
  let result = []
  let count = synthetic_safe_length(length)
  for index = 0; index < count; index = index + 1 {
    result.push(if index < start { before } else { after })
  }
  result
}

///|
pub fn synthetic_ramp_signal(
  length : Int,
  start : Int,
  slope : Double,
) -> Array[Double] {
  let result = []
  let count = synthetic_safe_length(length)
  for index = 0; index < count; index = index + 1 {
    result.push(
      if index < start {
        0.0
      } else {
        (index - start).to_double() * slope
      },
    )
  }
  result
}

///|
pub fn synthetic_pulse_signal(
  length : Int,
  start : Int,
  duration : Int,
  amplitude : Double,
) -> Array[Double] {
  let result = []
  let safe_duration = if duration < 0 { 0 } else { duration }
  let end = start + safe_duration
  let count = synthetic_safe_length(length)
  for index = 0; index < count; index = index + 1 {
    result.push(if index >= start && index < end { amplitude } else { 0.0 })
  }
  result
}

///|
pub fn synthetic_periodic_outliers(
  data : Array[Double],
  period : Int,
  amplitude : Double,
) -> SyntheticScenario {
  let values = data.copy()
  let indices = []
  let safe_period = if period < 1 { 1 } else { period }
  for index = 0; index < values.length(); index = index + 1 {
    if index % safe_period == 0 {
      values[index] += amplitude
      indices.push(index)
    }
  }
  {
    values,
    clean: data,
    anomaly_indices: indices,
    seed: 0,
    name: "periodic-outliers",
  }
}

///|
pub fn synthetic_score_summary(scenario : SyntheticScenario) -> Array[Double] {
  let detected = outlier_indices_z(scenario.values, threshold=3.5)
  let truth = synthetic_scenario_labels(scenario)
  let predicted = []
  for _ in truth {
    predicted.push(false)
  }
  for index in detected {
    if index >= 0 && index < predicted.length() {
      predicted[index] = true
    }
  }
  let matrix = confusion_matrix(truth, predicted)
  [
    precision(matrix),
    recall(matrix),
    f1_score(matrix),
    matthews_correlation(matrix),
    benchmark_contamination_fraction(scenario.values),
  ]
}

///|
pub fn synthetic_robust_gain(scenario : SyntheticScenario) -> Double {
  let raw_error = abs_double(mean(scenario.values) - mean(scenario.clean))
  let robust_error = abs_double(
    median(scenario.values) - median(scenario.clean),
  )
  if raw_error == 0.0 {
    0.0
  } else {
    (raw_error - robust_error) / raw_error
  }
}

///|
pub fn synthetic_reproducible(rule : SyntheticRule) -> Bool {
  let left = synthetic_scenario(rule)
  let right = synthetic_scenario(rule)
  left.values == right.values && left.anomaly_indices == right.anomaly_indices
}

///|
pub fn synthetic_seed_sweep(
  rule : SyntheticRule,
  seeds : Array[Int],
) -> Array[Double] {
  let result = []
  for seed in seeds {
    let variant = synthetic_scenario({ ..rule, seed, })
    result.push(synthetic_robust_gain(variant))
  }
  result
}

///|
pub fn synthetic_length_sweep(
  rule : SyntheticRule,
  lengths : Array[Int],
) -> Array[Double] {
  let result = []
  for length in lengths {
    let variant = synthetic_scenario({ ..rule, length, })
    result.push(synthetic_robust_gain(variant))
  }
  result
}

///|
pub fn synthetic_contamination_sweep(
  rule : SyntheticRule,
  rates : Array[Double],
) -> Array[Double] {
  let result = []
  for rate in rates {
    let variant = synthetic_scenario({ ..rule, outlier_rate: rate })
    result.push(synthetic_robust_gain(variant))
  }
  result
}

///|
pub fn synthetic_noise_sweep(
  rule : SyntheticRule,
  scales : Array[Double],
) -> Array[Double] {
  let result = []
  for scale in scales {
    let variant = synthetic_scenario({ ..rule, noise_scale: scale })
    result.push(synthetic_robust_gain(variant))
  }
  result
}

///|
pub fn synthetic_compare_estimators(
  scenario : SyntheticScenario,
) -> Array[Double] {
  [
    abs_double(mean(scenario.values) - mean(scenario.clean)),
    abs_double(median(scenario.values) - median(scenario.clean)),
    abs_double(huber_location(scenario.values) - huber_location(scenario.clean)),
    abs_double(
      trimmed_mean(scenario.values, 0.1) - trimmed_mean(scenario.clean, 0.1),
    ),
    abs_double(
      winsorized_mean(scenario.values, 0.1) -
      winsorized_mean(scenario.clean, 0.1),
    ),
  ]
}

///|
pub fn synthetic_estimator_rank(scenario : SyntheticScenario) -> Array[Int] {
  let errors = synthetic_compare_estimators(scenario)
  let indices = []
  for index = 0; index < errors.length(); index = index + 1 {
    indices.push(index)
  }
  for left = 0; left < indices.length(); left = left + 1 {
    for right = left + 1; right < indices.length(); right = right + 1 {
      if errors[indices[right]] < errors[indices[left]] {
        let value = indices[left]
        indices[left] = indices[right]
        indices[right] = value
      }
    }
  }
  indices
}

///|
pub fn synthetic_residuals(scenario : SyntheticScenario) -> Array[Double] {
  let result = []
  for index = 0
      index < scenario.values.length() && index < scenario.clean.length()
      index = index + 1 {
    result.push(scenario.values[index] - scenario.clean[index])
  }
  result
}

///|
pub fn synthetic_residual_summary(
  scenario : SyntheticScenario,
) -> Array[Double] {
  let residuals = synthetic_residuals(scenario)
  [
    mean(residuals),
    mad(residuals),
    sample_stddev(residuals),
    max_value(residuals),
    min_value(residuals),
  ]
}