///|
/// One weighted quality area in a Moon Doctor scorecard.
pub(all) struct QualityArea {
  category : RuleCategory
  possible : Int
  achieved : Int
  errors : Int
  warnings : Int
  infos : Int
  notes : Array[String]
} derive(Debug, Eq)

///|
pub fn QualityArea::percent(self : QualityArea) -> Int {
  if self.possible == 0 {
    100
  } else {
    self.achieved * 100 / self.possible
  }
}

///|
pub fn QualityArea::label(self : QualityArea) -> String {
  self.category.label()
}

///|
/// Explainable quality score independent of strict pass/fail mode.
pub(all) struct QualityScorecard {
  profile : String
  areas : Array[QualityArea]
  total_possible : Int
  total_achieved : Int
  percentage : Int
  grade : String
} derive(Debug, Eq)

///|
fn empty_area(category : RuleCategory) -> QualityArea {
  {
    category,
    possible: 100,
    achieved: 100,
    errors: 0,
    warnings: 0,
    infos: 0,
    notes: [],
  }
}

///|
fn category_of_diagnostic(diagnostic : Diagnostic) -> RuleCategory {
  match find_rule_definition(diagnostic.code) {
    Some(rule) => rule.category
    None => Release
  }
}

///|
fn penalty(severity : Severity) -> Int {
  match severity {
    Error => 30
    Warn => 12
    Info => 3
  }
}

///|
fn update_area(area : QualityArea, diagnostic : Diagnostic) -> QualityArea {
  let achieved = if area.achieved > penalty(diagnostic.severity) {
    area.achieved - penalty(diagnostic.severity)
  } else {
    0
  }
  let notes = area.notes
  if notes.length() < 5 {
    notes.push(diagnostic.code + ": " + diagnostic.message)
  }
  match diagnostic.severity {
    Error =>
      {
        category: area.category,
        possible: area.possible,
        achieved,
        errors: area.errors + 1,
        warnings: area.warnings,
        infos: area.infos,
        notes,
      }
    Warn =>
      {
        category: area.category,
        possible: area.possible,
        achieved,
        errors: area.errors,
        warnings: area.warnings + 1,
        infos: area.infos,
        notes,
      }
    Info =>
      {
        category: area.category,
        possible: area.possible,
        achieved,
        errors: area.errors,
        warnings: area.warnings,
        infos: area.infos + 1,
        notes,
      }
  }
}

///|
fn area_index(areas : Array[QualityArea], category : RuleCategory) -> Int {
  let mut index = 0
  for area in areas {
    if area.category == category {
      return index
    }
    index += 1
  }
  0
}

///|
fn score_grade(percentage : Int, errors : Int) -> String {
  if errors > 0 {
    "D"
  } else if percentage >= 90 {
    "A"
  } else if percentage >= 75 {
    "B"
  } else if percentage >= 60 {
    "C"
  } else {
    "D"
  }
}

///|
pub fn build_quality_scorecard(report : DoctorReport) -> QualityScorecard {
  let areas = [
    empty_area(Manifest),
    empty_area(Package),
    empty_area(Documentation),
    empty_area(Ci),
    empty_area(Release),
  ]
  for diagnostic in report.diagnostics {
    let index = area_index(areas, category_of_diagnostic(diagnostic))
    areas[index] = update_area(areas[index], diagnostic)
  }
  let mut possible = 0
  let mut achieved = 0
  let mut errors = 0
  for area in areas {
    possible += area.possible
    achieved += area.achieved
    errors += area.errors
  }
  let percentage = if possible == 0 { 100 } else { achieved * 100 / possible }
  {
    profile: report.profile,
    areas,
    total_possible: possible,
    total_achieved: achieved,
    percentage,
    grade: score_grade(percentage, errors),
  }
}

///|
pub fn quality_scorecard_to_json(scorecard : QualityScorecard) -> Json {
  let areas = scorecard.areas.map(area => {
    Json::object({
      "category": Json::string(area.label()),
      "possible": Json::number(area.possible.to_double()),
      "achieved": Json::number(area.achieved.to_double()),
      "percent": Json::number(area.percent().to_double()),
      "errors": Json::number(area.errors.to_double()),
      "warnings": Json::number(area.warnings.to_double()),
      "infos": Json::number(area.infos.to_double()),
      "notes": Json::array(area.notes.map(Json::string)),
    })
  })
  Json::object({
    "profile": Json::string(scorecard.profile),
    "grade": Json::string(scorecard.grade),
    "percentage": Json::number(scorecard.percentage.to_double()),
    "areas": Json::array(areas),
  })
}

///|
pub fn render_quality_scorecard(scorecard : QualityScorecard) -> String {
  let builder = StringBuilder()
  builder.write_string("Moon Doctor quality scorecard\n")
  builder.write_string("Profile: " + scorecard.profile + "\n")
  builder.write_string(
    "Grade: " + scorecard.grade + " (\{scorecard.percentage}%)\n\n",
  )
  for area in scorecard.areas {
    builder.write_string(
      "- " +
      area.label() +
      ": \{area.percent()}% (E:\{area.errors} W:\{area.warnings} I:\{area.infos})\n",
    )
    for note in area.notes {
      builder.write_string("  - " + note + "\n")
    }
  }
  builder.to_string()
}