///|
/// Immutable query parameters for filtering a DoctorReport.
pub(all) struct DiagnosticQuery {
  severity : Severity?
  category : RuleCategory?
  path_prefix : String?
  include_unlocated : Bool
} derive(Debug, Eq)

///|
pub fn DiagnosticQuery::all() -> DiagnosticQuery {
  { severity: None, category: None, path_prefix: None, include_unlocated: true }
}

///|
pub fn DiagnosticQuery::matches(
  self : DiagnosticQuery,
  diagnostic : Diagnostic,
) -> Bool {
  let severity_matches = match self.severity {
    Some(expected) => diagnostic.severity == expected
    None => true
  }
  let category_matches = match self.category {
    Some(expected) =>
      match find_rule_definition(diagnostic.code) {
        Some(rule) => rule.category == expected
        None => false
      }
    None => true
  }
  let location_matches = match self.path_prefix {
    Some(prefix) =>
      match diagnostic.location {
        Some(location) => location.path.has_prefix(prefix)
        None => self.include_unlocated
      }
    None => self.include_unlocated || diagnostic.location is Some(_)
  }
  severity_matches && category_matches && location_matches
}

///|
pub fn DiagnosticQuery::filter(
  self : DiagnosticQuery,
  report : DoctorReport,
) -> Array[Diagnostic] {
  let matches : Array[Diagnostic] = []
  for diagnostic in report.diagnostics {
    if self.matches(diagnostic) {
      matches.push(diagnostic)
    }
  }
  matches
}

///|
pub fn DiagnosticQuery::count(
  self : DiagnosticQuery,
  report : DoctorReport,
) -> Int {
  self.filter(report).length()
}

///|
pub fn diagnostic_counts_by_category(report : DoctorReport) -> Map[String, Int] {
  let counts : Map[String, Int] = {}
  for diagnostic in report.diagnostics {
    let category = match find_rule_definition(diagnostic.code) {
      Some(rule) => rule.category.label()
      None => "release"
    }
    let current = match counts.get(category) {
      Some(value) => value
      None => 0
    }
    counts[category] = current + 1
  }
  counts
}

///|
pub fn diagnostic_counts_by_severity(report : DoctorReport) -> Map[String, Int] {
  let counts : Map[String, Int] = { "ERROR": 0, "WARN": 0, "INFO": 0 }
  for diagnostic in report.diagnostics {
    let key = diagnostic.severity.label()
    counts[key] = counts[key] + 1
  }
  counts
}

///|
pub fn diagnostic_query_to_json(
  report : DoctorReport,
  query : DiagnosticQuery,
) -> Json {
  let diagnostics = query
    .filter(report)
    .map(diagnostic => {
      Json::object({
        "severity": Json::string(diagnostic.severity.label()),
        "code": Json::string(diagnostic.code),
        "message": Json::string(diagnostic.message),
        "detail": Json::string(diagnostic.detail),
      })
    })
  Json::object({
    "project": Json::string(report.root),
    "count": Json::number(diagnostics.length().to_double()),
    "diagnostics": Json::array(diagnostics),
  })
}

///|
pub fn diagnostic_query_summary(
  report : DoctorReport,
  query : DiagnosticQuery,
) -> String {
  let diagnostics = query.filter(report)
  let error_count = diagnostics.filter(item => item.severity == Error).length()
  let warning_count = diagnostics.filter(item => item.severity == Warn).length()
  let info_count = diagnostics.filter(item => item.severity == Info).length()
  "\{diagnostics.length()} findings: \{error_count} errors, \{warning_count} warnings, \{info_count} info"
}

///|
pub fn render_diagnostic_query(
  report : DoctorReport,
  query : DiagnosticQuery,
) -> String {
  let builder = StringBuilder()
  let diagnostics = query.filter(report)
  builder.write_string(
    "Moon Doctor diagnostic query: \{diagnostics.length()} matches\n",
  )
  for diagnostic in diagnostics {
    builder.write_string(
      "[" +
      diagnostic.severity.label() +
      "] " +
      diagnostic.code +
      ": " +
      diagnostic.message +
      "\n",
    )
  }
  builder.to_string()
}