///|
/// Aggregate facts about a rule set.
pub struct CoverageStats {
  total_rules : Int
  allow_rules : Int
  deny_rules : Int
  private_rules : Int
  public_rules : Int
  special_rules : Int
  widest_prefix : Int
  narrowest_prefix : Int
} derive(Eq, Debug)

///|
pub fn CoverageStats::total_rules(self : CoverageStats) -> Int {
  self.total_rules
}

///|
pub fn CoverageStats::allow_rules(self : CoverageStats) -> Int {
  self.allow_rules
}

///|
pub fn CoverageStats::deny_rules(self : CoverageStats) -> Int {
  self.deny_rules
}

///|
pub fn CoverageStats::private_rules(self : CoverageStats) -> Int {
  self.private_rules
}

///|
pub fn CoverageStats::public_rules(self : CoverageStats) -> Int {
  self.public_rules
}

///|
pub fn CoverageStats::special_rules(self : CoverageStats) -> Int {
  self.special_rules
}

///|
pub fn CoverageStats::widest_prefix(self : CoverageStats) -> Int {
  self.widest_prefix
}

///|
pub fn CoverageStats::narrowest_prefix(self : CoverageStats) -> Int {
  self.narrowest_prefix
}

///|
pub fn CoverageStats::summary(self : CoverageStats) -> String {
  "rules=" +
  self.total_rules.to_string() +
  ", allow=" +
  self.allow_rules.to_string() +
  ", deny=" +
  self.deny_rules.to_string() +
  ", private=" +
  self.private_rules.to_string() +
  ", public=" +
  self.public_rules.to_string() +
  ", special=" +
  self.special_rules.to_string() +
  ", widest=/" +
  self.widest_prefix.to_string() +
  ", narrowest=/" +
  self.narrowest_prefix.to_string()
}

///|
pub fn RuleSet::coverage_stats(self : RuleSet) -> CoverageStats {
  let mut allow_rules = 0
  let mut deny_rules = 0
  let mut private_rules = 0
  let mut public_rules = 0
  let mut special_rules = 0
  let mut widest_prefix = 32
  let mut narrowest_prefix = 0
  if self.rules.length() == 0 {
    widest_prefix = 0
    narrowest_prefix = 0
  }
  for rule in self.rules {
    if rule.action() == Allow {
      allow_rules = allow_rules + 1
    } else {
      deny_rules = deny_rules + 1
    }
    let scope = rule.block().network().scope_label()
    if scope == "private" {
      private_rules = private_rules + 1
    } else if scope == "public" {
      public_rules = public_rules + 1
    } else {
      special_rules = special_rules + 1
    }
    if rule.block().prefix() < widest_prefix {
      widest_prefix = rule.block().prefix()
    }
    if rule.block().prefix() > narrowest_prefix {
      narrowest_prefix = rule.block().prefix()
    }
  }
  {
    total_rules: self.rules.length(),
    allow_rules,
    deny_rules,
    private_rules,
    public_rules,
    special_rules,
    widest_prefix,
    narrowest_prefix,
  }
}

///|
pub fn RuleSet::matching_rules(self : RuleSet, ip : IPv4) -> Array[Rule] {
  let matched : Array[Rule] = []
  for rule in self.rules {
    if rule.matches(ip) {
      matched.push(rule)
    }
  }
  matched
}

///|
pub fn RuleSet::explain_ip(self : RuleSet, ip : IPv4) -> String {
  let matched = self.matching_rules(ip)
  let mut output = "IP " + ip.to_dotted() + " scope=" + ip.scope_label() + "\n"
  if matched.length() == 0 {
    output = output + "decision: no matching rule\n"
  } else {
    output = output + "decision: " + self.decide(ip).summary() + "\n"
    output = output + "matching rules:\n"
    for rule in matched {
      output = output + "- " + rule.summary() + "\n"
    }
  }
  output
}

///|
pub fn RuleSet::explain_many(self : RuleSet, probes : Array[String]) -> String {
  let mut output = ""
  for probe in probes {
    match IPv4::parse(probe) {
      Ok(ip) => output = output + self.explain_ip(ip) + "\n"
      Err(err) => output = output + "invalid probe " + probe + ": " + err + "\n"
    }
  }
  output
}

///|
pub fn RuleSet::first_rule_for_block(
  self : RuleSet,
  block : CidrBlock,
) -> String {
  for rule in self.rules {
    if rule.block().overlaps(block) {
      return rule.id() +
        " " +
        rule.action().label() +
        " " +
        rule.block().to_string()
    }
  }
  "no overlapping rule"
}

///|
pub fn RuleSet::inventory_report(self : RuleSet) -> String {
  let stats = self.coverage_stats()
  let mut output = "MoonCIDR inventory\n"
  output = output + stats.summary() + "\n"
  output = output + "rules:\n"
  for rule in self.rules {
    output = output + "- " + rule.summary() + "\n"
  }
  output
}