///|
/// Groups nearby alert events into incidents for operators.
pub struct Incident {
  metric : String
  mut first_timestamp : Int64
  mut last_timestamp : Int64
  mut alerts : Int
  mut critical : Int
  mut maximum_score : Double
  mut direction : ChangeDirection
}

///|
pub fn Incident::empty(metric : String) -> Incident {
  {
    metric,
    first_timestamp: 0L,
    last_timestamp: 0L,
    alerts: 0,
    critical: 0,
    maximum_score: 0.0,
    direction: Unknown,
  }
}

///|
pub fn Incident::add(self : Incident, event : AlertEvent) -> Unit {
  if self.alerts == 0 {
    self.first_timestamp = event.point.timestamp
    self.direction = event.point.direction
  }
  self.last_timestamp = event.point.timestamp
  self.alerts += 1
  if severity_name(event.point.severity) == "critical" {
    self.critical += 1
  }
  if event.point.score > self.maximum_score {
    self.maximum_score = event.point.score
    self.direction = event.point.direction
  }
}

///|
pub fn Incident::duration(self : Incident) -> Int64 {
  self.last_timestamp - self.first_timestamp
}

///|
pub fn Incident::is_critical(self : Incident) -> Bool {
  self.critical > 0 || self.maximum_score >= 0.9
}

///|
pub fn Incident::summary(self : Incident) -> String {
  self.metric +
  "@" +
  self.first_timestamp.to_string() +
  "-" +
  self.last_timestamp.to_string() +
  " alerts=" +
  self.alerts.to_string() +
  " score=" +
  self.maximum_score.to_string()
}

///|
pub fn cluster_incidents(
  events : Array[AlertEvent],
  maximum_gap : Int64,
) -> Array[Incident] {
  let result : Array[Incident] = []
  let gap = if maximum_gap < 0L { 0L } else { maximum_gap }
  for event in events {
    let mut matched = false
    if result.length() > 0 {
      let last = result[result.length() - 1]
      if last.metric == event.metric &&
        event.point.timestamp - last.last_timestamp <= gap {
        last.add(event)
        matched = true
      }
    }
    if !matched {
      let incident = Incident::empty(event.metric)
      incident.add(event)
      result.push(incident)
    }
  }
  result
}

///|
pub fn critical_incident_count(incidents : Array[Incident]) -> Int {
  let mut count = 0
  for incident in incidents {
    if incident.is_critical() {
      count += 1
    }
  }
  count
}

///|
pub fn incident_alert_count(incidents : Array[Incident]) -> Int {
  let mut count = 0
  for incident in incidents {
    count += incident.alerts
  }
  count
}

///|
pub fn incidents_markdown(incidents : Array[Incident]) -> String {
  let mut output = "| metric | first | last | alerts | critical | max score | direction |\n|---|---:|---:|---:|---:|---:|---|\n"
  for incident in incidents {
    output = output +
      "| " +
      incident.metric +
      " | " +
      incident.first_timestamp.to_string() +
      " | " +
      incident.last_timestamp.to_string() +
      " | " +
      incident.alerts.to_string() +
      " | " +
      incident.critical.to_string() +
      " | " +
      incident.maximum_score.to_string() +
      " | " +
      direction_name(incident.direction) +
      " |\n"
  }
  output
}

///|
pub fn incident_rate(
  incidents : Array[Incident],
  start : Int64,
  end : Int64,
) -> Double {
  if end <= start {
    0.0
  } else {
    incidents.length().to_double() / (end - start).to_double()
  }
}