///|
/// Severity assigned to an audit finding.
pub(all) enum Severity {
  Info
  Warning
  Critical
} derive(Eq, Debug)

///|
pub fn Severity::label(self : Severity) -> String {
  match self {
    Info => "info"
    Warning => "warning"
    Critical => "critical"
  }
}

///|
pub fn Severity::score(self : Severity) -> Int {
  match self {
    Info => 1
    Warning => 4
    Critical => 10
  }
}

///|
/// Classifies why a rule deserves attention.
pub(all) enum FindingKind {
  DuplicateRule
  ShadowedRule
  ConflictingOverlap
  RedundantOverlap
  TooWideAllow
  GlobalDeny
  PrivateRange
  LoopbackRange
  LinkLocalRange
  MulticastRange
  NonCanonicalCidr
  ParseError
} derive(Eq, Debug)

///|
pub fn FindingKind::label(self : FindingKind) -> String {
  match self {
    DuplicateRule => "duplicate_rule"
    ShadowedRule => "shadowed_rule"
    ConflictingOverlap => "conflicting_overlap"
    RedundantOverlap => "redundant_overlap"
    TooWideAllow => "too_wide_allow"
    GlobalDeny => "global_deny"
    PrivateRange => "private_range"
    LoopbackRange => "loopback_range"
    LinkLocalRange => "link_local_range"
    MulticastRange => "multicast_range"
    NonCanonicalCidr => "non_canonical_cidr"
    ParseError => "parse_error"
  }
}

///|
pub struct Finding {
  kind : FindingKind
  severity : Severity
  rule_id : String
  related_rule_id : String
  message : String
} derive(Eq, Debug)

///|
pub fn Finding::new(
  kind : FindingKind,
  severity : Severity,
  rule_id : String,
  message : String,
  related_rule_id? : String = "",
) -> Finding {
  { kind, severity, rule_id, related_rule_id, message }
}

///|
pub fn Finding::kind(self : Finding) -> FindingKind {
  self.kind
}

///|
pub fn Finding::severity(self : Finding) -> Severity {
  self.severity
}

///|
pub fn Finding::rule_id(self : Finding) -> String {
  self.rule_id
}

///|
pub fn Finding::related_rule_id(self : Finding) -> String {
  self.related_rule_id
}

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

///|
pub struct Decision {
  matched : Bool
  action : RuleAction
  rule_id : String
  block : String
} derive(Eq, Debug)

///|
pub fn Decision::matched(self : Decision) -> Bool {
  self.matched
}

///|
pub fn Decision::action(self : Decision) -> RuleAction {
  self.action
}

///|
pub fn Decision::rule_id(self : Decision) -> String {
  self.rule_id
}

///|
pub fn Decision::block(self : Decision) -> String {
  self.block
}

///|
pub fn Decision::summary(self : Decision) -> String {
  if self.matched {
    self.action.label() + " by " + self.rule_id + " (" + self.block + ")"
  } else {
    "no matching rule"
  }
}

///|
pub struct AuditReport {
  rules : Array[Rule]
  findings : Array[Finding]
  parse_errors : Array[String]
} derive(Debug)

///|
pub fn AuditReport::rules(self : AuditReport) -> Array[Rule] {
  self.rules
}

///|
pub fn AuditReport::findings(self : AuditReport) -> Array[Finding] {
  self.findings
}

///|
pub fn AuditReport::parse_errors(self : AuditReport) -> Array[String] {
  self.parse_errors
}

///|
pub fn AuditReport::finding_count(self : AuditReport) -> Int {
  self.findings.length()
}

///|
pub fn AuditReport::critical_count(self : AuditReport) -> Int {
  self.count_severity(Critical)
}

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

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

///|
pub fn AuditReport::count_kind(self : AuditReport, kind : FindingKind) -> Int {
  let mut count = 0
  for finding in self.findings {
    if finding.kind() == kind {
      count = count + 1
    }
  }
  count
}

///|
pub fn AuditReport::count_severity(
  self : AuditReport,
  severity : Severity,
) -> Int {
  let mut count = 0
  for finding in self.findings {
    if finding.severity() == severity {
      count = count + 1
    }
  }
  count
}

///|
pub fn AuditReport::risk_score(self : AuditReport) -> Int {
  let mut score = 0
  for finding in self.findings {
    score = score + finding.severity().score()
  }
  score + self.parse_errors.length() * 10
}

///|
pub fn AuditReport::risk_level(self : AuditReport) -> String {
  let score = self.risk_score()
  if self.critical_count() > 0 || score >= 30 {
    "high"
  } else if score >= 10 {
    "medium"
  } else if score > 0 {
    "low"
  } else {
    "clean"
  }
}

///|
pub fn AuditReport::recommended_action(self : AuditReport) -> String {
  if self.parse_errors.length() > 0 || self.critical_count() > 0 {
    "fix_before_release"
  } else if self.warning_count() > 0 {
    "review"
  } else {
    "accept"
  }
}

///|
pub struct RuleSet {
  rules : Array[Rule]
  parse_errors : Array[String]
} derive(Debug)

///|
pub fn RuleSet::new(rules : Array[Rule]) -> RuleSet {
  { rules, parse_errors: [] }
}

///|
pub fn RuleSet::from_lines(lines : Array[String]) -> RuleSet {
  let rules : Array[Rule] = []
  let errors : Array[String] = []
  for index = 0; index < lines.length(); index = index + 1 {
    let line = trim_ascii(lines[index])
    if line == "" || starts_with(line, "#") {
      continue
    }
    match Rule::parse("R" + (rules.length() + 1).to_string(), line) {
      Ok(rule) => rules.push(rule)
      Err(err) => errors.push("line " + (index + 1).to_string() + ": " + err)
    }
  }
  { rules, parse_errors: errors }
}

///|
pub fn RuleSet::rules(self : RuleSet) -> Array[Rule] {
  self.rules
}

///|
pub fn RuleSet::parse_errors(self : RuleSet) -> Array[String] {
  self.parse_errors
}

///|
pub fn RuleSet::decide(self : RuleSet, ip : IPv4) -> Decision {
  for rule in self.rules {
    if rule.matches(ip) {
      return {
        matched: true,
        action: rule.action(),
        rule_id: rule.id(),
        block: rule.block().to_string(),
      }
    }
  }
  { matched: false, action: Deny, rule_id: "", block: "" }
}

///|
pub fn RuleSet::audit(self : RuleSet) -> AuditReport {
  self.audit_with_policy(AuditPolicy::gateway())
}

///|
pub fn RuleSet::audit_with_policy(
  self : RuleSet,
  policy : AuditPolicy,
) -> AuditReport {
  let findings : Array[Finding] = []
  for index = 0; index < self.rules.length(); index = index + 1 {
    let rule = self.rules[index]
    analyze_single_rule(rule, policy, findings)
    for prev_index = 0; prev_index < index; prev_index = prev_index + 1 {
      analyze_pair(self.rules[prev_index], rule, policy, findings)
    }
  }
  for err in self.parse_errors {
    findings.push(Finding::new(ParseError, Critical, "", err))
  }
  { rules: self.rules, findings, parse_errors: self.parse_errors }
}

///|
pub fn audit_rules(rules : Array[Rule]) -> AuditReport {
  RuleSet::new(rules).audit()
}

///|
pub fn audit_rules_with_policy(
  rules : Array[Rule],
  policy : AuditPolicy,
) -> AuditReport {
  RuleSet::new(rules).audit_with_policy(policy)
}

///|
pub fn parse_rules(lines : Array[String]) -> RuleSet {
  RuleSet::from_lines(lines)
}

///|
fn analyze_single_rule(
  rule : Rule,
  policy : AuditPolicy,
  findings : Array[Finding],
) -> Unit {
  let block = rule.block()
  if policy.flag_non_canonical() && !rule.is_canonical() {
    findings.push(
      Finding::new(
        NonCanonicalCidr,
        Info,
        rule.id(),
        "input " +
        rule.source_block() +
        " was normalized to " +
        block.to_string(),
      ),
    )
  }
  if rule.action() == Allow && block.prefix() <= policy.wide_allow_prefix() {
    findings.push(
      Finding::new(
        TooWideAllow,
        Critical,
        rule.id(),
        "allow rule covers a very wide network: " + block.to_string(),
      ),
    )
  }
  if rule.action() == Deny && block.prefix() == 0 {
    findings.push(
      Finding::new(
        GlobalDeny,
        Critical,
        rule.id(),
        "deny rule blocks the entire IPv4 space",
      ),
    )
  }
  let scope = block.network().scope_label()
  if policy.flag_private() && scope == "private" {
    findings.push(
      Finding::new(
        PrivateRange,
        Info,
        rule.id(),
        "rule targets private address space: " + block.to_string(),
      ),
    )
  } else if policy.flag_special_ranges() && scope == "loopback" {
    findings.push(
      Finding::new(
        LoopbackRange,
        Warning,
        rule.id(),
        "rule targets loopback address space: " + block.to_string(),
      ),
    )
  } else if policy.flag_special_ranges() && scope == "link_local" {
    findings.push(
      Finding::new(
        LinkLocalRange,
        Warning,
        rule.id(),
        "rule targets link-local address space: " + block.to_string(),
      ),
    )
  } else if policy.flag_special_ranges() && scope == "multicast" {
    findings.push(
      Finding::new(
        MulticastRange,
        Warning,
        rule.id(),
        "rule targets multicast address space: " + block.to_string(),
      ),
    )
  }
}

///|
fn analyze_pair(
  previous : Rule,
  current : Rule,
  policy : AuditPolicy,
  findings : Array[Finding],
) -> Unit {
  if previous.block().equal_range(current.block()) {
    findings.push(
      Finding::new(
        DuplicateRule,
        Warning,
        current.id(),
        "rule duplicates earlier rule " +
        previous.id() +
        ": " +
        current.block().to_string(),
        related_rule_id=previous.id(),
      ),
    )
  }
  if previous.block().contains_block(current.block()) {
    findings.push(
      Finding::new(
        ShadowedRule,
        if previous.action() == current.action() {
          Warning
        } else {
          Critical
        },
        current.id(),
        "rule is already covered by earlier " +
        previous.action().label() +
        " rule " +
        previous.id(),
        related_rule_id=previous.id(),
      ),
    )
  } else if previous.block().overlaps(current.block()) {
    if previous.same_decision(current) && policy.flag_redundant_overlap() {
      findings.push(
        Finding::new(
          RedundantOverlap,
          Info,
          current.id(),
          "rule overlaps earlier same-action rule " + previous.id(),
          related_rule_id=previous.id(),
        ),
      )
    } else {
      findings.push(
        Finding::new(
          ConflictingOverlap,
          Critical,
          current.id(),
          "rule overlaps earlier opposite-action rule " + previous.id(),
          related_rule_id=previous.id(),
        ),
      )
    }
  }
}

///|
fn starts_with(input : String, prefix : String) -> Bool {
  let chars = input.to_array()
  let prefix_chars = prefix.to_array()
  if prefix_chars.length() > chars.length() {
    false
  } else {
    let mut ok = true
    for index = 0; index < prefix_chars.length(); index = index + 1 {
      if chars[index] != prefix_chars[index] {
        ok = false
      }
    }
    ok
  }
}