///|
pub struct SarifValidationPolicy {
  require_results_have_locations : Bool
  require_declared_rules : Bool
  require_artifacts_for_locations : Bool
  min_level : String
  max_results_per_run : Int?
} derive(Eq, Debug, ToJson)

///|
pub fn SarifValidationPolicy::SarifValidationPolicy(
  require_results_have_locations? : Bool = true,
  require_declared_rules? : Bool = true,
  require_artifacts_for_locations? : Bool = false,
  min_level? : String = "none",
  max_results_per_run? : Int,
) -> SarifValidationPolicy {
  {
    require_results_have_locations,
    require_declared_rules,
    require_artifacts_for_locations,
    min_level: normalize_level(min_level),
    max_results_per_run,
  }
}

///|
pub fn default_validation_policy() -> SarifValidationPolicy {
  SarifValidationPolicy::SarifValidationPolicy()
}

///|
fn push_error(
  out : Array[SarifDiagnostic],
  code : StringView,
  path : StringView,
  message : StringView,
) -> Unit {
  out.push(SarifDiagnostic::SarifDiagnostic("error", code, path, message))
}

///|
fn push_warning(
  out : Array[SarifDiagnostic],
  code : StringView,
  path : StringView,
  message : StringView,
) -> Unit {
  out.push(SarifDiagnostic::SarifDiagnostic("warning", code, path, message))
}

///|
fn rule_exists(rules : ArrayView[SarifRule], id : StringView) -> Bool {
  let needle = id.to_owned()
  for rule in rules {
    if rule.id == needle {
      return true
    }
  }
  false
}

///|
fn artifact_exists(artifacts : ArrayView[Artifact], uri : StringView) -> Bool {
  let needle = uri.to_owned()
  for artifact in artifacts {
    if artifact.location.uri == needle {
      return true
    }
  }
  false
}

///|
fn validate_region(
  region : Region,
  path : StringView,
  out : Array[SarifDiagnostic],
) -> Unit {
  if region.start_line < 1 {
    push_error(out, "region.startLine", path, "startLine must be one-based")
  }
  if region.start_column < 1 {
    push_error(out, "region.startColumn", path, "startColumn must be one-based")
  }
  if region.end_line is Some(end_line) {
    if end_line < region.start_line {
      push_error(
        out, "region.endLine", path, "endLine must not precede startLine",
      )
    }
  }
  if region.end_column is Some(end_column) {
    if end_column < 1 {
      push_error(out, "region.endColumn", path, "endColumn must be one-based")
    }
  }
  if region.char_offset is Some(offset) {
    if offset < 0 {
      push_error(
        out, "region.charOffset", path, "charOffset must be non-negative",
      )
    }
  }
  if region.char_length is Some(length) {
    if length < 0 {
      push_error(
        out, "region.charLength", path, "charLength must be non-negative",
      )
    }
  }
}

///|
fn validate_location(
  location : SarifLocation,
  path : StringView,
  artifacts : ArrayView[Artifact],
  policy : SarifValidationPolicy,
  out : Array[SarifDiagnostic],
) -> Unit {
  let uri = location.physical_location.artifact_location.uri
  if is_blank(uri) {
    push_error(
      out, "location.uri", path, "artifactLocation.uri must be non-empty",
    )
  }
  if policy.require_artifacts_for_locations && !artifact_exists(artifacts, uri) {
    push_warning(
      out, "location.artifact", path, "location uri is not listed in artifacts",
    )
  }
  if location.physical_location.region is Some(region) {
    validate_region(region, "\{path}.physicalLocation.region", out)
  }
}

///|
fn validate_rule(
  rule : SarifRule,
  path : StringView,
  out : Array[SarifDiagnostic],
) -> Unit {
  if is_blank(rule.id) {
    push_error(out, "rule.id", path, "rule id must be non-empty")
  }
  if !is_known_level(rule.default_level) {
    push_warning(
      out, "rule.level", path, "rule default level is not a standard SARIF level",
    )
  }
  if rule.tags.length() == 0 {
    push_warning(out, "rule.tags", path, "rule has no tags")
  }
}

///|
fn validate_result(
  result : SarifResult,
  path : StringView,
  run : SarifRun,
  policy : SarifValidationPolicy,
  out : Array[SarifDiagnostic],
) -> Unit {
  if is_blank(result.rule_id) {
    push_error(out, "result.ruleId", path, "result ruleId must be non-empty")
  }
  if policy.require_declared_rules &&
    !rule_exists(run.tool.rules, result.rule_id) {
    push_warning(
      out, "result.ruleId", path, "result references an undeclared rule",
    )
  }
  if !is_known_level(result.level) {
    push_warning(
      out, "result.level", path, "result level is not a standard SARIF level",
    )
  }
  if !is_known_kind(result.kind) {
    push_warning(
      out, "result.kind", path, "result kind is not a standard SARIF kind",
    )
  }
  if is_blank(result.message.text) {
    push_error(
      out, "result.message", path, "result message text must be non-empty",
    )
  }
  if policy.require_results_have_locations && result.locations.length() == 0 {
    push_warning(
      out, "result.locations", path, "result has no physical location",
    )
  }
  if !level_at_least(result.level, policy.min_level) {
    push_warning(
      out, "result.level.min", path, "result is below the configured minimum level",
    )
  }
  if result.baseline_state is Some(state) {
    if !is_known_baseline_state(state) {
      push_warning(
        out, "result.baselineState", path, "baselineState is not a standard SARIF value",
      )
    }
  }
  for i, location in result.locations {
    validate_location(
      location,
      "\{path}.locations[\{i}]",
      run.artifacts,
      policy,
      out,
    )
  }
}

///|
pub fn SarifRun::validate_with_policy(
  self : SarifRun,
  policy : SarifValidationPolicy,
) -> Array[SarifDiagnostic] {
  let out : Array[SarifDiagnostic] = []
  if is_blank(self.tool.name) {
    push_error(
      out, "tool.driver.name", "tool.driver", "tool driver name must be non-empty",
    )
  }
  if self.tool.rules.length() == 0 {
    push_warning(
      out, "tool.rules", "tool.driver.rules", "tool declares no rules",
    )
  }
  for i, rule in self.tool.rules {
    validate_rule(rule, "tool.driver.rules[\{i}]", out)
  }
  if policy.max_results_per_run is Some(max) {
    if self.results.length() > max {
      push_warning(
        out, "run.results.max", "results", "run exceeds configured result limit",
      )
    }
  }
  for i, result in self.results {
    validate_result(result, "results[\{i}]", self, policy, out)
  }
  out
}

///|
pub fn SarifRun::validate(self : SarifRun) -> Array[SarifDiagnostic] {
  self.validate_with_policy(default_validation_policy())
}

///|
pub fn SarifLog::validate_with_policy(
  self : SarifLog,
  policy : SarifValidationPolicy,
) -> Array[SarifDiagnostic] {
  let out : Array[SarifDiagnostic] = []
  if self.version != "2.1.0" {
    push_error(out, "log.version", "version", "SARIF version must be 2.1.0")
  }
  if self.runs.length() == 0 {
    push_error(
      out, "log.runs", "runs", "SARIF log must contain at least one run",
    )
  }
  for i, run in self.runs {
    for diag in run.validate_with_policy(policy) {
      out.push({ ..diag, path: "runs[\{i}].\{diag.path}" })
    }
  }
  out
}

///|
pub fn SarifLog::validate(self : SarifLog) -> Array[SarifDiagnostic] {
  self.validate_with_policy(default_validation_policy())
}

///|
pub fn SarifLog::is_valid(self : SarifLog) -> Bool {
  for diagnostic in self.validate() {
    if diagnostic.severity == "error" {
      return false
    }
  }
  true
}

///|
pub fn diagnostics_summary(items : ArrayView[SarifDiagnostic]) -> String {
  let mut errors = 0
  let mut warnings = 0
  for item in items {
    if item.severity == "error" {
      errors = errors + 1
    } else if item.severity == "warning" {
      warnings = warnings + 1
    }
  }
  "diagnostics errors=\{errors} warnings=\{warnings}"
}