///|
/// Diagnostic comparison between two adjacent windows.
pub struct WindowDiagnostic {
  before : StatsSummary
  after : StatsSummary
  mean_shift : Double
  variance_ratio : Double
  distribution_shift : Double
  severity : AlertSeverity
  actionable : Bool
}

///|
pub fn WindowDiagnostic::empty() -> WindowDiagnostic {
  {
    before: StatsSummary::empty(),
    after: StatsSummary::empty(),
    mean_shift: 0.0,
    variance_ratio: 1.0,
    distribution_shift: 0.0,
    severity: Informational,
    actionable: false,
  }
}

///|
pub fn diagnose_windows(
  before_values : Array[Double],
  after_values : Array[Double],
  shift_threshold? : Double = 0.5,
  distribution_threshold? : Double = 0.25,
) -> WindowDiagnostic {
  let before_accumulator = OnlineMoments::new()
  for value in before_values {
    before_accumulator.push(value)
  }
  let after_accumulator = OnlineMoments::new()
  for value in after_values {
    after_accumulator.push(value)
  }
  let before = before_accumulator.summary(median=median(before_values))
  let after = after_accumulator.summary(median=median(after_values))
  let shift = absolute(after.mean - before.mean)
  let ratio = if before.variance <= 1.0e-12 {
    if after.variance > 0.0 {
      1.7976931348623157e308
    } else {
      1.0
    }
  } else {
    after.variance / before.variance
  }
  let distribution = ks_statistic(before_values, after_values)
  let score = clamp_probability(shift / (shift + 1.0) + distribution / 2.0)
  let severity = severity_from_score(score)
  {
    before,
    after,
    mean_shift: shift,
    variance_ratio: ratio,
    distribution_shift: distribution,
    severity,
    actionable: shift >= shift_threshold ||
    distribution >= distribution_threshold,
  }
}

///|
pub fn diagnostic_score(diagnostic : WindowDiagnostic) -> Double {
  let variance_score = if diagnostic.variance_ratio >= 1.0 {
    1.0 - 1.0 / diagnostic.variance_ratio
  } else {
    1.0 - diagnostic.variance_ratio
  }
  clamp_probability(
    diagnostic.mean_shift / (diagnostic.mean_shift + 1.0) * 0.5 +
    diagnostic.distribution_shift * 0.3 +
    variance_score * 0.2,
  )
}

///|
pub fn diagnostic_direction(diagnostic : WindowDiagnostic) -> ChangeDirection {
  if diagnostic.mean_shift > 0.0 {
    if diagnostic.after.mean > diagnostic.before.mean {
      Increase
    } else {
      Decrease
    }
  } else if diagnostic.variance_ratio > 1.0 {
    VarianceIncrease
  } else if diagnostic.variance_ratio < 1.0 {
    VarianceDecrease
  } else {
    Unknown
  }
}

///|
pub fn diagnostic_label(diagnostic : WindowDiagnostic) -> String {
  severity_name(diagnostic.severity) +
  ":" +
  direction_name(diagnostic_direction(diagnostic))
}

///|
pub fn diagnostic_markdown(diagnostic : WindowDiagnostic) -> String {
  "| field | value |\n|---|---:|\n| before mean | \{diagnostic.before.mean} |\n| after mean | \{diagnostic.after.mean} |\n| mean shift | \{diagnostic.mean_shift} |\n| variance ratio | \{diagnostic.variance_ratio} |\n| distribution shift | \{diagnostic.distribution_shift} |\n| severity | \{severity_name(diagnostic.severity)} |"
}

///|
pub fn diagnose_series(
  values : Array[Double],
  split : Int,
  minimum_segment? : Int = 3,
) -> WindowDiagnostic {
  let safe_split = if split < 0 {
    0
  } else if split > values.length() {
    values.length()
  } else {
    split
  }
  let before : Array[Double] = []
  let after : Array[Double] = []
  for i in 0.. Array[WindowDiagnostic] {
  let result : Array[WindowDiagnostic] = []
  for split in splits {
    result.push(diagnose_series(values, split))
  }
  result
}

///|
pub fn actionable_diagnostics(diagnostics : Array[WindowDiagnostic]) -> Int {
  let mut count = 0
  for diagnostic in diagnostics {
    if diagnostic.actionable {
      count += 1
    }
  }
  count
}