///|
/// Robust location and spread estimators used by offline quality reviews.
pub fn sorted_copy(values : Array[Double]) -> Array[Double] {
  let output = values.copy()
  for i in 1.. 0 && output[j - 1] > value {
      output[j] = output[j - 1]
      j -= 1
    }
    output[j] = value
  }
  output
}

///|
/// Linearly interpolated quantile in the closed probability interval.
pub fn quantile(values : Array[Double], probability : Double) -> Double? {
  if values.length() == 0 || probability < 0.0 || probability > 1.0 {
    None
  } else {
    let ordered = sorted_copy(values)
    if ordered.length() == 1 {
      Some(ordered[0])
    } else {
      let position = probability * (ordered.length() - 1).to_double()
      let lower = position.to_int()
      let upper = if lower + 1 >= ordered.length() { lower } else { lower + 1 }
      let fraction = position - lower.to_double()
      Some(ordered[lower] + (ordered[upper] - ordered[lower]) * fraction)
    }
  }
}

///|
pub fn median(values : Array[Double]) -> Double? {
  quantile(values, 0.5)
}

///|
/// Median absolute deviation, a robust estimate of process spread.
pub fn mad(values : Array[Double]) -> Double? {
  match median(values) {
    None => None
    Some(center) => {
      let distances : Array[Double] = []
      for value in values {
        distances.push((value - center).abs())
      }
      median(distances)
    }
  }
}

///|
/// Interquartile range.
pub fn iqr(values : Array[Double]) -> Double? {
  match (quantile(values, 0.25), quantile(values, 0.75)) {
    (Some(low), Some(high)) => Some(high - low)
    _ => None
  }
}

///|
/// Population z-scores using the supplied baseline center and deviation.
pub fn z_scores(
  values : Array[Double],
  center : Double,
  deviation : Double,
) -> Array[Double]? {
  if deviation <= 0.0 {
    None
  } else {
    let result : Array[Double] = []
    for value in values {
      result.push((value - center) / deviation)
    }
    Some(result)
  }
}

///|
/// Exponentially weighted moving average. Invalid smoothing factors return None.
pub fn ewma(
  values : Array[Double],
  lambda~ : Double,
  initial~ : Double,
) -> Array[Double] {
  let result : Array[Double] = []
  if lambda < 0.0 || lambda > 1.0 {
    result
  } else {
    let mut previous = initial
    for value in values {
      previous = lambda * value + (1.0 - lambda) * previous
      result.push(previous)
    }
    result
  }
}

///|
/// Two-sided cumulative-sum chart statistic.
pub fn cusum(
  values : Array[Double],
  target~ : Double,
  allowance~ : Double,
  decision~ : Double,
) -> Array[Double] {
  let result : Array[Double] = []
  if allowance < 0.0 || decision <= 0.0 {
    result
  } else {
    let mut positive = 0.0
    let mut negative = 0.0
    for value in values {
      positive = (positive + value - target - allowance).max(0.0)
      negative = (negative + target - allowance - value).max(0.0)
      if positive >= decision || negative >= decision {
        result.push(if positive >= negative { positive } else { -negative })
      } else {
        result.push(if positive > 0.0 { positive } else { -negative })
      }
    }
    result
  }
}

///|
/// Arithmetic means for all contiguous windows of a fixed width.
pub fn moving_mean(values : Array[Double], width : Int) -> Array[Double]? {
  if width <= 0 || width > values.length() {
    None
  } else {
    let result : Array[Double] = []
    let mut sum = 0.0
    for i in 0.. Array[Double]? {
  if width <= 1 || width > values.length() {
    None
  } else {
    let result : Array[Double] = []
    for start in 0..<(values.length() - width + 1) {
      let window : Array[Double] = []
      for i in start..<(start + width) {
        window.push(values[i])
      }
      result.push(std_of(window, mean_of(window)))
    }
    Some(result)
  }
}

///|
/// A deterministic validation message attached to a source index.
pub(all) struct ValidationIssue {
  index : Int
  field : String
  message : String
} derive(Debug, ToJson)

///|
/// Validate traceability fields and measurement cardinality.
pub fn validate_subgroups(groups : Array[Subgroup]) -> Array[ValidationIssue] {
  let issues : Array[ValidationIssue] = []
  for index, group in groups {
    if group.values.length() == 0 {
      issues.push({
        index,
        field: "values",
        message: "subgroup must contain a measurement",
      })
    }
    if group.batch.length() == 0 {
      issues.push({
        index,
        field: "batch",
        message: "batch identifier must not be empty",
      })
    }
    if group.equipment.length() == 0 {
      issues.push({
        index,
        field: "equipment",
        message: "equipment identifier must not be empty",
      })
    }
    if group.tool.length() == 0 {
      issues.push({
        index,
        field: "tool",
        message: "tool identifier must not be empty",
      })
    }
    if group.operator.length() == 0 {
      issues.push({
        index,
        field: "operator",
        message: "operator identifier must not be empty",
      })
    }
    if group.window.length() == 0 {
      issues.push({
        index,
        field: "window",
        message: "time window must not be empty",
      })
    }
  }
  issues
}

///|
/// Validate non-negative counts and positive exposure values.
pub fn validate_counts(
  counts : Array[Int],
  exposures : Array[Int],
) -> Array[ValidationIssue] {
  let issues : Array[ValidationIssue] = []
  if counts.length() != exposures.length() {
    issues.push({
      index: -1,
      field: "length",
      message: "counts and exposures must have equal length",
    })
  } else {
    for index in 0.. exposures[index] {
        issues.push({
          index,
          field: "count",
          message: "count must not exceed exposure",
        })
      }
    }
  }
  issues
}

///|
/// Validate a specification before capability analysis.
pub fn validate_specification(
  specification : Specification,
) -> Array[ValidationIssue] {
  if specification.lower < specification.upper {
    []
  } else {
    [
      {
        index: -1,
        field: "specification",
        message: "lower bound must be below upper bound",
      },
    ]
  }
}

///|
/// Split measurements into fixed-size groups without silently dropping a tail.
pub fn subgroup(values : Array[Double], width : Int) -> Array[Array[Double]]? {
  if width <= 0 || values.length() == 0 || values.length() % width != 0 {
    None
  } else {
    let groups : Array[Array[Double]] = []
    let mut start = 0
    while start < values.length() {
      let group : Array[Double] = []
      for index in start..<(start + width) {
        group.push(values[index])
      }
      groups.push(group)
      start += width
    }
    Some(groups)
  }
}

///|
/// Normalize values to a unit interval; constant inputs are rejected.
pub fn min_max_scale(values : Array[Double]) -> Array[Double]? {
  if values.length() == 0 {
    None
  } else {
    let mut low = values[0]
    let mut high = values[0]
    for value in values {
      low = low.min(value)
      high = high.max(value)
    }
    if high == low {
      None
    } else {
      let result : Array[Double] = []
      for value in values {
        result.push((value - low) / (high - low))
      }
      Some(result)
    }
  }
}