///|
/// One explicit PSL rule membership and its observed selection count.
pub struct RuleCoverageEntry {
  rule_ : String
  section_ : RuleSection
  kind_ : RuleKind
  selected_count_ : Int
}

///|
pub fn RuleCoverageEntry::rule(self : RuleCoverageEntry) -> String {
  self.rule_
}

///|
pub fn RuleCoverageEntry::section(self : RuleCoverageEntry) -> RuleSection {
  self.section_
}

///|
pub fn RuleCoverageEntry::kind(self : RuleCoverageEntry) -> RuleKind {
  self.kind_
}

///|
pub fn RuleCoverageEntry::selected_count(self : RuleCoverageEntry) -> Int {
  self.selected_count_
}

///|
/// Selection frequency of every explicit rule for one hostname inventory.
pub struct RuleCoverageReport {
  entries_ : ReadOnlyArray[RuleCoverageEntry]
  scanned_count_ : Int
  explicit_count_ : Int
  implicit_count_ : Int
  unlisted_count_ : Int
  invalid_count_ : Int
  observed_rule_count_ : Int
}

///|
pub fn RuleCoverageReport::entries(
  self : RuleCoverageReport,
) -> ReadOnlyArray[RuleCoverageEntry] {
  self.entries_
}

///|
pub fn RuleCoverageReport::scanned_count(self : RuleCoverageReport) -> Int {
  self.scanned_count_
}

///|
pub fn RuleCoverageReport::explicit_count(self : RuleCoverageReport) -> Int {
  self.explicit_count_
}

///|
pub fn RuleCoverageReport::implicit_count(self : RuleCoverageReport) -> Int {
  self.implicit_count_
}

///|
pub fn RuleCoverageReport::unlisted_count(self : RuleCoverageReport) -> Int {
  self.unlisted_count_
}

///|
pub fn RuleCoverageReport::invalid_count(self : RuleCoverageReport) -> Int {
  self.invalid_count_
}

///|
pub fn RuleCoverageReport::observed_rule_count(
  self : RuleCoverageReport,
) -> Int {
  self.observed_rule_count_
}

///|
pub fn RuleCoverageReport::unobserved_rule_count(
  self : RuleCoverageReport,
) -> Int {
  self.entries_.length() - self.observed_rule_count_
}

///|
fn coverage_key(rule : String, section : RuleSection) -> String {
  let prefix = match section {
    UnsectionedRule => "u:"
    IcannSection => "i:"
    PrivateSection => "p:"
  }
  prefix + rule
}

///|
fn coverage_kind(rule : String) -> RuleKind {
  if rule.has_prefix("!") {
    ExceptionRule
  } else if rule.has_prefix("*.") {
    WildcardRule
  } else {
    ExactRule
  }
}

///|
fn append_coverage_rules(
  rules : Array[String],
  section : RuleSection,
  keys : Array[(String, RuleSection)],
  counts : Array[Int],
  positions : Map[String, Int],
) -> Unit {
  for rule in rules {
    positions.set(coverage_key(rule, section), keys.length())
    keys.push((rule, section))
    counts.push(0)
  }
}

///|
/// Count which explicit rules prevail for a hostname inventory.
///
/// Each rule-section membership is counted separately. Invalid hostnames and
/// unlisted suffixes are counted but do not interrupt later inputs. A zero
/// count means only that the supplied inventory did not select that rule; it
/// does not prove the rule is unreachable or unnecessary.
pub fn SuffixList::analyze_rule_coverage(
  self : SuffixList,
  domains : Array[String],
  options : LookupOptions,
) -> RuleCoverageReport {
  let (unsectioned, icann, private_rules) = self.sorted_rules_by_section()
  let keys : Array[(String, RuleSection)] = []
  let counts : Array[Int] = []
  let positions : Map[String, Int] = Map([])
  append_coverage_rules(unsectioned, UnsectionedRule, keys, counts, positions)
  append_coverage_rules(icann, IcannSection, keys, counts, positions)
  append_coverage_rules(private_rules, PrivateSection, keys, counts, positions)
  let mut explicit_count = 0
  let mut implicit_count = 0
  let mut unlisted_count = 0
  let mut invalid_count = 0
  for domain in domains {
    match self.lookup_with_options(domain, options) {
      Ok(result) =>
        match result.rule_section() {
          None => implicit_count = implicit_count + 1
          Some(section) => {
            explicit_count = explicit_count + 1
            let index = positions
              .get(coverage_key(result.matched_rule(), section))
              .unwrap()
            counts[index] = counts[index] + 1
          }
        }
      Err(UnlistedSuffix(_)) => unlisted_count = unlisted_count + 1
      Err(_) => invalid_count = invalid_count + 1
    }
  }
  let entries : Array[RuleCoverageEntry] = []
  let mut observed_rule_count = 0
  for index, pair in keys {
    let (rule, section) = pair
    let selected_count = counts[index]
    if selected_count > 0 {
      observed_rule_count = observed_rule_count + 1
    }
    entries.push({
      rule_: rule,
      section_: section,
      kind_: coverage_kind(rule),
      selected_count_: selected_count,
    })
  }
  {
    entries_: ReadOnlyArray::from_array(entries),
    scanned_count_: domains.length(),
    explicit_count_: explicit_count,
    implicit_count_: implicit_count,
    unlisted_count_: unlisted_count,
    invalid_count_: invalid_count,
    observed_rule_count_: observed_rule_count,
  }
}

///|
/// Export every explicit rule membership, including unobserved ones, as CSV.
pub fn RuleCoverageReport::to_csv(self : RuleCoverageReport) -> String {
  let output = StringBuilder()
  output.write_string("\"section\",\"rule\",\"kind\",\"selected_count\"\n")
  for entry in self.entries_ {
    write_csv_row(output, [
      section_name(entry.section_),
      entry.rule_,
      rule_kind_name(entry.kind_),
      entry.selected_count_.to_string(),
    ])
  }
  output.to_string()
}