///|
/// Operational severity for normalized protocol diagnostics.
pub enum DiagnosticSeverity {
  NoticeDiagnostic
  WarningDiagnostic
  ErrorDiagnostic
  CriticalDiagnostic
} derive(Eq, Debug)

///|
pub fn DiagnosticSeverity::weight(self : DiagnosticSeverity) -> Int {
  match self {
    NoticeDiagnostic => 0
    WarningDiagnostic => 1
    ErrorDiagnostic => 2
    CriticalDiagnostic => 3
  }
}

///|
pub struct DiagnosticRecord {
  key : String
  kind : DiagnosticKind
  severity : DiagnosticSeverity
  message : String
  station : CommonAddress?
  first_seen : Int
  mut last_seen : Int
  mut occurrences : Int
  mut active : Bool
  mut acknowledged : Bool
  mut acknowledged_by : String?
} derive(Eq, Debug)

///|
pub fn DiagnosticRecord::key(self : DiagnosticRecord) -> String {
  self.key
}

///|
pub fn DiagnosticRecord::kind(self : DiagnosticRecord) -> DiagnosticKind {
  self.kind
}

///|
pub fn DiagnosticRecord::severity(
  self : DiagnosticRecord,
) -> DiagnosticSeverity {
  self.severity
}

///|
pub fn DiagnosticRecord::message(self : DiagnosticRecord) -> String {
  self.message
}

///|
pub fn DiagnosticRecord::station(self : DiagnosticRecord) -> CommonAddress? {
  self.station
}

///|
pub fn DiagnosticRecord::first_seen(self : DiagnosticRecord) -> Int {
  self.first_seen
}

///|
pub fn DiagnosticRecord::last_seen(self : DiagnosticRecord) -> Int {
  self.last_seen
}

///|
pub fn DiagnosticRecord::occurrences(self : DiagnosticRecord) -> Int {
  self.occurrences
}

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

///|
pub fn DiagnosticRecord::acknowledged(self : DiagnosticRecord) -> Bool {
  self.acknowledged
}

///|
pub fn DiagnosticRecord::acknowledged_by(self : DiagnosticRecord) -> String? {
  self.acknowledged_by
}

///|
pub struct DiagnosticCatalogMetrics {
  mut raised : Int
  mut resolved : Int
  mut acknowledged : Int
  mut deduplicated : Int
} derive(Eq, Debug)

///|
pub fn DiagnosticCatalogMetrics::new() -> DiagnosticCatalogMetrics {
  { raised: 0, resolved: 0, acknowledged: 0, deduplicated: 0 }
}

///|
pub fn DiagnosticCatalogMetrics::raised(self : DiagnosticCatalogMetrics) -> Int {
  self.raised
}

///|
pub fn DiagnosticCatalogMetrics::resolved(
  self : DiagnosticCatalogMetrics,
) -> Int {
  self.resolved
}

///|
pub fn DiagnosticCatalogMetrics::acknowledged(
  self : DiagnosticCatalogMetrics,
) -> Int {
  self.acknowledged
}

///|
pub fn DiagnosticCatalogMetrics::deduplicated(
  self : DiagnosticCatalogMetrics,
) -> Int {
  self.deduplicated
}

///|
pub struct DiagnosticCatalog {
  records : Map[String, DiagnosticRecord]
  history : Array[DiagnosticRecord]
  history_limit : Int
  metrics : DiagnosticCatalogMetrics
} derive(Debug)

///|
pub fn DiagnosticCatalog::new(
  history_limit? : Int = 4096,
) -> Result[DiagnosticCatalog, String] {
  if history_limit < 1 {
    Err("diagnostic history limit must be positive")
  } else {
    Ok({
      records: {},
      history: [],
      history_limit,
      metrics: DiagnosticCatalogMetrics::new(),
    })
  }
}

///|
pub fn DiagnosticCatalog::metrics(
  self : DiagnosticCatalog,
) -> DiagnosticCatalogMetrics {
  self.metrics
}

///|
pub fn DiagnosticCatalog::len(self : DiagnosticCatalog) -> Int {
  self.records.length()
}

///|
pub fn DiagnosticCatalog::history(
  self : DiagnosticCatalog,
) -> Array[DiagnosticRecord] {
  self.history.copy()
}

///|
pub fn DiagnosticCatalog::get(
  self : DiagnosticCatalog,
  key : String,
) -> DiagnosticRecord? {
  self.records.get(key)
}

///|
pub fn DiagnosticCatalog::active(
  self : DiagnosticCatalog,
) -> Array[DiagnosticRecord] {
  let result : Array[DiagnosticRecord] = []
  for _, record in self.records {
    if record.active() {
      result.push(record)
    }
  }
  result.sort_by((left, right) => {
    if left.severity().weight() != right.severity().weight() {
      right.severity().weight() - left.severity().weight()
    } else {
      left.key().compare(right.key())
    }
  })
  result
}

///|
fn diagnostic_kind_text(kind : DiagnosticKind) -> String {
  match kind {
    MalformedFrame => "malformed-frame"
    InvalidSequence => "invalid-sequence"
    InvalidAddress => "invalid-address"
    InvalidType => "invalid-type"
    InvalidQualifier => "invalid-qualifier"
    UnsupportedFeature => "unsupported-feature"
    WindowExhausted => "window-exhausted"
    TimerExpired => "timer-expired"
    StateViolation => "state-violation"
    TransportFailure => "transport-failure"
  }
}

///|
fn diagnostic_key(
  kind : DiagnosticKind,
  station : CommonAddress?,
  message : String,
) -> String {
  let station_text = match station {
    Some(value) => "\{value.number()}"
    None => "global"
  }
  "\{diagnostic_kind_text(kind)}:\{station_text}:\{message}"
}

///|
pub fn DiagnosticCatalog::record_diagnostic(
  self : DiagnosticCatalog,
  diagnostic : Diagnostic,
  severity : DiagnosticSeverity,
  station : CommonAddress?,
  timestamp : Int,
) -> DiagnosticRecord {
  let key = diagnostic_key(diagnostic.kind(), station, diagnostic.message())
  match self.records.get(key) {
    None => {
      let record = {
        key,
        kind: diagnostic.kind(),
        severity,
        message: diagnostic.message(),
        station,
        first_seen: timestamp,
        last_seen: timestamp,
        occurrences: 1,
        active: true,
        acknowledged: false,
        acknowledged_by: None,
      }
      self.records[key] = record
      self.metrics.raised += 1
      self.history.push(record)
      while self.history.length() > self.history_limit {
        ignore(self.history.remove(0))
      }
      record
    }
    Some(record) => {
      record.last_seen = timestamp
      record.occurrences += 1
      record.active = true
      self.records[key] = record
      self.metrics.deduplicated += 1
      record
    }
  }
}

///|
pub fn DiagnosticCatalog::resolve(
  self : DiagnosticCatalog,
  key : String,
  timestamp : Int,
) -> Result[DiagnosticRecord, String] {
  match self.records.get(key) {
    None => Err("diagnostic record does not exist")
    Some(record) => {
      record.active = false
      record.last_seen = timestamp
      self.records[key] = record
      self.metrics.resolved += 1
      self.history.push(record)
      while self.history.length() > self.history_limit {
        ignore(self.history.remove(0))
      }
      Ok(record)
    }
  }
}

///|
pub fn DiagnosticCatalog::acknowledge(
  self : DiagnosticCatalog,
  key : String,
  operator : String,
) -> Result[DiagnosticRecord, String] {
  if operator.length() == 0 {
    Err("diagnostic acknowledging operator is empty")
  } else {
    match self.records.get(key) {
      None => Err("diagnostic record does not exist")
      Some(record) => {
        record.acknowledged = true
        record.acknowledged_by = Some(operator)
        self.records[key] = record
        self.metrics.acknowledged += 1
        Ok(record)
      }
    }
  }
}

///|
pub fn DiagnosticCatalog::purge_resolved(self : DiagnosticCatalog) -> Int {
  let mut count = 0
  let keys : Array[String] = []
  for key, record in self.records {
    if !record.active() {
      keys.push(key)
    }
  }
  for key in keys {
    self.records.remove(key)
    count += 1
  }
  count
}

///|
pub fn diagnostic_severity_examples() -> Array[DiagnosticSeverity] {
  [NoticeDiagnostic, WarningDiagnostic, ErrorDiagnostic, CriticalDiagnostic]
}