///|
/// A project file captured by the caller.
pub(all) struct FileSnapshot {
  path : String
  content : String
} derive(Eq, @debug.Debug)

///|
/// Audit outcome for one requirement.
pub(all) enum Status {
  Pass
  Warn
  Fail
} derive(Eq, @debug.Debug)

///|
/// Built-in checks map directly to MoonBit hackathon submission needs.
pub(all) enum CheckId {
  Manifest
  Readme
  UsageExample
  Tests
  ContinuousIntegration
  BuildCommands
  ProjectMetadata
  License
  Traceability
  MaintenanceDocs
  Originality
} derive(Eq, @debug.Debug)

///|
/// One audit finding with short evidence.
pub(all) struct Finding {
  id : CheckId
  status : Status
  message : String
  evidence : String
} derive(Eq, @debug.Debug)

///|
/// Parsed project facts used by the audit report.
pub(all) struct ProjectProfile {
  package_name : String?
  version : String?
  license : String?
  repository : String?
  moonbit_files : Int
  test_files : Int
  ci_files : Int
  readme_chars : Int
  originality_signals : Int
} derive(Eq, @debug.Debug)

///|
/// Full audit report. `score` is 0..100, where warns count as partial credit.
pub(all) struct Report {
  score : Int
  profile : ProjectProfile
  findings : Array[Finding]
} derive(Eq, @debug.Debug)

///|
/// Audit a MoonBit project represented as file snapshots.
pub fn audit_project(files : Array[FileSnapshot]) -> Report {
  let profile = build_profile(files)
  let findings = []
  findings.push(check_manifest(files, profile))
  findings.push(check_readme(files, profile))
  findings.push(check_usage_example(files))
  findings.push(check_tests(profile))
  findings.push(check_ci(files, profile))
  findings.push(check_build_commands(files))
  findings.push(check_project_metadata(files, profile))
  findings.push(check_license(files, profile))
  findings.push(check_traceability(files))
  findings.push(check_maintenance_docs(files))
  findings.push(check_originality(files, profile))
  { score: score_findings(findings), profile, findings }
}

///|
/// Render a compact human-readable report.
pub fn render_report(report : Report) -> String {
  let lines = [
    "MoonBit Submit Guard Report",
    "score: " + report.score.to_string() + "/100",
    "package: " + report.profile.package_name.unwrap_or(""),
    "",
  ]
  for finding in report.findings {
    lines.push(
      "- [" +
      status_name(finding.status) +
      "] " +
      check_name(finding.id) +
      ": " +
      finding.message +
      evidence_suffix(finding.evidence),
    )
  }
  lines.join("\n")
}

///|
/// Return commands that should pass before submitting the project.
pub fn submission_commands() -> Array[String] {
  ["moon check", "moon build", "moon test", "moon run cmd/main"]
}

///|
/// Count findings with the selected status.
pub fn count_status(report : Report, status : Status) -> Int {
  let mut count = 0
  for finding in report.findings {
    if finding.status == status {
      count = count + 1
    }
  }
  count
}

///|
pub fn status_name(status : Status) -> String {
  match status {
    Pass => "pass"
    Warn => "warn"
    Fail => "fail"
  }
}

///|
pub fn check_name(id : CheckId) -> String {
  match id {
    Manifest => "moon.mod"
    Readme => "README"
    UsageExample => "runnable example"
    Tests => "tests"
    ContinuousIntegration => "CI"
    BuildCommands => "build commands"
    ProjectMetadata => "project metadata"
    License => "license"
    Traceability => "traceability"
    MaintenanceDocs => "maintenance docs"
    Originality => "originality"
  }
}

///|
/// Extract a quoted string value from moon.mod, for example `name = "owner/pkg"`.
pub fn moon_mod_string(content : String, key : String) -> String? {
  let prefix = key + " = "
  for line_view in content.split("\n") {
    let line = trim(line_view.to_owned())
    if line.has_prefix(prefix) {
      return first_quoted_value(line)
    }
  }
  None
}

///|
fn build_profile(files : Array[FileSnapshot]) -> ProjectProfile {
  let moon_mod = find_file(files, "moon.mod")
  let readme = readme_content(files)
  {
    package_name: moon_mod.bind(content => moon_mod_string(content, "name")),
    version: moon_mod.bind(content => moon_mod_string(content, "version")),
    license: moon_mod.bind(content => moon_mod_string(content, "license")),
    repository: moon_mod.bind(content => moon_mod_string(content, "repository")),
    moonbit_files: count_files(files, ".mbt"),
    test_files: count_test_files(files),
    ci_files: count_ci_files(files),
    readme_chars: readme.unwrap_or("").length(),
    originality_signals: originality_signals(files),
  }
}

///|
fn check_manifest(
  files : Array[FileSnapshot],
  profile : ProjectProfile,
) -> Finding {
  guard find_file(files, "moon.mod") is Some(_) else {
    return finding(Manifest, Fail, "missing moon.mod", "moon.mod")
  }
  guard profile.package_name is Some(name) && name.contains("/") else {
    return finding(Manifest, Fail, "package name must use owner/name", "name")
  }
  guard profile.version is Some(version) && version != "" else {
    return finding(Manifest, Warn, "version is not set", "version")
  }
  finding(Manifest, Pass, "manifest is present and named", "moon.mod")
}

///|
fn check_readme(
  files : Array[FileSnapshot],
  profile : ProjectProfile,
) -> Finding {
  match readme_content(files) {
    None => finding(Readme, Fail, "missing README.md", "README.md")
    Some(readme) => {
      let lower = readme.to_lower()
      let has_usage = contains_any(lower, [
        "usage", "quick", "install", "example",
      ])
      let has_boundary = contains_any(lower, [
        "boundary", "scope", "unsupported",
      ])
      if profile.readme_chars >= 1200 && has_usage && has_boundary {
        finding(
          Readme,
          Pass,
          "README explains purpose, usage, and scope",
          "README.md",
        )
      } else if profile.readme_chars >= 600 && has_usage {
        finding(
          Readme,
          Warn,
          "README is useful but scope could be clearer",
          "README.md",
        )
      } else {
        finding(Readme, Fail, "README is too thin for review", "README.md")
      }
    }
  }
}

///|
fn check_usage_example(files : Array[FileSnapshot]) -> Finding {
  let main = find_file(files, "cmd/main/main.mbt")
  match main {
    Some(content) =>
      if content.contains("fn main") && !content.contains("Hello") {
        finding(
          UsageExample,
          Pass,
          "cmd/main has a non-template example",
          "cmd/main",
        )
      } else {
        finding(
          UsageExample,
          Warn,
          "example still looks like a template",
          "cmd/main",
        )
      }
    None =>
      finding(
        UsageExample,
        Fail,
        "missing runnable cmd/main example",
        "cmd/main",
      )
  }
}

///|
fn check_tests(profile : ProjectProfile) -> Finding {
  if profile.test_files >= 2 {
    finding(
      Tests,
      Pass,
      "blackbox and whitebox tests are present",
      profile.test_files.to_string() + " test files",
    )
  } else if profile.test_files == 1 {
    finding(Tests, Warn, "only one test file found", "1 test file")
  } else {
    finding(Tests, Fail, "no MoonBit tests found", "*_test.mbt")
  }
}

///|
fn check_ci(files : Array[FileSnapshot], profile : ProjectProfile) -> Finding {
  if profile.ci_files == 0 {
    return finding(
      ContinuousIntegration,
      Fail,
      "missing GitHub Actions workflow",
      ".github/workflows",
    )
  }
  let ci = concat_matching(files, ".github/workflows/")
  if contains_all(ci, ["moon check", "moon build", "moon test"]) {
    finding(
      ContinuousIntegration,
      Pass,
      "CI validates check, build, and tests",
      ".github/workflows",
    )
  } else {
    finding(
      ContinuousIntegration,
      Warn,
      "CI exists but does not cover the full MoonBit loop",
      ".github/workflows",
    )
  }
}

///|
fn check_build_commands(files : Array[FileSnapshot]) -> Finding {
  let docs = all_text(files).to_lower()
  if contains_all(docs, [
      "moon check", "moon build", "moon test", "moon run cmd/main",
    ]) {
    finding(
      BuildCommands,
      Pass,
      "submission validation commands are documented",
      "README/docs",
    )
  } else {
    finding(
      BuildCommands,
      Warn,
      "document all local validation commands",
      "moon check/build/test/run",
    )
  }
}

///|
fn check_project_metadata(
  files : Array[FileSnapshot],
  profile : ProjectProfile,
) -> Finding {
  guard find_file(files, "moon.mod") is Some(content) else {
    return finding(ProjectMetadata, Fail, "missing moon.mod", "moon.mod")
  }
  let has_description = moon_mod_string(content, "description").unwrap_or("") !=
    ""
  let has_keywords = content.contains("keywords") && content.contains("[")
  let has_repository = profile.repository.unwrap_or("") != ""
  if has_description && has_keywords && has_repository {
    finding(
      ProjectMetadata,
      Pass,
      "description, keywords, and repository are set",
      "moon.mod",
    )
  } else {
    finding(
      ProjectMetadata,
      Warn,
      "project metadata needs description, keywords, and repository",
      "moon.mod",
    )
  }
}

///|
fn check_license(
  files : Array[FileSnapshot],
  profile : ProjectProfile,
) -> Finding {
  let has_license_file = file_exists(files, "LICENSE")
  let manifest_license = profile.license.unwrap_or("")
  if has_license_file && manifest_license != "" {
    finding(
      License,
      Pass,
      "license file and manifest license are present",
      manifest_license,
    )
  } else {
    finding(
      License,
      Fail,
      "license file or manifest license is missing",
      "LICENSE",
    )
  }
}

///|
fn check_traceability(files : Array[FileSnapshot]) -> Finding {
  let has_changelog = file_exists(files, "CHANGELOG.md")
  let has_issue_template = any_path_contains(files, ".github/ISSUE_TEMPLATE/")
  let has_pr_template = file_exists(files, ".github/PULL_REQUEST_TEMPLATE.md")
  if has_changelog && has_issue_template && has_pr_template {
    finding(
      Traceability,
      Pass,
      "changelog plus issue and PR templates are present",
      ".github",
    )
  } else if has_changelog {
    finding(
      Traceability,
      Warn,
      "add issue and PR templates for review traceability",
      "CHANGELOG.md",
    )
  } else {
    finding(
      Traceability,
      Fail,
      "missing changelog or tracking templates",
      "CHANGELOG.md",
    )
  }
}

///|
fn check_maintenance_docs(files : Array[FileSnapshot]) -> Finding {
  let has_design = file_exists(files, "docs/DESIGN.md")
  let has_api = file_exists(files, "docs/API.md")
  if has_design && has_api {
    finding(
      MaintenanceDocs,
      Pass,
      "API and design notes are documented",
      "docs/",
    )
  } else if has_design || has_api {
    finding(MaintenanceDocs, Warn, "add both API and design notes", "docs/")
  } else {
    finding(MaintenanceDocs, Fail, "missing maintenance documentation", "docs/")
  }
}

///|
fn check_originality(
  files : Array[FileSnapshot],
  profile : ProjectProfile,
) -> Finding {
  let text = all_text(files).to_lower()
  if contains_any(text, ["put public apis", "hello", "todo app", "calculator"]) {
    return finding(
      Originality,
      Fail,
      "template or common demo text found",
      "template marker",
    )
  }
  if profile.originality_signals >= 5 {
    finding(
      Originality,
      Pass,
      "project has enough non-template originality signals",
      profile.originality_signals.to_string() + " signals",
    )
  } else if profile.originality_signals >= 3 {
    finding(
      Originality,
      Warn,
      "project is not template-like but needs stronger originality proof",
      profile.originality_signals.to_string() + " signals",
    )
  } else {
    finding(
      Originality,
      Fail,
      "project lacks originality evidence",
      profile.originality_signals.to_string() + " signals",
    )
  }
}

///|
fn originality_signals(files : Array[FileSnapshot]) -> Int {
  let text = all_text(files).to_lower()
  let mut signals = 0
  if file_exists(files, "docs/DESIGN.md") {
    signals = signals + 1
  }
  if file_exists(files, "PROJECT_APPLICATION.md") {
    signals = signals + 1
  }
  if text.contains("boundary") || text.contains("scope") {
    signals = signals + 1
  }
  if text.contains("github") ||
    text.contains("submission") ||
    text.contains("hackathon") {
    signals = signals + 1
  }
  if text.contains("traceability") || text.contains("changelog") {
    signals = signals + 1
  }
  if text.contains("originality") ||
    text.contains("unique") ||
    text.contains("collision") {
    signals = signals + 1
  }
  signals
}

///|
fn score_findings(findings : Array[Finding]) -> Int {
  let mut points = 0
  let total = findings.length() * 2
  guard total > 0 else { return 0 }
  for item in findings {
    match item.status {
      Pass => points = points + 2
      Warn => points = points + 1
      Fail => ()
    }
  }
  points * 100 / total
}

///|
fn finding(
  id : CheckId,
  status : Status,
  message : String,
  evidence : String,
) -> Finding {
  { id, status, message, evidence }
}

///|
fn find_file(files : Array[FileSnapshot], path : String) -> String? {
  let wanted = normalize_path(path)
  for file in files {
    if normalize_path(file.path) == wanted {
      return Some(file.content)
    }
  }
  None
}

///|
fn file_exists(files : Array[FileSnapshot], path : String) -> Bool {
  find_file(files, path) is Some(_)
}

///|
fn readme_content(files : Array[FileSnapshot]) -> String? {
  match find_file(files, "README.md") {
    Some(content) => Some(content)
    None => match find_file(files, "README.mbt.md") {
      Some(content) => Some(content)
      None => find_file(files, "README.txt")
    }
  }
}

///|
fn count_files(files : Array[FileSnapshot], suffix : String) -> Int {
  let mut count = 0
  for file in files {
    if normalize_path(file.path).has_suffix(suffix) {
      count = count + 1
    }
  }
  count
}

///|
fn count_test_files(files : Array[FileSnapshot]) -> Int {
  let mut count = 0
  for file in files {
    let path = normalize_path(file.path)
    if path.has_suffix("_test.mbt") || path.has_suffix("_wbtest.mbt") {
      count = count + 1
    }
  }
  count
}

///|
fn count_ci_files(files : Array[FileSnapshot]) -> Int {
  let mut count = 0
  for file in files {
    let path = normalize_path(file.path)
    if path.has_prefix(".github/workflows/") && path.has_suffix(".yml") {
      count = count + 1
    }
  }
  count
}

///|
fn any_path_contains(files : Array[FileSnapshot], needle : String) -> Bool {
  let target = normalize_path(needle)
  for file in files {
    if normalize_path(file.path).contains(target) {
      return true
    }
  }
  false
}

///|
fn concat_matching(files : Array[FileSnapshot], path_prefix : String) -> String {
  let target = normalize_path(path_prefix)
  let parts = []
  for file in files {
    if normalize_path(file.path).has_prefix(target) {
      parts.push(file.content)
    }
  }
  parts.join("\n")
}

///|
fn all_text(files : Array[FileSnapshot]) -> String {
  let parts = []
  for file in files {
    parts.push(file.path)
    parts.push(file.content)
  }
  parts.join("\n").to_lower()
}

///|
fn first_quoted_value(line : String) -> String? {
  match line.find("\"") {
    None => None
    Some(start) => {
      let rest = line[start + 1:]
      match rest.to_owned().find("\"") {
        None => None
        Some(end) => Some(rest[:end].to_owned())
      }
    }
  }
}

///|
fn contains_all(text : String, needles : Array[String]) -> Bool {
  let lower = text.to_lower()
  for needle in needles {
    if !lower.contains(needle.to_lower()) {
      return false
    }
  }
  true
}

///|
fn contains_any(text : String, needles : Array[String]) -> Bool {
  let lower = text.to_lower()
  for needle in needles {
    if lower.contains(needle.to_lower()) {
      return true
    }
  }
  false
}

///|
fn evidence_suffix(evidence : String) -> String {
  if evidence == "" {
    ""
  } else {
    " (" + evidence + ")"
  }
}

///|
fn normalize_path(path : String) -> String {
  path.replace_all(old="\\", new="/").trim(chars="/").to_owned().to_lower()
}

///|
fn trim(value : String) -> String {
  value.trim(chars=" \t\r").to_owned()
}