///|
pub enum DiagnosticSeverity {
  Info
  Warning
  Error
} derive(Eq, Debug)

///|
pub enum DiagnosticKind {
  DuplicateNetwork
  CoveredNetwork
  OverlappingNetwork
  SpecialPurposeAddress
  EmptyPolicy
} derive(Eq, Debug)

///|
pub struct CidrDiagnostic {
  severity : DiagnosticSeverity
  kind : DiagnosticKind
  message : String
} derive(Eq, Debug)

///|
pub struct DiagnosticReport {
  diagnostics : Array[CidrDiagnostic]
} derive(Debug)

///|
pub fn DiagnosticSeverity::to_string(self : DiagnosticSeverity) -> String {
  match self {
    Info => "info"
    Warning => "warning"
    Error => "error"
  }
}

///|
pub fn DiagnosticKind::to_string(self : DiagnosticKind) -> String {
  match self {
    DuplicateNetwork => "duplicate-network"
    CoveredNetwork => "covered-network"
    OverlappingNetwork => "overlapping-network"
    SpecialPurposeAddress => "special-purpose-address"
    EmptyPolicy => "empty-policy"
  }
}

///|
pub fn CidrDiagnostic::new(
  severity : DiagnosticSeverity,
  kind : DiagnosticKind,
  message : String,
) -> CidrDiagnostic {
  { severity, kind, message }
}

///|
pub fn CidrDiagnostic::to_string(self : CidrDiagnostic) -> String {
  "[\{self.severity.to_string()}] \{self.kind.to_string()}: \{self.message}"
}

///|
pub fn DiagnosticReport::new() -> DiagnosticReport {
  { diagnostics: Array::new() }
}

///|
pub fn DiagnosticReport::push(
  self : DiagnosticReport,
  diagnostic : CidrDiagnostic,
) -> DiagnosticReport {
  let diagnostics = self.diagnostics.copy()
  diagnostics.push(diagnostic)
  { diagnostics, }
}

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

///|
pub fn DiagnosticReport::is_empty(self : DiagnosticReport) -> Bool {
  self.len() == 0
}

///|
pub fn DiagnosticReport::diagnostics(
  self : DiagnosticReport,
) -> Array[CidrDiagnostic] {
  self.diagnostics.copy()
}

///|
pub fn DiagnosticReport::error_count(self : DiagnosticReport) -> Int {
  count_severity(self.diagnostics, Error)
}

///|
pub fn DiagnosticReport::warning_count(self : DiagnosticReport) -> Int {
  count_severity(self.diagnostics, Warning)
}

///|
pub fn DiagnosticReport::info_count(self : DiagnosticReport) -> Int {
  count_severity(self.diagnostics, Info)
}

///|
pub fn DiagnosticReport::to_string(self : DiagnosticReport) -> String {
  if self.diagnostics.length() == 0 {
    "no diagnostics"
  } else {
    let mut text = ""
    let mut index = 0
    for diagnostic in self.diagnostics {
      if index > 0 {
        text = text + "\n"
      }
      text = text + diagnostic.to_string()
      index = index + 1
    }
    text
  }
}

///|
pub fn diagnose_networks(networks : Array[Ipv4Network]) -> DiagnosticReport {
  let mut report = DiagnosticReport::new()
  let mut i = 0
  while i < networks.length() {
    let mut j = i + 1
    while j < networks.length() {
      if networks[i] == networks[j] {
        report = report.push(
          CidrDiagnostic::new(
            Warning,
            DuplicateNetwork,
            "duplicate \{networks[i].to_string()}",
          ),
        )
      } else if networks[i].contains_network(networks[j]) {
        report = report.push(
          CidrDiagnostic::new(
            Info,
            CoveredNetwork,
            "\{networks[j].to_string()} is covered by \{networks[i].to_string()}",
          ),
        )
      } else if networks[j].contains_network(networks[i]) {
        report = report.push(
          CidrDiagnostic::new(
            Info,
            CoveredNetwork,
            "\{networks[i].to_string()} is covered by \{networks[j].to_string()}",
          ),
        )
      } else if networks[i].overlaps(networks[j]) {
        report = report.push(
          CidrDiagnostic::new(
            Warning,
            OverlappingNetwork,
            "\{networks[i].to_string()} overlaps \{networks[j].to_string()}",
          ),
        )
      }
      j = j + 1
    }
    i = i + 1
  }
  report
}

///|
pub fn diagnose_addresses(addresses : Array[Ipv4Address]) -> DiagnosticReport {
  let mut report = DiagnosticReport::new()
  for address in addresses {
    if address.is_special_purpose() {
      report = report.push(
        CidrDiagnostic::new(
          Info,
          SpecialPurposeAddress,
          "\{address.to_string()} is \{address.scope().to_string()}",
        ),
      )
    }
  }
  report
}

///|
pub fn diagnose_acl(policy : AclPolicy) -> DiagnosticReport {
  let mut report = DiagnosticReport::new()
  if policy.len() == 0 {
    report = report.push(
      CidrDiagnostic::new(Warning, EmptyPolicy, "policy has no explicit rules"),
    )
  }
  report
}

///|
fn count_severity(
  items : Array[CidrDiagnostic],
  severity : DiagnosticSeverity,
) -> Int {
  let mut count = 0
  for item in items {
    if item.severity == severity {
      count = count + 1
    }
  }
  count
}