///|
/// Service-level agreement availability policy.
pub struct SlaPolicy {
  window : Double
  promised_availability : Double
  credit_rate : Double
  maximum_credit : Double
}

///|
pub fn sla_policy(
  window~ : Double,
  promised_availability~ : Double,
  credit_rate~ : Double,
  maximum_credit~ : Double,
) -> SlaPolicy {
  if window <= 0.0 ||
    promised_availability <= 0.0 ||
    promised_availability > 1.0 ||
    credit_rate < 0.0 ||
    maximum_credit < 0.0 {
    abort("invalid SLA policy")
  }
  { window, promised_availability, credit_rate, maximum_credit }
}

///|
pub struct SlaResult {
  observed_availability : Double
  downtime : Double
  breach : Bool
  credit : Double
  error_budget_remaining : Double
  confidence : MetricEstimate
}

///|
pub fn sla_result(
  observed_availability~ : Double,
  downtime~ : Double,
  breach~ : Bool,
  credit~ : Double,
  error_budget_remaining~ : Double,
  confidence~ : MetricEstimate,
) -> SlaResult {
  {
    observed_availability,
    downtime,
    breach,
    credit,
    error_budget_remaining,
    confidence,
  }
}

///|
pub fn evaluate_sla(
  failures : Array[Double],
  policy : SlaPolicy,
  observation_window : Double,
) -> SlaResult {
  if observation_window <= 0.0 {
    abort("observation window must be positive")
  }
  let downtime = failures.fold(init=0.0, (total, duration) => total + duration)
  let availability = (1.0 - downtime / observation_window).max(0.0).min(1.0)
  let breach = availability < policy.promised_availability
  let credit = if breach {
    ((policy.promised_availability - availability) * policy.credit_rate).min(
      policy.maximum_credit,
    )
  } else {
    0.0
  }
  let budget = (availability - policy.promised_availability).max(0.0) *
    observation_window
  let confidence = defect_rate(failures.length(), failures.length().max(1))
  sla_result(
    observed_availability=availability,
    downtime~,
    breach~,
    credit~,
    error_budget_remaining=budget,
    confidence~,
  )
}

///|
pub fn monthly_downtime_budget(
  window : Double,
  promised_availability : Double,
) -> Double {
  window * (1.0 - promised_availability)
}

///|
pub fn availability_from_incidents(
  incident_durations : Array[Double],
  window : Double,
) -> Double {
  if window <= 0.0 {
    abort("window must be positive")
  }
  (1.0 -
  incident_durations.fold(init=0.0, (sum, duration) => sum + duration) / window).max(
    0.0,
  )
}

///|
pub fn availability_slo_curve(
  model : ReliabilityModel,
  windows : Array[Double],
) -> Array[MetricEstimate] {
  windows.map(window => {
    let availability = model.survival(window)
    metric_estimate(
      estimate=availability,
      lower=availability,
      upper=availability,
      confidence_level=1.0,
    )
  })
}

///|
pub fn burn_rate(
  observed : Double,
  target : Double,
  interval : Double,
) -> Double {
  if target <= 0.0 || interval <= 0.0 {
    abort("invalid burn-rate arguments")
  }
  (1.0 - observed) / (1.0 - target)
}

///|
pub fn alert_severity(burn : Double) -> String {
  if burn >= 14.0 {
    "critical"
  } else if burn >= 4.0 {
    "high"
  } else if burn >= 1.0 {
    "elevated"
  } else {
    "normal"
  }
}