///|
/// A release manifest records deterministic package-level evidence for a
/// de-identification export without retaining the source document.
pub(all) struct ManifestCheck {
  id : String
  passed : Bool
  value : String
  severity : DiagnosticSeverity
} derive(Debug, Eq)

///|
pub(all) struct ReleaseManifest {
  package_name : String
  package_version : String
  mut document_count : Int
  mut source_checksum : String
  mut output_checksum : String
  policy_checksum : String
  checks : Array[ManifestCheck]
  mut generated_at : String
} derive(Debug, Eq)

///|
pub(all) struct ReleaseDecision {
  approved : Bool
  reasons : Array[String]
  manifest_checksum : String
} derive(Debug, Eq)

///|
pub fn manifest_check(
  id : String,
  passed : Bool,
  value : String,
  severity : DiagnosticSeverity,
) -> ManifestCheck {
  { id, passed, value, severity }
}

///|
pub fn ReleaseManifest::new(
  package_name : String,
  package_version : String,
  policy_checksum : String,
) -> ReleaseManifest {
  {
    package_name,
    package_version,
    document_count: 0,
    source_checksum: "",
    output_checksum: "",
    policy_checksum,
    checks: [],
    generated_at: "",
  }
}

///|
pub fn ReleaseManifest::add_check(
  self : ReleaseManifest,
  check : ManifestCheck,
) -> Unit {
  self.checks.push(check)
}

///|
pub fn ReleaseManifest::set_counts(
  self : ReleaseManifest,
  document_count : Int,
  source_checksum : String,
  output_checksum : String,
) -> Unit {
  self.document_count = document_count
  self.source_checksum = source_checksum
  self.output_checksum = output_checksum
}

///|
pub fn ReleaseManifest::set_generated_at(
  self : ReleaseManifest,
  value : String,
) -> Unit {
  self.generated_at = value
}

///|
pub fn ReleaseManifest::passed(self : ReleaseManifest) -> Bool {
  !self.checks.any(fn(check) { !check.passed && check.severity == Error })
}

///|
pub fn ReleaseManifest::failed_checks(
  self : ReleaseManifest,
) -> Array[ManifestCheck] {
  self.checks.filter(fn(check) { !check.passed })
}

///|
pub fn ReleaseManifest::error_count(self : ReleaseManifest) -> Int {
  self.failed_checks().filter(fn(check) { check.severity == Error }).length()
}

///|
pub fn ReleaseManifest::warning_count(self : ReleaseManifest) -> Int {
  self.failed_checks().filter(fn(check) { check.severity == Warning }).length()
}

///|
pub fn ReleaseManifest::checksum(self : ReleaseManifest) -> String {
  stable_hash(
    self.package_name +
    ":" +
    self.package_version +
    ":" +
    self.source_checksum +
    ":" +
    self.output_checksum +
    ":" +
    self.policy_checksum +
    ":" +
    self.checks
    .map(fn(check) { check.id + ":\{check.passed}:" + check.value })
    .join("\n"),
  )
}

///|
pub fn ReleaseManifest::summary(self : ReleaseManifest) -> String {
  [
    "package=\{self.package_name}",
    "version=\{self.package_version}",
    "documents=\{self.document_count}",
    "checks=\{self.checks.length()}",
    "errors=\{self.error_count()}",
    "warnings=\{self.warning_count()}",
    "passed=\{self.passed()}",
    "checksum=\{self.checksum()}",
  ].join("\n")
}

///|
pub fn manifest_check_json(check : ManifestCheck) -> String {
  "{" +
  "\"id\":\{json_escape(check.id)}," +
  "\"passed\":\{check.passed}," +
  "\"value\":\{json_escape(check.value)}," +
  "\"severity\":\{json_escape(severity_name(check.severity))}" +
  "}"
}

///|
pub fn ReleaseManifest::to_json(self : ReleaseManifest) -> String {
  "{" +
  "\"package_name\":\{json_escape(self.package_name)}," +
  "\"package_version\":\{json_escape(self.package_version)}," +
  "\"document_count\":\{self.document_count}," +
  "\"source_checksum\":\{json_escape(self.source_checksum)}," +
  "\"output_checksum\":\{json_escape(self.output_checksum)}," +
  "\"policy_checksum\":\{json_escape(self.policy_checksum)}," +
  "\"generated_at\":\{json_escape(self.generated_at)}," +
  "\"manifest_checksum\":\{json_escape(self.checksum())}," +
  "\"checks\":[" +
  self.checks.map(manifest_check_json).join(",") +
  "]}"
}

///|
pub fn ReleaseManifest::to_markdown(self : ReleaseManifest) -> String {
  let lines = [
    "# Release manifest", "", "| Check | Status | Severity | Value |", "| --- | --- | --- | --- |",
  ]
  for check in self.checks {
    lines.push(
      "| \{check.id} | \{check.passed} | \{severity_name(check.severity)} | \{check.value} |",
    )
  }
  lines.push("")
  lines.push("passed=\{self.passed()}")
  lines.push("checksum=\{self.checksum()}")
  lines.join("\n")
}

///|
pub fn release_decision(manifest : ReleaseManifest) -> ReleaseDecision {
  let reasons = manifest
    .failed_checks()
    .map(fn(check) { check.id + ": " + check.value })
  {
    approved: manifest.passed() && manifest.document_count >= 0,
    reasons,
    manifest_checksum: manifest.checksum(),
  }
}

///|
pub fn release_decision_summary(decision : ReleaseDecision) -> String {
  let reasons = decision.reasons.join(",")
  [
    "approved=\{decision.approved}",
    "reasons=\{reasons}",
    "manifest_checksum=\{decision.manifest_checksum}",
  ].join("\n")
}

///|
pub fn release_manifest_for_result(
  package_name : String,
  package_version : String,
  input : String,
  result : DeidResult,
  policy : RedactionPolicy,
) -> ReleaseManifest {
  let manifest = ReleaseManifest::new(
    package_name,
    package_version,
    stable_hash(policy_summary(policy)),
  )
  manifest.set_counts(1, stable_hash(input), stable_hash(result.text))
  manifest.add_check(
    manifest_check(
      "output_contract",
      output_contract_passes(input, result),
      "output contract",
      Error,
    ),
  )
  manifest.add_check(
    manifest_check(
      "finding_spans",
      quality_finding_spans_valid(input, result.findings),
      "finding spans",
      Error,
    ),
  )
  manifest.add_check(
    manifest_check(
      "audit_counts",
      result.audit.finding_count == result.findings.length(),
      "audit count matches",
      Error,
    ),
  )
  manifest.add_check(
    manifest_check(
      "no_original_phi",
      quality_no_original_phi(result.text, result.findings),
      "no original selected values",
      Error,
    ),
  )
  manifest
}

///|
pub fn release_manifest_for_batch(
  package_name : String,
  package_version : String,
  items : Array[BatchItem],
  outcome : BatchOutcome,
  policy : RedactionPolicy,
) -> ReleaseManifest {
  let manifest = ReleaseManifest::new(
    package_name,
    package_version,
    stable_hash(policy_summary(policy)),
  )
  let input_checksum = stable_hash(
    items.map(fn(item) { item.id + ":" + item.text }).join("\n"),
  )
  let output_checksum = stable_hash(
    outcome.results
    .map(fn(item) { item.id + ":" + item.result.text })
    .join("\n"),
  )
  manifest.set_counts(items.length(), input_checksum, output_checksum)
  manifest.add_check(
    manifest_check(
      "batch_consistency",
      batch_consistency_issues(outcome).is_empty(),
      "batch totals",
      Error,
    ),
  )
  manifest.add_check(
    manifest_check(
      "batch_success_ratio",
      batch_success_ratio(outcome) >= 1.0,
      "all documents processed",
      Error,
    ),
  )
  manifest
}

///|
pub fn release_manifest_check_ids(manifest : ReleaseManifest) -> Array[String] {
  manifest.checks.map(fn(check) { check.id })
}

///|
pub fn release_manifest_duplicate_checks(
  manifest : ReleaseManifest,
) -> Array[String] {
  let counts : Map[String, Int] = Map([])
  for check in manifest.checks {
    counts[check.id] = counts.get_or_default(check.id, 0) + 1
  }
  let result = []
  for id, count in counts {
    if count > 1 {
      result.push(id)
    }
  }
  result
}

///|
pub fn release_manifest_is_well_formed(manifest : ReleaseManifest) -> Bool {
  manifest.package_name.trim().length() > 0 &&
  manifest.package_version.trim().length() > 0 &&
  manifest.document_count >= 0 &&
  release_manifest_duplicate_checks(manifest).is_empty()
}

///|
pub fn release_manifest_ready(manifest : ReleaseManifest) -> Bool {
  release_manifest_is_well_formed(manifest) &&
  manifest.passed() &&
  manifest.error_count() == 0
}

///|
pub fn release_manifest_checksum_stable(manifest : ReleaseManifest) -> Bool {
  manifest.checksum() == manifest.checksum()
}

///|
pub fn release_manifest_merge(
  left : ReleaseManifest,
  right : ReleaseManifest,
) -> ReleaseManifest {
  let result = ReleaseManifest::new(
    left.package_name,
    left.package_version,
    left.policy_checksum,
  )
  result.set_counts(
    left.document_count + right.document_count,
    stable_hash(left.source_checksum + right.source_checksum),
    stable_hash(left.output_checksum + right.output_checksum),
  )
  for check in left.checks {
    result.add_check(check)
  }
  for check in right.checks {
    result.add_check(check)
  }
  result
}