///|
/// Policy for production anomaly alerts.
pub struct AlertPolicy {
  warning_threshold : Double
  critical_threshold : Double
  recovery_threshold : Double
  consecutive_count : Int
  cooldown : Int
  direction : Int
}

///|
/// One alert transition emitted by a monitor.
pub struct AlertEvent {
  index : Int
  value : Double
  score : Double
  level : Int
  active : Bool
  recovered : Bool
}

///|
/// Stateful alert monitor with hysteresis and cooldown.
pub struct AlertMonitor {
  policy : AlertPolicy
  mut index : Int
  mut level : Int
  mut consecutive : Int
  mut cooldown_left : Int
  mut active : Bool
  mut events : Int
}

///|
pub fn alert_default_policy() -> AlertPolicy {
  {
    warning_threshold: 2.5,
    critical_threshold: 4.0,
    recovery_threshold: 1.5,
    consecutive_count: 2,
    cooldown: 0,
    direction: 0,
  }
}

///|
pub fn alert_policy(
  warning_threshold : Double,
  critical_threshold : Double,
  recovery_threshold : Double,
  consecutive_count : Int,
  cooldown : Int,
  direction : Int,
) -> AlertPolicy {
  {
    warning_threshold: if warning_threshold < 0.0 {
      0.0
    } else {
      warning_threshold
    },
    critical_threshold: if critical_threshold < warning_threshold {
      warning_threshold
    } else {
      critical_threshold
    },
    recovery_threshold: if recovery_threshold < 0.0 {
      0.0
    } else {
      recovery_threshold
    },
    consecutive_count: if consecutive_count < 1 {
      1
    } else {
      consecutive_count
    },
    cooldown: if cooldown < 0 {
      0
    } else {
      cooldown
    },
    direction: if direction < -1 {
      -1
    } else if direction > 1 {
      1
    } else {
      direction
    },
  }
}

///|
pub fn alert_score(value : Double, center : Double, scale : Double) -> Double {
  if scale <= 1.0e-12 {
    if value == center {
      0.0
    } else {
      1.0e12
    }
  } else {
    abs_double(value - center) / scale
  }
}

///|
pub fn alert_signed_score(
  value : Double,
  center : Double,
  scale : Double,
) -> Double {
  if scale <= 1.0e-12 {
    0.0
  } else {
    (value - center) / scale
  }
}

///|
pub fn alert_level(score : Double, policy : AlertPolicy) -> Int {
  if score >= policy.critical_threshold {
    2
  } else if score >= policy.warning_threshold {
    1
  } else {
    0
  }
}

///|
pub fn alert_direction_allowed(
  value : Double,
  center : Double,
  policy : AlertPolicy,
) -> Bool {
  if policy.direction == 0 {
    true
  } else if policy.direction > 0 {
    value >= center
  } else {
    value <= center
  }
}

///|
pub fn alert_flags(
  data : Array[Double],
  center : Double,
  scale : Double,
  policy : AlertPolicy,
) -> Array[Bool] {
  let result = []
  for value in data {
    let score = alert_score(value, center, scale)
    result.push(
      alert_level(score, policy) > 0 &&
      alert_direction_allowed(value, center, policy),
    )
  }
  result
}

///|
pub fn alert_levels(
  data : Array[Double],
  center : Double,
  scale : Double,
  policy : AlertPolicy,
) -> Array[Int] {
  let result = []
  for value in data {
    let score = alert_score(value, center, scale)
    result.push(
      if alert_direction_allowed(value, center, policy) {
        alert_level(score, policy)
      } else {
        0
      },
    )
  }
  result
}

///|
pub fn alert_debounce(flags : Array[Bool], consecutive : Int) -> Array[Bool] {
  let required = if consecutive < 1 { 1 } else { consecutive }
  let result = []
  let mut run = 0
  for flag in flags {
    if flag {
      run += 1
    } else {
      run = 0
    }
    result.push(run >= required)
  }
  result
}

///|
pub fn alert_recovery_flags(
  scores : Array[Double],
  policy : AlertPolicy,
) -> Array[Bool] {
  let result = []
  let mut active = false
  for score in scores {
    let entered = score >= policy.warning_threshold
    let recovered = score <= policy.recovery_threshold
    if !active && entered {
      active = true
    } else if active && recovered {
      active = false
    }
    result.push(active)
  }
  result
}

///|
pub fn alert_cooldown_flags(flags : Array[Bool], cooldown : Int) -> Array[Bool] {
  let wait = if cooldown < 0 { 0 } else { cooldown }
  let result = []
  let mut remaining = 0
  for flag in flags {
    if flag && remaining == 0 {
      result.push(true)
      remaining = wait
    } else {
      result.push(false)
      if remaining > 0 {
        remaining -= 1
      }
    }
  }
  result
}

///|
pub fn alert_event_indices(flags : Array[Bool]) -> Array[Int] {
  let result = []
  let mut previous = false
  for index = 0; index < flags.length(); index = index + 1 {
    if flags[index] && !previous {
      result.push(index)
    }
    previous = flags[index]
  }
  result
}

///|
pub fn alert_recovery_indices(flags : Array[Bool]) -> Array[Int] {
  let result = []
  let mut previous = false
  for index = 0; index < flags.length(); index = index + 1 {
    if !flags[index] && previous {
      result.push(index)
    }
    previous = flags[index]
  }
  result
}

///|
pub fn alert_duration_series(flags : Array[Bool]) -> Array[Int] {
  let result = []
  let mut duration = 0
  for flag in flags {
    if flag {
      duration += 1
    } else {
      duration = 0
    }
    result.push(duration)
  }
  result
}

///|
pub fn alert_incident_durations(flags : Array[Bool]) -> Array[Int] {
  let result = []
  let mut duration = 0
  for flag in flags {
    if flag {
      duration += 1
    } else if duration > 0 {
      result.push(duration)
      duration = 0
    }
  }
  if duration > 0 {
    result.push(duration)
  }
  result
}

///|
pub fn alert_incident_count(flags : Array[Bool]) -> Int {
  alert_event_indices(flags).length()
}

///|
pub fn alert_incident_rate(flags : Array[Bool]) -> Double {
  if flags.length() == 0 {
    0.0
  } else {
    alert_incident_count(flags).to_double() / flags.length().to_double()
  }
}

///|
pub fn alert_active_fraction(flags : Array[Bool]) -> Double {
  let mut count = 0
  for flag in flags {
    if flag {
      count += 1
    }
  }
  if flags.length() == 0 {
    0.0
  } else {
    count.to_double() / flags.length().to_double()
  }
}

///|
pub fn alert_mean_duration(flags : Array[Bool]) -> Double {
  let durations = alert_incident_durations(flags)
  if durations.length() == 0 {
    0.0
  } else {
    let values = []
    for duration in durations {
      values.push(duration.to_double())
    }
    mean(values)
  }
}

///|
pub fn alert_max_duration(flags : Array[Bool]) -> Int {
  let durations = alert_incident_durations(flags)
  let mut result = 0
  for duration in durations {
    if duration > result {
      result = duration
    }
  }
  result
}

///|
pub fn alert_quality_score(
  flags : Array[Bool],
  scores : Array[Double],
) -> Double {
  let active = alert_active_fraction(flags)
  let magnitude = if scores.length() == 0 { 0.0 } else { mean(scores) }
  1.0 / (1.0 + active + magnitude)
}

///|
pub fn alert_health_score(
  quality_score : Double,
  drift_score : Double,
  risk_score : Double,
) -> Double {
  let penalty = if quality_score < 0.0 { 1.0 } else { 1.0 - quality_score }
  let raw = 1.0 - (penalty + drift_score + risk_score) / 3.0
  if raw < 0.0 {
    0.0
  } else if raw > 1.0 {
    1.0
  } else {
    raw
  }
}

///|
pub fn alert_health_from_data(
  data : Array[Double],
  rule : QualityRule,
  drift_baseline : Array[Double],
) -> Double {
  let quality = quality_report(data, rule).quality_score
  let drift = drift_score(drift_baseline, data, 10)
  let risk = robust_signal_quality(data)
  alert_health_score(quality, drift, risk)
}

///|
pub fn AlertMonitor::new(policy : AlertPolicy) -> AlertMonitor {
  {
    policy,
    index: 0,
    level: 0,
    consecutive: 0,
    cooldown_left: 0,
    active: false,
    events: 0,
  }
}

///|
pub fn AlertMonitor::observe(
  self : AlertMonitor,
  value : Double,
  center : Double,
  scale : Double,
) -> AlertEvent {
  let score = alert_signed_score(value, center, scale)
  let magnitude = abs_double(score)
  let candidate = alert_direction_allowed(value, center, self.policy)
  let candidate_level = if candidate {
    alert_level(magnitude, self.policy)
  } else {
    0
  }
  let previous_active = self.active
  if candidate_level > 0 {
    self.consecutive += 1
  } else {
    self.consecutive = 0
  }
  if self.cooldown_left > 0 {
    self.cooldown_left -= 1
  }
  if !self.active &&
    candidate_level > 0 &&
    self.consecutive >= self.policy.consecutive_count &&
    self.cooldown_left == 0 {
    self.active = true
    self.level = candidate_level
    self.events += 1
    self.cooldown_left = self.policy.cooldown
  } else if self.active && magnitude <= self.policy.recovery_threshold {
    self.active = false
    self.level = 0
  } else if self.active && candidate_level > self.level {
    self.level = candidate_level
  }
  let recovered = previous_active && !self.active
  let event = {
    index: self.index,
    value,
    score: magnitude,
    level: self.level,
    active: self.active,
    recovered,
  }
  self.index += 1
  event
}

///|
pub fn AlertMonitor::observe_many(
  self : AlertMonitor,
  data : Array[Double],
  center : Double,
  scale : Double,
) -> Array[AlertEvent] {
  let result = []
  for value in data {
    result.push(self.observe(value, center, scale))
  }
  result
}

///|
pub fn AlertMonitor::reset(self : AlertMonitor) -> Unit {
  self.index = 0
  self.level = 0
  self.consecutive = 0
  self.cooldown_left = 0
  self.active = false
  self.events = 0
}

///|
pub fn AlertMonitor::is_active(self : AlertMonitor) -> Bool {
  self.active
}

///|
pub fn AlertMonitor::level(self : AlertMonitor) -> Int {
  self.level
}

///|
pub fn AlertMonitor::seen(self : AlertMonitor) -> Int {
  self.index
}

///|
pub fn AlertMonitor::event_count(self : AlertMonitor) -> Int {
  self.events
}

///|
pub fn alert_score_series(data : Array[Double], window : Int) -> Array[Double] {
  let center = rolling_median(data, if window < 2 { 2 } else { window })
  let scale = rolling_mad(data, if window < 2 { 2 } else { window })
  let result = []
  for index = 0; index < data.length(); index = index + 1 {
    let local_scale = if scale[index] <= 1.0e-12 {
      1.0
    } else {
      scale[index] * 1.4826
    }
    result.push(alert_score(data[index], center[index], local_scale))
  }
  result
}

///|
pub fn alert_series(
  data : Array[Double],
  window : Int,
  policy : AlertPolicy,
) -> Array[AlertEvent] {
  let monitor = AlertMonitor::new(policy)
  let safe_window = if window < 2 { 2 } else { window }
  let center = mean(rolling_median(data, safe_window))
  let scale = mad(rolling_mad(data, safe_window)) * 1.4826
  monitor.observe_many(data, center, if scale <= 1.0e-12 { 1.0 } else { scale })
}

///|
pub fn alert_series_flags(
  data : Array[Double],
  window : Int,
  policy : AlertPolicy,
) -> Array[Bool] {
  let scores = alert_score_series(data, window)
  let flags = alert_flags(scores, 0.0, 1.0, policy)
  alert_debounce(flags, policy.consecutive_count)
}

///|
pub fn alert_series_levels(
  data : Array[Double],
  window : Int,
  policy : AlertPolicy,
) -> Array[Int] {
  let scores = alert_score_series(data, window)
  alert_levels(scores, 0.0, 1.0, policy)
}

///|
pub fn alert_slo_breach_fraction(
  flags : Array[Bool],
  target : Double,
) -> Double {
  let actual = alert_active_fraction(flags)
  if actual <= target {
    0.0
  } else {
    actual - target
  }
}

///|
pub fn alert_slo_score(flags : Array[Bool], target : Double) -> Double {
  let breach = alert_slo_breach_fraction(flags, target)
  if target <= 0.0 {
    if breach == 0.0 {
      1.0
    } else {
      0.0
    }
  } else {
    1.0 - breach / target
  }
}

///|
pub fn alert_trend(scores : Array[Double]) -> Double {
  if scores.length() < 2 {
    0.0
  } else {
    let x = []
    for index = 0; index < scores.length(); index = index + 1 {
      x.push(index.to_double())
    }
    linear_regression(x, scores).slope
  }
}

///|
pub fn alert_escalation_indices(levels : Array[Int]) -> Array[Int] {
  let result = []
  for index = 1; index < levels.length(); index = index + 1 {
    if levels[index] > levels[index - 1] {
      result.push(index)
    }
  }
  result
}

///|
pub fn alert_recovery_quality(flags : Array[Bool]) -> Double {
  let starts = alert_event_indices(flags)
  let ends = alert_recovery_indices(flags)
  if starts.length() == 0 {
    1.0
  } else {
    ends.length().to_double() / starts.length().to_double()
  }
}