///|
fn has_file(files : Array[String], expected : StringView) -> Bool {
  let expected = expected.to_lower().to_owned()
  files.any(fn(file) { file.to_lower() == expected })
}

///|
fn has_file_named(files : Array[String], names : Array[String]) -> Bool {
  files.any(fn(file) {
    let normalized = file.replace_all(old="\\", new="/").to_lower()
    names.any(fn(name) {
      normalized == name || normalized.has_suffix("/\{name}")
    })
  })
}

///|
fn has_workflow(files : Array[String]) -> Bool {
  files.any(fn(file) {
    let normalized = file.replace_all(old="\\", new="/").to_lower()
    normalized.has_prefix(".github/workflows/") &&
    (normalized.has_suffix(".yml") || normalized.has_suffix(".yaml"))
  })
}

///|
fn valid_repository_url(value : StringView) -> Bool {
  let value = value.trim()
  value.has_prefix("https://") ||
  value.has_prefix("http://") ||
  (value.has_prefix("git@") && value.contains(":"))
}

///|
fn add_finding(
  findings : Array[Finding],
  severity : Severity,
  code : String,
  path : String,
  message : String,
) -> Unit {
  findings.push({ severity, code, path, message })
}

///|
/// Audit pure scan facts. This function performs no file-system access and is
/// therefore portable and deterministic.
pub fn audit(facts : ScanFacts) -> AuditReport {
  let findings = []
  let project = facts.project
  if project.name.is_empty() {
    add_finding(findings, Error, "MS001", "moon.mod", "Module name is missing.")
  }
  if project.version.is_empty() {
    add_finding(
      findings,
      Error,
      "MS002",
      "moon.mod",
      "Module version is missing.",
    )
  }
  if project.license.is_empty() {
    add_finding(
      findings,
      Error,
      "MS003",
      "moon.mod",
      "SPDX license identifier is missing.",
    )
  } else if !valid_spdx(project.license) {
    add_finding(
      findings,
      Error,
      "MS004",
      "moon.mod",
      "License is not a recognized SPDX expression: \{project.license}",
    )
  }
  if project.repository.is_empty() {
    add_finding(
      findings,
      Warning,
      "MS005",
      "moon.mod",
      "Repository URL is missing; published packages should link to source.",
    )
  } else if !valid_repository_url(project.repository) {
    add_finding(
      findings,
      Warning,
      "MS014",
      "moon.mod",
      "Repository is not a recognized HTTP(S) or SSH Git URL.",
    )
  }
  if !has_file_named(facts.files, ["license", "license.md", "license.txt"]) {
    add_finding(
      findings,
      Error,
      "MS006",
      "LICENSE",
      "No license file was found.",
    )
  }
  if !has_file_named(facts.files, [
      "readme", "readme.md", "readme.mbt.md", "readme.txt",
    ]) {
    add_finding(
      findings,
      Error,
      "MS007",
      "README.md",
      "No README file was found.",
    )
  }
  if !has_workflow(facts.files) {
    add_finding(
      findings,
      Warning,
      "MS008",
      ".github/workflows",
      "No GitHub Actions workflow was found.",
    )
  }
  if !has_file_named(facts.files, ["changelog.md", "changes.md"]) {
    add_finding(
      findings,
      Warning,
      "MS009",
      "CHANGELOG.md",
      "No changelog was found.",
    )
  }
  if !has_file(facts.files, ".gitignore") {
    add_finding(
      findings,
      Info,
      "MS010",
      ".gitignore",
      "Consider adding a .gitignore file.",
    )
  }
  match facts.license_text {
    Some(text) =>
      match detect_license(text) {
        Some(detected) if normalize_license(project.license) != detected =>
          add_finding(
            findings,
            Error,
            "MS011",
            "LICENSE",
            "Manifest declares \{project.license}, but the license text looks like \{detected}.",
          )
        None =>
          add_finding(
            findings,
            Warning,
            "MS012",
            "LICENSE",
            "License text could not be identified automatically.",
          )
        _ => ()
      }
    None => ()
  }
  if facts.moonbit_source_lines == 0 {
    add_finding(
      findings,
      Error,
      "MS013",
      "",
      "No MoonBit source lines were found.",
    )
  }
  let penalty = findings.fold(init=0, (score, finding) => {
    score +
    (match finding.severity {
      Error => 18
      Warning => 6
      Info => 1
    })
  })
  let score = if penalty >= 100 { 0 } else { 100 - penalty }
  { project, findings, moonbit_source_lines: facts.moonbit_source_lines, score }
}