///|
/// The severity of a finding. Errors sort first.
pub(all) enum Severity {
  Error
  Warning
  Info
} derive(Eq, Compare, Hash, Debug)

///|
/// The short form of a severity, as reports print it: `error`, `warn` or `info`.
pub fn Severity::to_string(self : Severity) -> String {
  match self {
    Error => "error"
    Warning => "warn"
    Info => "info"
  }
}

///|
/// What a finding is about.
pub(all) enum FindingSubject {
  /// A document of a bundle.
  InBundle(BundleLabel, BundlePath)
  /// A Markdown file outside every bundle.
  Stray(StorePath)
  /// A profile, by name, for a profile definition that breaks a rule.
  ProfileSubject(String)
  /// The validation config.
  ConfigSubject
} derive(Eq, Debug)

///|
/// The bundle label of the subject, when it is a document of a bundle.
pub fn FindingSubject::bundle(self : FindingSubject) -> BundleLabel? {
  match self {
    InBundle(label, _) => Some(label)
    _ => None
  }
}

///|
/// The path text of the subject: a bundle path or a store path. A profile or
/// the config has no path.
pub fn FindingSubject::path_text(self : FindingSubject) -> String? {
  match self {
    InBundle(_, path) => Some(path.to_string())
    Stray(path) => Some(path.to_string())
    ProfileSubject(_) | ConfigSubject => None
  }
}

///|
fn FindingSubject::sort_key(self : FindingSubject) -> (Int32, String, String) {
  match self {
    ConfigSubject => (0, "", "")
    ProfileSubject(name) => (1, name, "")
    InBundle(label, path) => (2, label.to_string(), path.to_string())
    Stray(path) => (3, "", path.to_string())
  }
}

///|
/// One validation result. Only `validate` makes findings.
pub struct Finding {
  severity : Severity
  rule : RuleName
  subject : FindingSubject
  line : Int32?
  message : String
  hint : String?
} derive(Eq, Debug)

///|
/// What a rule reports. Validation adds the rule name and the severity.
pub struct Issue {
  subject : FindingSubject
  line : Int32?
  message : String
  hint : String?
} derive(Eq, Debug)

///|
/// An issue about one document of a bundle.
pub fn Issue::at(
  bundle : Bundle,
  doc : Document,
  message : String,
  line? : Int32,
  hint? : String,
) -> Issue {
  { subject: InBundle(bundle.label(), doc.path()), line, message, hint, }
}

///|
/// An issue about a document of a bundle, by its key.
pub fn Issue::in_bundle(
  key : DocKey,
  message : String,
  line? : Int32,
  hint? : String,
) -> Issue {
  { subject: InBundle(key.bundle, key.path), line, message, hint, }
}

///|
/// An issue about a Markdown file outside every bundle.
pub fn Issue::stray(
  path : StorePath,
  message : String,
  hint? : String,
) -> Issue {
  { subject: Stray(path), line: None, message, hint, }
}

///|
/// The inputs that every rule can read.
pub struct ValidationContext {
  knowledge : Knowledge
  graph : Graph
  now : Instant
  options : Options
  vocabulary : Vocabulary
  profiles : ReadOnlyArray[&Profile]
}

///|
/// One named check with a default severity.
struct Rule {
  name : RuleName
  severity : Severity
  check : (ValidationContext) -> Array[Issue]
}

///|
pub fn Rule::Rule(
  name : RuleName,
  severity : Severity,
  check : (ValidationContext) -> Array[Issue],
) -> Rule {
  { name, severity, check, }
}

///|
pub fn Rule::name(self : Rule) -> RuleName {
  self.name
}

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

///|
/// A named group of rules. The name is a label for reports.
struct RuleSet {
  name : String
  rules : ReadOnlyArray[Rule]
}

///|
pub fn RuleSet::RuleSet(name : String, rules : ArrayView[Rule]) -> RuleSet {
  { name, rules: ReadOnlyArray::from_array(rules.to_owned()), }
}

///|
pub fn RuleSet::name(self : RuleSet) -> String {
  self.name
}

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

///|
/// How a caller adjusts validation. The config keeps its own copies, so later
/// changes to the arrays that built it do not change it.
struct ValidationConfig {
  overrides : ReadOnlyArray[(RuleName, Severity)]
  disabled : ReadOnlyArray[RuleName]
}

///|
pub fn ValidationConfig::ValidationConfig(
  overrides~ : ArrayView[(RuleName, Severity)] = [],
  disabled~ : ArrayView[RuleName] = [],
) -> ValidationConfig {
  {
    overrides: ReadOnlyArray::from_array(overrides.to_owned()),
    disabled: ReadOnlyArray::from_array(disabled.to_owned()),
  }
}

///|
/// The severity changes, by rule name.
pub fn ValidationConfig::overrides(
  self : ValidationConfig,
) -> ReadOnlyArray[(RuleName, Severity)] {
  self.overrides
}

///|
/// The rules that do not run.
pub fn ValidationConfig::disabled(
  self : ValidationConfig,
) -> ReadOnlyArray[RuleName] {
  self.disabled
}

///|
fn ValidationConfig::override_of(
  self : ValidationConfig,
  name : RuleName,
) -> Severity? {
  self.overrides.iter().find_first(p => p.0 == name).map(p => p.1)
}

///|
/// The sorted findings of one validation run.
struct Report {
  findings : ReadOnlyArray[Finding]
}

///|
fn severity_rank(s : Severity) -> Int32 {
  match s {
    Error => 0
    Warning => 1
    Info => 2
  }
}

///|
fn compare_findings(a : Finding, b : Finding) -> Int32 {
  let c = severity_rank(a.severity).compare(severity_rank(b.severity))
  if c != 0 {
    return c
  }
  let (ka, kb) = (a.subject.sort_key(), b.subject.sort_key())
  let c = ka.0.compare(kb.0)
  if c != 0 {
    return c
  }
  let c = lexical_compare(ka.1, kb.1)
  if c != 0 {
    return c
  }
  let c = lexical_compare(ka.2, kb.2)
  if c != 0 {
    return c
  }
  let c = a.line.unwrap_or(0).compare(b.line.unwrap_or(0))
  if c != 0 {
    return c
  }
  let c = a.rule.compare(b.rule)
  if c != 0 {
    return c
  }
  lexical_compare(a.message, b.message)
}

///|
/// A report from findings, sorted by severity, subject (bundle label, then
/// path), line and rule.
fn Report::of_findings(findings : ArrayView[Finding]) -> Report {
  let sorted = findings.to_owned()
  sorted.sort_by(compare_findings)
  { findings: ReadOnlyArray::from_array(sorted), }
}

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

///|
pub fn Report::count(self : Report, severity : Severity) -> Int32 {
  self.findings.iter().filter(f => f.severity == severity).count()
}

///|
/// True when the result fails: any error, or any warning in strict mode.
pub fn Report::fails(self : Report, strict~ : Bool = false) -> Bool {
  self.count(Error) > 0 || (strict && self.count(Warning) > 0)
}

///|
/// The findings of one rule.
pub fn Report::of_rule(self : Report, rule : RuleName) -> Array[Finding] {
  self.findings.iter().filter(f => f.rule == rule).collect()
}

///|
/// A rule name that this library defines, written in code.
fn builtin_rule(text : String) -> RuleName {
  { text, }
}

///|
/// The rules that `validate` itself runs, with their default severities. The
/// validation config can disable them or change their severities.
let builtin_rules : ReadOnlyArray[(String, Severity)] = [
  ("profile-invalid", Error),
  ("validation-config-unknown-rule", Warning),
]

///|
/// Runs the rule sets over the knowledge and returns the sorted findings. A
/// profile whose definitions break a rule gives a `profile-invalid` error, and
/// a config that names a rule that no active rule set defines gives a
/// `validation-config-unknown-rule` warning.
pub fn validate(
  knowledge : Knowledge,
  rule_sets : ArrayView[RuleSet],
  clock : &Clock,
  config~ : ValidationConfig = ValidationConfig(),
  resolver? : &LinkResolver,
  profiles~ : ArrayView[&Profile] = [],
) -> Report {
  let graph = Graph::build(knowledge, resolver?, profiles~)
  let context = {
    knowledge,
    graph,
    now: clock.now(),
    options: knowledge.options(),
    vocabulary: Vocabulary::of_profiles(profiles),
    profiles: ReadOnlyArray::from_array(profiles.to_owned()),
  }
  let findings : Array[Finding] = []
  // The findings of the built-in rules, before the config applies.
  let builtin : Array[(RuleName, FindingSubject, String)] = []
  let sets = rule_sets.to_owned()
  let profile_severity : Array[(RuleName, Severity)] = []
  for profile in profiles {
    try {
      // Every method that can raise, so that each invalid profile is reported.
      profile.vocabulary() |> ignore
      profile.bundle_keys() |> ignore
      profile.registers() |> ignore
      profile.reference_fields() |> ignore
      profile.templates() |> ignore
      for set in profile.rule_sets() {
        sets.push(set)
      }
      for pair in profile.severities() {
        if !profile_severity.any(p => p.0 == pair.0) {
          profile_severity.push(pair)
        }
      }
    } catch {
      e =>
        builtin.push(
          (
            builtin_rule("profile-invalid"),
            ProfileSubject(profile.name()),
            "the profile \{profile.name()} has an invalid definition: \{@debug.to_string(e)}",
          ),
        )
    }
  }
  let known : Array[RuleName] = builtin_rules
    .iter()
    .map(p => builtin_rule(p.0))
    .collect()
  for set in sets {
    for rule in set.rules {
      known.push(rule.name)
      guard !config.disabled.contains(rule.name) else { continue }
      let severity = match config.override_of(rule.name) {
        Some(s) => s
        None =>
          profile_severity
          .iter()
          .find_first(p => p.0 == rule.name)
          .map(p => p.1)
          .unwrap_or(rule.severity)
      }
      for issue in (rule.check)(context) {
        findings.push({
          severity,
          rule: rule.name,
          subject: issue.subject,
          line: issue.line,
          message: issue.message,
          hint: issue.hint,
        })
      }
    }
  }
  let named = config.overrides.iter().map(p => p.0).collect()
  for name in config.disabled {
    named.push(name)
  }
  for name in named {
    if !known.contains(name) {
      builtin.push(
        (
          builtin_rule("validation-config-unknown-rule"),
          ConfigSubject,
          "the validation config names `\{name}`, which no active rule set defines",
        ),
      )
    }
  }
  for item in builtin {
    let (rule, subject, message) = item
    guard !config.disabled.contains(rule) else { continue }
    let severity = match config.override_of(rule) {
      Some(s) => s
      None =>
        builtin_rules
        .iter()
        .find_first(p => p.0 == rule.text)
        .map(p => p.1)
        .unwrap_or(Error)
    }
    findings.push({ severity, rule, subject, line: None, message, hint: None, })
  }
  Report::of_findings(findings)
}

///|
/// The keys that validation treats as known: OKF keys and profile keys.
struct Vocabulary {
  concept_keys : Map[String, String]
  bundle_keys : Map[String, String]
}

///|
/// The OKF vocabulary plus the vocabulary of each profile. The value of each
/// key is the name of the profile that defines it, or `okf`. A profile whose
/// definitions break a rule adds no keys; `validate` reports it.
pub fn Vocabulary::of_profiles(profiles : ArrayView[&Profile]) -> Vocabulary {
  let concept_keys : Map[String, String] = Map([])
  for key in okf_concept_keys {
    concept_keys[key] = "okf"
  }
  let bundle_keys : Map[String, String] = Map([])
  bundle_keys["okf_version"] = "okf"
  for profile in profiles {
    let defs = profile.vocabulary() catch { _ => [] }
    for def in defs {
      let key = def.key.to_string()
      if !concept_keys.contains(key) {
        concept_keys[key] = profile.name()
      }
    }
    let defs = profile.bundle_keys() catch { _ => [] }
    for def in defs {
      let key = def.key.to_string()
      if !bundle_keys.contains(key) {
        bundle_keys[key] = profile.name()
      }
    }
  }
  { concept_keys, bundle_keys, }
}

///|
/// True when the key is an OKF or profile concept key.
pub fn Vocabulary::knows(self : Vocabulary, key : String) -> Bool {
  self.concept_keys.contains(key)
}

///|
/// True when the key is an OKF or profile root index key.
pub fn Vocabulary::knows_bundle_key(self : Vocabulary, key : String) -> Bool {
  self.bundle_keys.contains(key)
}

///|
pub extend Severity with Eq::{not_equal, equal}

///|
pub extend Severity with Compare::{op_lt, op_le, op_ge, compare, op_gt}

///|
pub extend Severity with Hash::{hash, hash_combine}

///|
pub extend Severity with @debug.Debug::{to_repr}

///|
pub extend FindingSubject with Eq::{not_equal, equal}

///|
pub extend FindingSubject with @debug.Debug::{to_repr}

///|
pub extend Finding with Eq::{not_equal, equal}

///|
pub extend Finding with @debug.Debug::{to_repr}

///|
pub extend Issue with Eq::{not_equal, equal}

///|
pub extend Issue with @debug.Debug::{to_repr}