///|
pub struct DatasetSummary {
  count : Int
  minimum : Double
  maximum : Double
  mean : Double
  median : Double
  q1 : Double
  q3 : Double
  iqr : Double
  mad : Double
  standard_deviation : Double
  skewness : Double
  outlier_count : Int
  outlier_fraction : Double
}

///|
pub fn summarize(
  data : Array[Double],
  outlier_threshold? : Double = 3.5,
) -> DatasetSummary {
  let indices = outlier_indices_z(data, threshold=outlier_threshold)
  {
    count: data.length(),
    minimum: min_value(data),
    maximum: max_value(data),
    mean: mean(data),
    median: median(data),
    q1: lower_quartile(data),
    q3: upper_quartile(data),
    iqr: interquartile_range(data),
    mad: mad(data),
    standard_deviation: sample_stddev(data),
    skewness: skewness(data),
    outlier_count: indices.length(),
    outlier_fraction: outlier_fraction(indices, data.length()),
  }
}

///|
pub fn summary_to_lines(summary : DatasetSummary) -> Array[String] {
  [
    "count=" + summary.count.to_string(),
    "minimum=" + summary.minimum.to_string(),
    "maximum=" + summary.maximum.to_string(),
    "mean=" + summary.mean.to_string(),
    "median=" + summary.median.to_string(),
    "q1=" + summary.q1.to_string(),
    "q3=" + summary.q3.to_string(),
    "iqr=" + summary.iqr.to_string(),
    "mad=" + summary.mad.to_string(),
    "standard_deviation=" + summary.standard_deviation.to_string(),
    "skewness=" + summary.skewness.to_string(),
    "outlier_count=" + summary.outlier_count.to_string(),
    "outlier_fraction=" + summary.outlier_fraction.to_string(),
  ]
}

///|
pub fn summary_to_string(summary : DatasetSummary) -> String {
  summary_to_lines(summary).join("\n")
}

///|
pub fn robust_summary_score(data : Array[Double]) -> Double {
  let summary = summarize(data)
  if summary.mad == 0.0 {
    0.0
  } else {
    summary.iqr / summary.mad
  }
}

///|
pub fn compare_location_estimators(data : Array[Double]) -> Array[Double] {
  [
    mean(data),
    median(data),
    trimmed_mean(data, 0.1),
    huber_location(data),
    tukey_bisquare_location(data).location,
    median_of_means(data, 4),
  ]
}

///|
pub fn estimator_bias_against_clean(
  data : Array[Double],
  clean_center : Double,
) -> Array[Double] {
  let estimates = compare_location_estimators(data)
  let result = []
  for estimate in estimates {
    result.push(estimate - clean_center)
  }
  result
}

///|
pub fn detect_shift(
  reference : Array[Double],
  current : Array[Double],
  threshold : Double,
) -> Bool {
  if threshold <= 0.0 {
    abort("threshold must be positive")
  }
  if reference.length() == 0 || current.length() == 0 {
    return false
  }
  let location_shift = abs_double(median(current) - median(reference))
  let scale = mad(reference)
  if scale == 0.0 {
    location_shift > 0.0
  } else {
    location_shift / scale > threshold
  }
}

///|
pub fn detect_scale_change(
  reference : Array[Double],
  current : Array[Double],
  threshold : Double,
) -> Bool {
  if threshold <= 1.0 {
    abort("threshold must be greater than one")
  }
  let reference_scale = mad(reference)
  let current_scale = mad(current)
  if reference_scale == 0.0 {
    current_scale > 0.0
  } else {
    let ratio = current_scale / reference_scale
    ratio > threshold || ratio < 1.0 / threshold
  }
}

///|
pub fn robust_signal_quality(data : Array[Double]) -> Double {
  if data.length() == 0 {
    return 0.0
  }
  let spread = mad(data)
  let total = sum_absolute(data)
  if total == 0.0 {
    1.0
  } else {
    1.0 / (1.0 + spread / (total / data.length().to_double()))
  }
}

///|
pub fn winsorized_summary(
  data : Array[Double],
  trim_percent : Double,
) -> DatasetSummary {
  summarize(winsorize(data, trim_percent))
}

///|
pub fn robust_confidence_width(
  data : Array[Double],
  confidence? : Double = 0.95,
) -> Double {
  validate_confidence(confidence)
  if data.length() == 0 {
    return 0.0
  }
  let interval = bootstrap_median_interval(data, 200, confidence~, seed=202608)
  interval.upper - interval.lower
}

///|
pub fn stable_against_single_outlier(
  data : Array[Double],
  outlier : Double,
) -> Double {
  let augmented = copy_array(data)
  augmented.push(outlier)
  let before = median(data)
  let after = median(augmented)
  abs_double(after - before)
}

///|
pub fn estimate_reliability(data : Array[Double]) -> Array[Double] {
  let summary = summarize(data)
  [
    summary.mad,
    summary.iqr,
    summary.standard_deviation,
    summary.outlier_fraction,
    robust_signal_quality(data),
  ]
}

///|
pub fn summary_json_fields(summary : DatasetSummary) -> Array[String] {
  [
    "\"count\":" + summary.count.to_string(),
    "\"minimum\":" + summary.minimum.to_string(),
    "\"maximum\":" + summary.maximum.to_string(),
    "\"mean\":" + summary.mean.to_string(),
    "\"median\":" + summary.median.to_string(),
    "\"q1\":" + summary.q1.to_string(),
    "\"q3\":" + summary.q3.to_string(),
    "\"iqr\":" + summary.iqr.to_string(),
    "\"mad\":" + summary.mad.to_string(),
    "\"standard_deviation\":" + summary.standard_deviation.to_string(),
    "\"skewness\":" + summary.skewness.to_string(),
    "\"outlier_count\":" + summary.outlier_count.to_string(),
    "\"outlier_fraction\":" + summary.outlier_fraction.to_string(),
  ]
}

///|
pub fn summary_to_json(summary : DatasetSummary) -> String {
  let fields = summary_json_fields(summary).join(",")
  "{" + fields + "}"
}