///|
pub struct RobustPipeline {
  window : Int
  trim_percent : Double
  threshold : Double
  detector : AnomalyDetector
  mut fitted : Bool
}

///|
pub struct PipelineResult {
  cleaned : Array[Double]
  scores : Array[Double]
  flags : Array[Bool]
  baseline : Array[Double]
}

///|
pub fn RobustPipeline::new(
  window : Int,
  trim_percent : Double,
  threshold : Double,
) -> RobustPipeline {
  if window <= 0 {
    abort("window must be positive")
  }
  validate_trim(trim_percent)
  if threshold <= 0.0 {
    abort("threshold must be positive")
  }
  {
    window,
    trim_percent,
    threshold,
    detector: AnomalyDetector::new(threshold~),
    fitted: false,
  }
}

///|
pub fn RobustPipeline::fit(
  self : RobustPipeline,
  reference : Array[Double],
) -> Unit {
  self.detector.fit(reference)
  self.fitted = reference.length() > 0
}

///|
pub fn RobustPipeline::transform(
  self : RobustPipeline,
  data : Array[Double],
) -> PipelineResult {
  let cleaned = hampel_filter(data, self.window, threshold=self.threshold)
  let scores = rolling_mad_residuals(data, self.window)
  let flags = rolling_outlier_flags(data, self.window, threshold=self.threshold)
  let baseline = rolling_winsorized_mean(data, self.window, self.trim_percent)
  { cleaned, scores, flags, baseline }
}

///|
pub fn RobustPipeline::fit_transform(
  self : RobustPipeline,
  reference : Array[Double],
) -> PipelineResult {
  self.fit(reference)
  self.transform(reference)
}

///|
pub fn RobustPipeline::predict_next(
  self : RobustPipeline,
  data : Array[Double],
) -> Double {
  robust_forecast_next(data, self.window)
}

///|
pub fn RobustPipeline::summary(
  self : RobustPipeline,
  data : Array[Double],
) -> DatasetSummary {
  summarize(self.transform(data).cleaned, outlier_threshold=self.threshold)
}

///|
pub fn RobustPipeline::is_fitted(self : RobustPipeline) -> Bool {
  self.fitted
}

///|
pub fn RobustPipeline::window(self : RobustPipeline) -> Int {
  self.window
}

///|
pub fn RobustPipeline::trim_percent(self : RobustPipeline) -> Double {
  self.trim_percent
}

///|
pub fn RobustPipeline::threshold(self : RobustPipeline) -> Double {
  self.threshold
}

///|
pub fn pipeline_clean(
  data : Array[Double],
  window : Int,
  threshold : Double,
) -> Array[Double] {
  hampel_filter(data, window, threshold~)
}

///|
pub fn pipeline_score(data : Array[Double], window : Int) -> Array[Double] {
  rolling_mad_residuals(data, window)
}

///|
pub fn pipeline_flags(
  data : Array[Double],
  window : Int,
  threshold : Double,
) -> Array[Bool] {
  rolling_outlier_flags(data, window, threshold~)
}

///|
pub fn pipeline_baseline(
  data : Array[Double],
  window : Int,
  trim_percent : Double,
) -> Array[Double] {
  rolling_winsorized_mean(data, window, trim_percent)
}

///|
pub fn pipeline_quality(
  data : Array[Double],
  window : Int,
  trim_percent : Double,
  threshold : Double,
) -> Array[Double] {
  let cleaned = pipeline_clean(data, window, threshold)
  let summary = summarize(cleaned, outlier_threshold=threshold)
  [
    summary.mad,
    summary.iqr,
    summary.outlier_fraction,
    robust_signal_quality(cleaned),
    abs_double(mean(data) - mean(cleaned)),
    abs_double(median(data) - median(cleaned)),
    trim_percent,
    window.to_double(),
    threshold,
  ]
}

///|
pub fn pipeline_residual_report(
  data : Array[Double],
  window : Int,
  threshold : Double,
) -> Array[Double] {
  let residuals = robust_residuals(data, window)
  [
    mean_absolute_error(data, difference_from_baseline(data, mean(residuals))),
    mad(residuals),
    maximum_influence(residuals),
    robust_signal_quality(residuals),
    threshold,
  ]
}

///|
pub fn pipeline_compare(
  data : Array[Double],
  window : Int,
  trim_percent : Double,
  threshold : Double,
) -> Array[Array[Double]] {
  let raw = summarize(data, outlier_threshold=threshold)
  let cleaned = summarize(
    pipeline_clean(data, window, threshold),
    outlier_threshold=threshold,
  )
  [
    [raw.mean, raw.median, raw.mad, raw.outlier_fraction],
    [cleaned.mean, cleaned.median, cleaned.mad, cleaned.outlier_fraction],
    [
      raw.standard_deviation,
      cleaned.standard_deviation,
      trim_percent,
      threshold,
    ],
  ]
}

///|
pub fn pipeline_stability(
  data : Array[Double],
  window : Int,
  threshold : Double,
) -> Double {
  let cleaned = pipeline_clean(data, window, threshold)
  1.0 / (1.0 + mean_absolute_error(data, cleaned))
}

///|
pub fn pipeline_resample(
  data : Array[Double],
  window : Int,
  threshold : Double,
  replicates : Int,
  seed? : Int = 12345,
) -> BootstrapInterval {
  let samples = bootstrap_replicates(data, replicates, seed~)
  let estimates = []
  for sample in samples {
    estimates.push(mean(pipeline_clean(sample, window, threshold)))
  }
  if estimates.length() == 0 {
    return {
      estimate: 0.0,
      lower: 0.0,
      upper: 0.0,
      confidence: 0.95,
      replicates: 0,
    }
  }
  {
    estimate: mean(pipeline_clean(data, window, threshold)),
    lower: quantile(estimates, 0.025),
    upper: quantile(estimates, 0.975),
    confidence: 0.95,
    replicates,
  }
}

///|
pub fn pipeline_apply_to_segments(
  data : Array[Double],
  segments : Int,
  window : Int,
  threshold : Double,
) -> Array[Array[Double]] {
  let result = []
  for values in segment_values(data, segments) {
    result.push(pipeline_quality(values, window, 0.1, threshold))
  }
  result
}

///|
pub fn pipeline_alert_count(
  data : Array[Double],
  window : Int,
  threshold : Double,
) -> Int {
  let mut count = 0
  for flag in pipeline_flags(data, window, threshold) {
    if flag {
      count += 1
    }
  }
  count
}

///|
pub fn pipeline_alert_rate(
  data : Array[Double],
  window : Int,
  threshold : Double,
) -> Double {
  if data.length() == 0 {
    0.0
  } else {
    pipeline_alert_count(data, window, threshold).to_double() /
    data.length().to_double()
  }
}

///|
pub fn pipeline_has_alert(
  data : Array[Double],
  window : Int,
  threshold : Double,
) -> Bool {
  pipeline_alert_count(data, window, threshold) > 0
}

///|
pub fn pipeline_change_points(
  data : Array[Double],
  window : Int,
  threshold : Double,
) -> Array[Int] {
  change_point_indices(data, window, threshold)
}

///|
pub fn pipeline_forecast_error(data : Array[Double], window : Int) -> Double {
  let errors = rolling_forecast_errors(data, window)
  mad(errors)
}

///|
pub fn pipeline_risk(data : Array[Double]) -> Array[Double] {
  robust_risk_summary(data)
}