///|
pub enum PrivacyClass {
  DirectIdentifier
  QuasiIdentifier
  ContactIdentifier
  AccountIdentifier
  ContextualIdentifier
  NonIdentifier
} derive(Debug, Eq, Compare)

///|
pub(all) struct PrivacySummary {
  direct : Int
  quasi : Int
  contact : Int
  account : Int
  contextual : Int
  non_identifier : Int
} derive(Debug, Eq)

///|
pub fn privacy_class(kind : PhiKind) -> PrivacyClass {
  match kind {
    IdNumber | MedicalRecord => DirectIdentifier
    Date | Address | Organization => QuasiIdentifier
    PersonName | Phone | Email => ContactIdentifier
    Insurance => AccountIdentifier
    Custom(name) =>
      if name.contains("context") {
        ContextualIdentifier
      } else {
        NonIdentifier
      }
  }
}

///|
pub fn privacy_class_name(class : PrivacyClass) -> String {
  match class {
    DirectIdentifier => "direct"
    QuasiIdentifier => "quasi"
    ContactIdentifier => "contact"
    AccountIdentifier => "account"
    ContextualIdentifier => "contextual"
    NonIdentifier => "non-identifier"
  }
}

///|
pub fn privacy_summary(findings : Array[Finding]) -> PrivacySummary {
  let mut direct = 0
  let mut quasi = 0
  let mut contact = 0
  let mut account = 0
  let mut contextual = 0
  let mut non_identifier = 0
  for item in findings {
    match privacy_class(item.kind) {
      DirectIdentifier => direct += 1
      QuasiIdentifier => quasi += 1
      ContactIdentifier => contact += 1
      AccountIdentifier => account += 1
      ContextualIdentifier => contextual += 1
      NonIdentifier => non_identifier += 1
    }
  }
  { direct, quasi, contact, account, contextual, non_identifier }
}

///|
pub fn privacy_summary_to_map(summary : PrivacySummary) -> Map[String, Int] {
  {
    "direct": summary.direct,
    "quasi": summary.quasi,
    "contact": summary.contact,
    "account": summary.account,
    "contextual": summary.contextual,
    "non-identifier": summary.non_identifier,
  }
}

///|
pub fn direct_findings(findings : Array[Finding]) -> Array[Finding] {
  findings.filter(fn(item) { privacy_class(item.kind) == DirectIdentifier })
}

///|
pub fn quasi_findings(findings : Array[Finding]) -> Array[Finding] {
  findings.filter(fn(item) { privacy_class(item.kind) == QuasiIdentifier })
}

///|
pub fn contact_findings(findings : Array[Finding]) -> Array[Finding] {
  findings.filter(fn(item) { privacy_class(item.kind) == ContactIdentifier })
}

///|
pub fn account_findings(findings : Array[Finding]) -> Array[Finding] {
  findings.filter(fn(item) { privacy_class(item.kind) == AccountIdentifier })
}

///|
pub fn privacy_class_counts(findings : Array[Finding]) -> Map[String, Int] {
  let result : Map[String, Int] = Map([])
  for item in findings {
    let key = privacy_class_name(privacy_class(item.kind))
    result[key] = result.get_or_default(key, 0) + 1
  }
  result
}

///|
pub fn high_risk_findings(findings : Array[Finding]) -> Array[Finding] {
  findings.filter(fn(item) {
    let risk = risk_level(item)
    risk == High || risk == Critical
  })
}

///|
pub fn contains_direct_identifier(findings : Array[Finding]) -> Bool {
  !direct_findings(findings).is_empty()
}

///|
pub fn all_high_confidence(findings : Array[Finding], threshold : Int) -> Bool {
  findings.all(fn(item) { item.confidence >= threshold })
}

///|
pub fn output_contains_original_identifier(result : DeidResult) -> Bool {
  result.findings.any(fn(item) { result.text.contains(item.text) })
}

///|
pub fn output_is_redacted(result : DeidResult) -> Bool {
  !output_contains_original_identifier(result)
}

///|
pub fn output_contract_issues(
  input : String,
  result : DeidResult,
) -> Array[String] {
  let issues = []
  if !validate_findings(input, result.findings).is_empty() {
    issues.push("invalid findings")
  }
  if !offsets_are_monotonic(result.offsets) {
    issues.push("non-monotonic offsets")
  }
  if !redaction_preserves_newlines(input, result) {
    issues.push("newline count changed")
  }
  if result.audit.applied_count != result.offsets.length() {
    issues.push("audit applied mismatch")
  }
  if !result.findings.is_empty() && !output_is_redacted(result) {
    issues.push("original identifier remains")
  }
  issues
}

///|
pub fn output_contract_passes(input : String, result : DeidResult) -> Bool {
  output_contract_issues(input, result).is_empty()
}

///|
pub fn audit_kind_count(report : AuditReport, kind : PhiKind) -> Int {
  report.counts.get_or_default(phi_kind_name(kind), 0)
}

///|
pub fn audit_risk_count(report : AuditReport, level : RiskLevel) -> Int {
  report.findings.filter(fn(item) { risk_level(item) == level }).length()
}

///|
pub fn audit_rule_ids(report : AuditReport) -> Array[String] {
  let result = []
  for item in report.findings {
    if !result.contains(item.rule_id) {
      result.push(item.rule_id)
    }
  }
  result
}

///|
pub fn audit_finding_ids(report : AuditReport) -> Array[String] {
  report.findings.map(fn(item) { item.id })
}

///|
pub fn audit_has_kind(report : AuditReport, kind : PhiKind) -> Bool {
  report.findings.any(fn(item) { item.kind == kind })
}

///|
pub fn audit_has_rule(report : AuditReport, rule_id : String) -> Bool {
  report.findings.any(fn(item) { item.rule_id == rule_id })
}

///|
pub fn audit_summary_json(report : AuditReport) -> String {
  "{" +
  "\"finding_count\":\{report.finding_count}," +
  "\"applied_count\":\{report.applied_count}," +
  "\"risk\":\{json_string("\{audit_risk_level(report)}")}," +
  "\"coverage\":\{audit_offset_coverage(report)}" +
  "}"
}