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

///|
pub(all) struct Diagnostic {
  code : String
  severity : DiagnosticSeverity
  message : String
  start : Int
  end : Int
} derive(Debug, Eq)

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

///|
pub fn diagnose_rule(rule : Rule) -> Array[Diagnostic] {
  let result = []
  for issue in validate_rule(rule) {
    result.push({
      code: "RULE",
      severity: Error,
      message: issue.message,
      start: 0,
      end: 0,
    })
  }
  if rule.contextual &&
    !rule.pattern.contains(":") &&
    !rule.pattern.contains(":") {
    result.push({
      code: "RULE_CONTEXT",
      severity: Warning,
      message: "contextual rule has no explicit field separator",
      start: 0,
      end: 0,
    })
  }
  if rule.confidence < 60 {
    result.push({
      code: "RULE_CONFIDENCE",
      severity: Info,
      message: "rule is intentionally below the default confidence floor",
      start: 0,
      end: 0,
    })
  }
  result
}

///|
pub fn diagnose_rules(rules : Array[Rule]) -> Array[Diagnostic] {
  let result = []
  for rule in rules {
    result.append(diagnose_rule(rule))
  }
  result
}

///|
pub fn diagnose_policy(
  policy : RedactionPolicy,
  input_length : Int,
) -> Array[Diagnostic] {
  let result = []
  if policy.min_confidence < 0 || policy.min_confidence > 100 {
    result.push({
      code: "POLICY_CONFIDENCE",
      severity: Error,
      message: "confidence must be 0..100",
      start: 0,
      end: 0,
    })
  }
  if policy.context_window < 0 {
    result.push({
      code: "POLICY_CONTEXT",
      severity: Error,
      message: "context window cannot be negative",
      start: 0,
      end: 0,
    })
  }
  for item in policy.protected_ranges {
    if !item.as_span().is_valid(input_length) {
      result.push({
        code: "POLICY_PROTECTED",
        severity: Error,
        message: item.reason,
        start: item.start,
        end: item.end,
      })
    }
  }
  result
}

///|
pub fn diagnose_findings(
  input : String,
  findings : Array[Finding],
) -> Array[Diagnostic] {
  let result = []
  for issue in validate_findings(input, findings) {
    result.push({
      code: "FINDING",
      severity: Error,
      message: issue,
      start: 0,
      end: 0,
    })
  }
  for pair in overlap_pairs(findings) {
    result.push({
      code: "OVERLAP",
      severity: Warning,
      message: pair.0 + " overlaps " + pair.1,
      start: 0,
      end: 0,
    })
  }
  result
}

///|
pub fn diagnostic_count_by_severity(
  items : Array[Diagnostic],
) -> Map[String, Int] {
  let result : Map[String, Int] = Map([])
  for item in items {
    let key = severity_name(item.severity)
    result[key] = result.get_or_default(key, 0) + 1
  }
  result
}

///|
pub fn diagnostics_have_errors(items : Array[Diagnostic]) -> Bool {
  items.any(fn(item) { item.severity == Error })
}

///|
pub fn diagnostics_to_text(items : Array[Diagnostic]) -> String {
  if items.is_empty() {
    "No diagnostics."
  } else {
    items
    .map(fn(item) {
      "[\{severity_name(item.severity)}] \{item.code}: \{item.message} (\{item.start}..\{item.end})"
    })
    .join("\n")
  }
}

///|
pub fn diagnostics_to_json(items : Array[Diagnostic]) -> String {
  let body = items
    .map(fn(item) {
      "{" +
      "\"code\":\{json_string(item.code)}," +
      "\"severity\":\{json_string(severity_name(item.severity))}," +
      "\"message\":\{json_string(item.message)}," +
      "\"start\":\{item.start},\"end\":\{item.end}" +
      "}"
    })
    .join(",")
  "[" + body + "]"
}

///|
pub fn explain_risk(kind : PhiKind) -> String {
  match kind {
    IdNumber => "Direct government identifier; use a strict policy."
    MedicalRecord => "Patient-linked medical identifier; keep audit evidence."
    Insurance => "Coverage or account identifier; review downstream exports."
    PersonName => "Direct personal name; contextual rules may be conservative."
    Phone => "Contact number; preserve separators only when required."
    Email => "Electronic contact address; verify domain-like patterns."
    Address => "Location data; institution-specific rules may be needed."
    Date => "Date may be quasi-identifying; choose policy by use case."
    Organization => "Organization field; review whether it is identifying."
    Custom(name) => "Custom category: \{name}."
  }
}

///|
pub fn rule_recommendation(rule : Rule) -> String {
  if rule.confidence >= 95 {
    "safe-default"
  } else if rule.confidence >= 80 {
    "review-context"
  } else {
    "opt-in"
  }
}

///|
pub fn rule_recommendations(rules : Array[Rule]) -> Map[String, String] {
  let result : Map[String, String] = Map([])
  for rule in rules {
    result[rule.id] = rule_recommendation(rule)
  }
  result
}

///|
pub fn document_diagnostics(
  input : String,
  policy : RedactionPolicy,
  rules : Array[Rule],
) -> Array[Diagnostic] raise DeidError {
  let result = diagnose_policy(policy, input.length())
  result.append(diagnose_rules(rules))
  let findings = scan(input, rules~)
  result.append(diagnose_findings(input, findings))
  result
}

///|
pub fn diagnostics_summary(items : Array[Diagnostic]) -> String {
  let counts = diagnostic_count_by_severity(items)
  [
    "total=\{items.length()}",
    "errors=\{counts.get_or_default("error", 0)}",
    "warnings=\{counts.get_or_default("warning", 0)}",
    "info=\{counts.get_or_default("info", 0)}",
  ].join(" ")
}