///|
fn finding(
  code : String,
  severity : Severity,
  message : String,
  rule : Rule,
  attribute? : String? = None,
) -> Finding {
  {
    code,
    severity,
    message,
    source: rule.source,
    line: rule.line,
    pattern: rule.pattern,
    attribute,
  }
}

///|
fn audit_duplicate_assignments(rule : Rule, findings : Array[Finding]) -> Unit {
  let seen : Map[String, Unit] = Map([])
  for assignment in rule.assignments {
    if seen.contains(assignment.name) {
      findings.push(
        finding(
          "MGA101",
          Warning,
          "attribute '\{assignment.name}' is assigned more than once in one rule",
          rule,
          attribute=Some(assignment.name),
        ),
      )
    }
    seen[assignment.name] = ()
  }
}

///|
fn audit_reserved_names(rule : Rule, findings : Array[Finding]) -> Unit {
  for assignment in rule.assignments {
    if assignment.name.has_prefix("builtin_") {
      findings.push(
        finding(
          "MGA102",
          Error,
          "the builtin_* namespace is reserved by Git",
          rule,
          attribute=Some(assignment.name),
        ),
      )
    }
  }
}

///|
fn audit_eol_values(rule : Rule, findings : Array[Finding]) -> Unit {
  for assignment in rule.assignments {
    if assignment.name == "eol" {
      match assignment.state {
        Value("lf") | Value("crlf") => ()
        Value(value) =>
          findings.push(
            finding(
              "MGA103",
              Error,
              "eol only accepts 'lf' or 'crlf', not '\{value}'",
              rule,
              attribute=Some("eol"),
            ),
          )
        _ =>
          findings.push(
            finding(
              "MGA104",
              Warning,
              "eol is normally written as eol=lf or eol=crlf",
              rule,
              attribute=Some("eol"),
            ),
          )
      }
    }
  }
}

///|
fn audit_macro_usage(
  rule : Rule,
  macros : Array[MacroDefinition],
  findings : Array[Finding],
) -> Unit {
  for assignment in rule.assignments {
    let is_macro = assignment.name == "binary" ||
      macros.any(definition => definition.name == assignment.name)
    if is_macro && assignment.state != Set {
      findings.push(
        finding(
          "MGA105",
          Warning,
          "attribute macros expand only when set without a value",
          rule,
          attribute=Some(assignment.name),
        ),
      )
    }
  }
}

///|
fn audit_text_conflicts(rule : Rule, findings : Array[Finding]) -> Unit {
  let mut has_binary = false
  let mut has_text_override = false
  for assignment in rule.assignments {
    if assignment.name == "binary" && assignment.state == Set {
      has_binary = true
    }
    if assignment.name == "text" && assignment.state != Unset {
      has_text_override = true
    }
  }
  if has_binary && has_text_override {
    findings.push(
      finding(
        "MGA106",
        Warning,
        "binary expands to -text, but this rule also assigns text",
        rule,
        attribute=Some("text"),
      ),
    )
  }
}

///|
/// Audits parsed rules for common configuration mistakes and ambiguous intent.
pub fn RuleSet::audit(self : RuleSet) -> AuditReport {
  let findings : Array[Finding] = []
  for rule in self.rules {
    audit_duplicate_assignments(rule, findings)
    audit_reserved_names(rule, findings)
    audit_eol_values(rule, findings)
    audit_macro_usage(rule, self.macros, findings)
    audit_text_conflicts(rule, findings)
  }
  { diagnostics: self.diagnostics.copy(), findings, }
}

///|
/// Returns true when parsing or policy auditing found an error.
pub fn AuditReport::has_errors(self : AuditReport) -> Bool {
  self.diagnostics.any(item => item.severity == Error) ||
  self.findings.any(item => item.severity == Error)
}