///|
pub(all) struct QualityFinding {
  rule : String
  severity : String
  path : String
  message : String
  score : Int
} derive(Debug, Eq, ToJson)

///|
fn finding(
  rule : String,
  severity : String,
  path : String,
  message : String,
  score : Int,
) -> QualityFinding {
  { rule, severity, path, message, score }
}

///|
pub fn quality_finding_count(value : Json) -> Int {
  quality_findings(value).length()
}

///|
pub fn quality_findings(value : Json) -> Array[QualityFinding] {
  let output : Array[QualityFinding] = []
  let kind = resource_type(value).unwrap_or("")
  if kind == "" {
    output.push(
      finding(
        "resource-type", "error", "/resourceType", "resourceType is missing", 0,
      ),
    )
  } else if !is_known_resource_type(kind) {
    output.push(
      finding(
        "resource-type", "warning", "/resourceType", "resource type is outside metadata catalog",
        2,
      ),
    )
  }
  for issue_item in resource_quality_report(value).issues {
    let score = if issue_item.severity == "error" { 0 } else { 2 }
    output.push(
      finding(
        issue_item.code,
        issue_item.severity,
        issue_item.path,
        issue_item.message,
        score,
      ),
    )
  }
  match resource_type(value) {
    Some("Observation") =>
      match observation_measurement(value) {
        Some(number) =>
          if number < 0.0 {
            output.push(
              finding(
                "negative-measurement", "warning", "/valueQuantity/value", "measurement is negative",
                1,
              ),
            )
          }
        None => ()
      }
    _ => ()
  }
  output
}

///|
pub fn quality_score(value : Json) -> Int {
  let findings = quality_findings(value)
  let penalty = findings.fold(init=0, fn(total, item) {
    total + (10 - item.score)
  })
  if penalty >= 100 {
    0
  } else {
    100 - penalty
  }
}

///|
pub fn quality_status(value : Json) -> String {
  let score = quality_score(value)
  if score >= 90 {
    "excellent"
  } else if score >= 70 {
    "acceptable"
  } else if score >= 40 {
    "needs-review"
  } else {
    "rejected"
  }
}

///|
pub fn batch_quality_findings(values : Array[Json]) -> Array[QualityFinding] {
  values.fold(init=[], fn(output, value) {
    output.append(quality_findings(value)[:])
    output
  })
}

///|
pub fn batch_quality_score_average(values : Array[Json]) -> Int {
  if values.length() == 0 {
    100
  } else {
    values.fold(init=0, fn(total, value) { total + quality_score(value) }) /
    values.length()
  }
}

///|
pub fn validate_temporal_consistency(value : Json) -> Array[Issue] {
  let output : Array[Issue] = []
  let start = query_text(value, "period.start", fallback="")
  let end = query_text(value, "period.end", fallback="")
  if start != "" && end != "" && !period_is_valid(start, end) {
    output.push(
      issue("/period", "value", "period.start must be before period.end"),
    )
  }
  let birth = query_text(value, "birthDate", fallback="")
  let issued = query_text(value, "issued", fallback="")
  if birth != "" && issued != "" && date_is_after(birth, issued) {
    output.push(
      issue("/birthDate", "value", "birthDate must not be after issued"),
    )
  }
  output
}

///|
pub fn validate_reference_targets(
  value : Json,
  values : Array[Json],
) -> Array[Issue] {
  unresolved_references(value, values).map(fn(reference) {
    issue(
      "",
      "reference",
      "reference target is not present: \{reference}",
      severity="warning",
    )
  })
}

///|
pub fn complete_quality_report(
  value : Json,
  values : Array[Json],
) -> ValidationReport {
  let report = resource_quality_report(value)
  report.issues.append(validate_temporal_consistency(value)[:])
  report.issues.append(validate_reference_targets(value, values)[:])
  report
}

///|
pub fn complete_quality_is_valid(value : Json, values : Array[Json]) -> Bool {
  report_error_count(complete_quality_report(value, values)) == 0
}

///|
pub fn quality_dashboard_rows(values : Array[Json]) -> Array[String] {
  values.map(fn(value) {
    csv_row([
      resource_type(value).unwrap_or(""),
      field_string(value, "id").unwrap_or(""),
      quality_status(value),
      "\{quality_score(value)}",
      "\{quality_finding_count(value)}",
    ])
  })
}