///|
pub(all) struct ReportDelta {
  added : Int
  removed : Int
  unchanged : Int
  added_codes : Array[String]
  removed_codes : Array[String]
} derive(Debug, Eq, ToJson)

///|
pub(all) struct ResourceDiff {
  added_fields : Array[String]
  removed_fields : Array[String]
  changed_fields : Array[String]
} derive(Debug, Eq, ToJson)

///|
fn issue_key(item : Issue) -> String {
  "\{item.path}|\{item.code}|\{item.severity}|\{item.message}"
}

///|
pub fn report_error_count(report : ValidationReport) -> Int {
  report.issues.count_if(fn(item) { item.severity == "error" })
}

///|
pub fn report_warning_count(report : ValidationReport) -> Int {
  report.issues.count_if(fn(item) { item.severity == "warning" })
}

///|
pub fn report_info_count(report : ValidationReport) -> Int {
  report.issues.count_if(fn(item) { item.severity == "info" })
}

///|
pub fn report_is_warning_only(report : ValidationReport) -> Bool {
  report_error_count(report) == 0 && report_warning_count(report) > 0
}

///|
pub fn report_is_clean(report : ValidationReport) -> Bool {
  report.issues.length() == 0
}

///|
pub fn issue_fingerprints(report : ValidationReport) -> Array[String] {
  report.issues.map(issue_key)
}

///|
pub fn compare_reports(
  before : ValidationReport,
  after : ValidationReport,
) -> ReportDelta {
  let before_keys = issue_fingerprints(before)
  let after_keys = issue_fingerprints(after)
  let added_codes : Array[String] = []
  let removed_codes : Array[String] = []
  for key in after_keys {
    if !before_keys.contains(key) {
      added_codes.push(key)
    }
  }
  for key in before_keys {
    if !after_keys.contains(key) {
      removed_codes.push(key)
    }
  }
  let unchanged = after_keys.length() - added_codes.length()
  {
    added: added_codes.length(),
    removed: removed_codes.length(),
    unchanged,
    added_codes,
    removed_codes,
  }
}

///|
pub fn merge_validation_reports(
  reports : Array[ValidationReport],
) -> ValidationReport {
  let issues : Array[Issue] = []
  let resource_type = reports
    .get(0)
    .map(fn(report) { report.resource_type })
    .unwrap_or("")
  let version = reports.get(0).map(fn(report) { report.version }).unwrap_or(R4)
  for report in reports {
    issues.append(report.issues[:])
  }
  { resource_type, version, issues }
}

///|
pub fn report_json(report : ValidationReport) -> Json {
  Json::object(
    Map([
      ("resourceType", Json::string(report.resource_type)),
      ("version", Json::string(report.version.to_string())),
      ("errorCount", Json::number(report_error_count(report).to_double())),
      ("warningCount", Json::number(report_warning_count(report).to_double())),
      ("issues", Json::array(report.issues.map(fn(item) { Json::Json(item) }))),
    ]),
  )
}

///|
pub fn report_text(report : ValidationReport) -> String {
  let status = if report_error_count(report) == 0 { "valid" } else { "invalid" }
  "\{report.resource_type} \{status}: errors=\{report_error_count(report)} warnings=\{report_warning_count(report)}"
}

///|
pub fn resource_diff(before : Json, after : Json) -> ResourceDiff {
  let before_fields = json_object_keys(before)
  let after_fields = json_object_keys(after)
  let added = after_fields.filter(fn(field) { !before_fields.contains(field) })
  let removed = before_fields.filter(fn(field) { !after_fields.contains(field) })
  let changed : Array[String] = []
  for field in before_fields {
    match query_first(before, field) {
      None => ()
      Some(left) =>
        match query_first(after, field) {
          Some(right) => if !Json::equal(left, right) { changed.push(field) }
          None => ()
        }
    }
  }
  { added_fields: added, removed_fields: removed, changed_fields: changed }
}

///|
pub fn resource_diff_count(before : Json, after : Json) -> Int {
  let diff = resource_diff(before, after)
  diff.added_fields.length() +
  diff.removed_fields.length() +
  diff.changed_fields.length()
}