///|
pub fn build_quality_matrix(input : AuditInput) -> QualityMatrix {
  quality_from_report(input, audit_project(input))
}

///|
pub fn quality_from_report(
  input : AuditInput,
  report : AuditReport,
) -> QualityMatrix {
  let manifest = parse_manifest(input.moon_mod)
  let readme = analyze_readme(input.readme)
  let workflow = analyze_workflow(input.ci)
  let license = analyze_license(input.license_text)
  let namespace_data = namespace_from_manifest(manifest)
  let release = build_release_plan(input)
  let axes : Array[QualityAxis] = []
  axes.push(
    quality_axis(
      "metadata",
      quality_metadata_score(manifest, namespace_data, report),
      15,
      quality_metadata_note(manifest, namespace_data),
    ),
  )
  axes.push(
    quality_axis(
      "readme",
      readme.coverage_score(),
      18,
      quality_readme_note(readme),
    ),
  )
  axes.push(
    quality_axis("ci", workflow.coverage_score(), 16, quality_ci_note(workflow)),
  )
  axes.push(
    quality_axis(
      "license",
      license.completeness_score(),
      12,
      quality_license_note(license),
    ),
  )
  axes.push(
    quality_axis(
      "repository",
      quality_repository_score(input),
      12,
      quality_repository_note(input),
    ),
  )
  axes.push(
    quality_axis(
      "release",
      quality_release_score(input, release),
      15,
      quality_release_note(input, release),
    ),
  )
  axes.push(
    quality_axis(
      "maintainability",
      quality_maintainability_score(input, report),
      12,
      quality_maintainability_note(input, report),
    ),
  )
  quality_matrix(
    axes,
    quality_weighted_score(axes),
    quality_max_score(axes),
    quality_normalized_score(axes),
  )
}

///|
pub fn QualityAxis::weighted_points(self : QualityAxis) -> Int {
  quality_clamp(self.score) * self.weight
}

///|
pub fn QualityAxis::max_points(self : QualityAxis) -> Int {
  self.weight * 100
}

///|
pub fn QualityAxis::rating(self : QualityAxis) -> String {
  let score = quality_clamp(self.score)
  if score >= 90 {
    "excellent"
  } else if score >= 75 {
    "good"
  } else if score >= 60 {
    "fair"
  } else {
    "weak"
  }
}

///|
pub fn QualityAxis::markdown_row(self : QualityAxis) -> String {
  "| " +
  self.name +
  " | " +
  quality_clamp(self.score).to_string() +
  " | " +
  self.weight.to_string() +
  " | " +
  self.rating() +
  " | " +
  self.note +
  " |\n"
}

///|
pub fn QualityMatrix::grade(self : QualityMatrix) -> String {
  if self.normalized_score >= 90 {
    "A"
  } else if self.normalized_score >= 80 {
    "B"
  } else if self.normalized_score >= 70 {
    "C"
  } else if self.normalized_score >= 60 {
    "D"
  } else {
    "E"
  }
}

///|
pub fn QualityMatrix::is_competitive(self : QualityMatrix) -> Bool {
  self.normalized_score >= 85 && self.lowest_score() >= 65
}

///|
pub fn QualityMatrix::lowest_score(self : QualityMatrix) -> Int {
  if self.axes.length() == 0 {
    return 0
  }
  let mut value = quality_clamp(self.axes[0].score)
  let mut index = 1
  while index < self.axes.length() {
    let score = quality_clamp(self.axes[index].score)
    if score < value {
      value = score
    }
    index += 1
  }
  value
}

///|
pub fn QualityMatrix::lowest_axis_name(self : QualityMatrix) -> String {
  if self.axes.length() == 0 {
    return ""
  }
  let mut name = self.axes[0].name
  let mut score = quality_clamp(self.axes[0].score)
  let mut index = 1
  while index < self.axes.length() {
    let current = quality_clamp(self.axes[index].score)
    if current < score {
      score = current
      name = self.axes[index].name
    }
    index += 1
  }
  name
}

///|
pub fn QualityMatrix::improvement_notes(self : QualityMatrix) -> Array[String] {
  let notes : Array[String] = []
  let mut index = 0
  while index < self.axes.length() {
    let axis = self.axes[index]
    if quality_clamp(axis.score) < 80 {
      notes.push(axis.name + ": " + axis.note)
    }
    index += 1
  }
  notes
}

///|
pub fn QualityMatrix::summary(self : QualityMatrix) -> String {
  "quality=\{self.normalized_score}/100 grade=\{self.grade()} lowest=\{self.lowest_axis_name()}"
}

///|
pub fn QualityMatrix::to_markdown(self : QualityMatrix) -> String {
  let mut out = "## Quality Matrix\n\n"
  out = out + "- Score: " + self.normalized_score.to_string() + "/100\n"
  out = out + "- Grade: " + self.grade() + "\n"
  out = out + "- Competitive: " + bool_text(self.is_competitive()) + "\n"
  out = out + "- Lowest axis: " + self.lowest_axis_name() + "\n\n"
  out = out +
    "| Axis | Score | Weight | Rating | Note |\n| --- | --- | --- | --- | --- |\n"
  let mut index = 0
  while index < self.axes.length() {
    out = out + self.axes[index].markdown_row()
    index += 1
  }
  let notes = self.improvement_notes()
  if notes.length() > 0 {
    out = out + "\n### Improvements\n\n"
    let mut note_index = 0
    while note_index < notes.length() {
      out = out + "- " + notes[note_index] + "\n"
      note_index += 1
    }
  }
  out
}

///|
pub fn quality_matrix_table(matrices : Array[QualityMatrix]) -> String {
  let mut out = "| # | Score | Grade | Competitive | Lowest |\n| --- | --- | --- | --- | --- |\n"
  let mut index = 0
  while index < matrices.length() {
    let matrix = matrices[index]
    out = out + "| " + (index + 1).to_string()
    out = out + " | " + matrix.normalized_score.to_string()
    out = out + " | " + matrix.grade()
    out = out + " | " + bool_text(matrix.is_competitive())
    out = out + " | " + matrix.lowest_axis_name() + " |\n"
    index += 1
  }
  out
}

///|
pub fn quality_axis_named(
  matrix : QualityMatrix,
  name : String,
) -> QualityAxis? {
  let mut index = 0
  while index < matrix.axes.length() {
    if matrix.axes[index].name == name {
      return Some(matrix.axes[index])
    }
    index += 1
  }
  None
}

///|
fn quality_metadata_score(
  manifest : ManifestInfo,
  namespace_data : NamespaceInfo,
  report : AuditReport,
) -> Int {
  let mut score = 0
  if manifest.has("name") {
    score += 15
  }
  if manifest.has("version") {
    score += 15
  }
  if manifest.has("repository") {
    score += 15
  }
  if manifest.has("license") {
    score += 10
  }
  if manifest.has("description") {
    score += 10
  }
  if namespace_data.valid {
    score += 20
  }
  if report.score.error_count == 0 {
    score += 15
  }
  quality_clamp(score)
}

///|
fn quality_metadata_note(
  manifest : ManifestInfo,
  namespace_data : NamespaceInfo,
) -> String {
  if !namespace_data.valid {
    "fix the package namespace format"
  } else if manifest.duplicates.length() > 0 {
    "remove duplicate moon.mod fields"
  } else if manifest.malformed_lines.length() > 0 {
    "fix malformed moon.mod lines"
  } else {
    "metadata is coherent"
  }
}

///|
fn quality_readme_note(readme : ReadmeMetrics) -> String {
  if readme.is_complete() {
    "README covers the reviewer's first-run workflow"
  } else {
    "missing: " + quality_join_strings(readme.missing_topics())
  }
}

///|
fn quality_ci_note(workflow : WorkflowInfo) -> String {
  if workflow.is_acceptance_ready() {
    "CI covers check, build, test and example execution"
  } else {
    "missing: " + quality_join_strings(workflow.missing_commands())
  }
}

///|
fn quality_license_note(license : LicenseFacts) -> String {
  if license.is_publishable() {
    "license is recognized and publishable"
  } else {
    "add a complete OSI-approved LICENSE file"
  }
}

///|
fn quality_repository_score(input : AuditInput) -> Int {
  let mut score = 0
  if input.repository_public {
    score += 45
  }
  if input.commit_count >= 5 {
    score += 35
  }
  if trim_ascii(input.changelog).length() > 0 {
    score += 20
  }
  quality_clamp(score)
}

///|
fn quality_repository_note(input : AuditInput) -> String {
  if !input.repository_public {
    "make the GitHub repository public"
  } else if input.commit_count < 5 {
    "keep at least five meaningful contest-period commits"
  } else if trim_ascii(input.changelog).length() == 0 {
    "add a small changelog"
  } else {
    "repository evidence is traceable"
  }
}

///|
fn quality_release_score(input : AuditInput, plan : ReleasePlan) -> Int {
  let mut score = 100
  if plan.is_blocked() {
    score -= plan.blockers.length() * 12
  }
  if input.package_published {
    score += 10
  }
  quality_clamp(score)
}

///|
fn quality_release_note(input : AuditInput, plan : ReleasePlan) -> String {
  if input.package_published {
    "package is marked as published"
  } else if plan.blockers.length() == 0 {
    "ready for moon publish after logging into Mooncakes"
  } else {
    "blocker: " + plan.first_blocker()
  }
}

///|
fn quality_maintainability_score(
  input : AuditInput,
  report : AuditReport,
) -> Int {
  let readme = analyze_readme(input.readme)
  let mut score = 45
  if readme.has_support_scope {
    score += 15
  }
  if readme.has_unsupported_scope {
    score += 15
  }
  if trim_ascii(input.changelog).length() > 0 {
    score += 10
  }
  if report.score.warning_count == 0 {
    score += 10
  }
  if input.commit_count >= 8 {
    score += 5
  }
  quality_clamp(score)
}

///|
fn quality_maintainability_note(
  input : AuditInput,
  report : AuditReport,
) -> String {
  let readme = analyze_readme(input.readme)
  if !readme.has_support_scope {
    "document supported scope"
  } else if !readme.has_unsupported_scope {
    "document unsupported or future scope"
  } else if report.score.warning_count > 0 {
    "resolve remaining audit warnings"
  } else {
    "maintenance boundary is clear"
  }
}

///|
fn quality_weighted_score(axes : Array[QualityAxis]) -> Int {
  let mut total = 0
  let mut index = 0
  while index < axes.length() {
    total += axes[index].weighted_points()
    index += 1
  }
  total
}

///|
fn quality_max_score(axes : Array[QualityAxis]) -> Int {
  let mut total = 0
  let mut index = 0
  while index < axes.length() {
    total += axes[index].max_points()
    index += 1
  }
  total
}

///|
fn quality_normalized_score(axes : Array[QualityAxis]) -> Int {
  let max = quality_max_score(axes)
  if max == 0 {
    0
  } else {
    quality_weighted_score(axes) * 100 / max
  }
}

///|
fn quality_clamp(value : Int) -> Int {
  if value < 0 {
    0
  } else if value > 100 {
    100
  } else {
    value
  }
}

///|
fn quality_join_strings(items : Array[String]) -> String {
  if items.length() == 0 {
    return "none"
  }
  let mut out = ""
  let mut index = 0
  while index < items.length() {
    if index > 0 {
      out = out + ", "
    }
    out = out + items[index]
    index += 1
  }
  out
}