///|
/// Service-level indicators derived from change-point evidence.
pub struct SloWindow {
  mut total : Int
  mut bad : Int
  mut changed : Int
  mut severe : Int
}

///|
pub fn SloWindow::new() -> SloWindow {
  { total: 0, bad: 0, changed: 0, severe: 0 }
}

///|
pub fn SloWindow::observe(self : SloWindow, result : DetectionResult) -> Unit {
  self.total += 1
  if result.changed {
    self.changed += 1
  }
  if result.score >= 0.6 {
    self.bad += 1
  }
  if result.score >= 0.9 {
    self.severe += 1
  }
}

///|
pub fn SloWindow::total(self : SloWindow) -> Int {
  self.total
}

///|
pub fn SloWindow::bad(self : SloWindow) -> Int {
  self.bad
}

///|
pub fn SloWindow::changed(self : SloWindow) -> Int {
  self.changed
}

///|
pub fn SloWindow::severe(self : SloWindow) -> Int {
  self.severe
}

///|
pub fn SloWindow::availability(self : SloWindow) -> Double {
  if self.total == 0 {
    1.0
  } else {
    1.0 - self.bad.to_double() / self.total.to_double()
  }
}

///|
pub fn SloWindow::change_rate(self : SloWindow) -> Double {
  if self.total == 0 {
    0.0
  } else {
    self.changed.to_double() / self.total.to_double()
  }
}

///|
pub fn error_budget(availability : Double, target : Double) -> Double {
  let safe_target = clamp_probability(target)
  let safe_availability = clamp_probability(availability)
  if safe_availability >= safe_target {
    safe_availability - safe_target
  } else {
    0.0
  }
}

///|
pub fn burn_rate(window : SloWindow, target : Double) -> Double {
  let allowed = 1.0 - clamp_probability(target)
  if allowed <= 0.0 {
    if window.bad() > 0 {
      1.7976931348623157e308
    } else {
      0.0
    }
  } else {
    (1.0 - window.availability()) / allowed
  }
}

///|
pub struct SloReport {
  window : SloWindow
  target : Double
  budget : Double
  burn : Double
  breached : Bool
  recommendation : String
}

///|
pub fn evaluate_slo(
  window : SloWindow,
  target? : Double = 0.99,
  burn_limit? : Double = 1.0,
) -> SloReport {
  let safe_target = clamp_probability(target)
  let budget = error_budget(window.availability(), safe_target)
  let burn = burn_rate(window, safe_target)
  let breached = burn > burn_limit
  let recommendation = if breached {
    "freeze risky changes and investigate"
  } else if burn > burn_limit * 0.5 {
    "watch the error budget closely"
  } else {
    "within budget"
  }
  { window, target: safe_target, budget, burn, breached, recommendation }
}

///|
pub fn SloReport::availability(self : SloReport) -> Double {
  self.window.availability()
}

///|
pub fn SloReport::budget(self : SloReport) -> Double {
  self.budget
}

///|
pub fn SloReport::burn(self : SloReport) -> Double {
  self.burn
}

///|
pub fn SloReport::breached(self : SloReport) -> Bool {
  self.breached
}

///|
pub fn SloReport::summary(self : SloReport) -> String {
  "availability=\{self.availability()},budget=\{self.budget},burn=\{self.burn},breached=\{self.breached},recommendation=\{self.recommendation}"
}

///|
pub struct SloTracker {
  window : SloWindow
  target : Double
  burn_limit : Double
  mut periods : Int
}

///|
pub fn SloTracker::new(
  target? : Double = 0.99,
  burn_limit? : Double = 1.0,
) -> SloTracker {
  {
    window: SloWindow::new(),
    target: clamp_probability(target),
    burn_limit: if burn_limit < 0.0 {
      0.0
    } else {
      burn_limit
    },
    periods: 0,
  }
}

///|
pub fn SloTracker::observe(self : SloTracker, result : DetectionResult) -> Unit {
  self.window.observe(result)
}

///|
pub fn SloTracker::close_period(self : SloTracker) -> SloReport {
  self.periods += 1
  evaluate_slo(self.window, target=self.target, burn_limit=self.burn_limit)
}

///|
pub fn SloTracker::periods(self : SloTracker) -> Int {
  self.periods
}

///|
pub fn SloTracker::window(self : SloTracker) -> SloWindow {
  self.window
}