///|
/// Receipt for an accepted report, including its full binding and exact UTF-8
/// source digest. Returned arrays are independent copies of manifest storage.
pub(all) struct IntegrityDocument {
  binding : @ledger.ReportBinding
  sha256 : String
  utf8_bytes : Int32
} derive(Debug, Eq)

///|
pub extend IntegrityDocument with Eq::{equal, not_equal}

///|
pub extend IntegrityDocument with @debug.Debug::{to_repr}

///|
/// A validated checksum manifest. Trust the expected manifest separately from
/// the archive: checksums do not authenticate its author or prove execution.
struct IntegrityManifest {
  run_id : String
  revision : String
  deny_recovered : Bool
  plan_sha256 : String
  archive_sha256 : String
  documents : Array[IntegrityDocument]
  missing_reports : Array[String]
}

///|
pub fn IntegrityManifest::documents(
  self : IntegrityManifest,
) -> Array[IntegrityDocument] {
  self.documents.copy()
}

///|
pub fn IntegrityManifest::missing_reports(
  self : IntegrityManifest,
) -> Array[String] {
  self.missing_reports.copy()
}

///|
fn sha256_text(source : String) -> String {
  @crypto.bytes_to_hex_string(@crypto.sha256(@utf8.encode(source)))
}

///|
/// Bind the full validated inventory, recovery policy, accepted report bindings
/// and original XML. Incomplete and failing archives can have valid manifests.
/// Canonicalization is versioned MoonTestLedger JSON, not RFC 8785.
pub fn Evidence::integrity(
  self : Evidence,
  limits~ : @ledger.Limits = @ledger.Limits::default(),
) -> IntegrityManifest raise @ledger.InputError {
  // Revalidation refuses projections or budgets that cannot be preserved.
  let archive = self.encode(limits~)
  let snapshot = self.snapshot()
  let documents = snapshot.documents.map(document => {
    let bytes = @utf8.encode(document.report.source)
    {
      binding: document.binding,
      sha256: @crypto.bytes_to_hex_string(@crypto.sha256(bytes)),
      utf8_bytes: bytes.length(),
    }
  })
  documents.sort_by((a, b) => a.binding.id.compare(b.binding.id))
  let missing_reports = self.summary().missing_reports
  missing_reports.sort()
  let manifest : IntegrityManifest = {
    run_id: snapshot.plan.run_id,
    revision: snapshot.plan.revision,
    deny_recovered: self.deny_recovered,
    plan_sha256: sha256_text(encode_plan(snapshot.plan)),
    archive_sha256: sha256_text(archive),
    documents,
    missing_reports,
  }
  ignore(manifest.encode(limits~))
  manifest
}

///|
fn mismatch(path : String, message : String) -> @ledger.InputError {
  bad("IntegrityMismatch", path, message)
}

///|
/// Check every manifest component against the replayed archive. Raises on any
/// discrepancy without mutating either input. Success does not change the
/// archive's existing gate: call Evidence::exit_code after checking integrity.
pub fn Evidence::check_integrity(
  self : Evidence,
  expected : IntegrityManifest,
  limits~ : @ledger.Limits = @ledger.Limits::default(),
) -> Unit raise @ledger.InputError {
  ignore(expected.encode(limits~))
  let actual = self.integrity(limits~)
  if actual.run_id != expected.run_id {
    raise mismatch("$/run_id", "Run identity differs from the trusted manifest")
  }
  if actual.revision != expected.revision {
    raise mismatch("$/revision", "Revision differs from the trusted manifest")
  }
  if actual.deny_recovered != expected.deny_recovered {
    raise mismatch(
      "$/deny_recovered", "Recovery gate policy differs from the trusted manifest",
    )
  }
  if actual.plan_sha256 != expected.plan_sha256 {
    raise mismatch(
      "$/plan_sha256", "Full expected inventory differs from the trusted manifest",
    )
  }
  if actual.missing_reports != expected.missing_reports {
    raise mismatch(
      "$/missing_reports", "Missing report inventory differs from the trusted manifest",
    )
  }
  if actual.documents.length() != expected.documents.length() {
    raise mismatch(
      "$/documents", "Accepted report count differs from the trusted manifest",
    )
  }
  for i, document in actual.documents {
    let receipt = expected.documents[i]
    let path = "$/documents/" + document.binding.id
    if document.binding != receipt.binding {
      raise mismatch(
        path, "Report identity or full binding differs from the trusted manifest",
      )
    }
    if document.utf8_bytes != receipt.utf8_bytes ||
      document.sha256 != receipt.sha256 {
      raise mismatch(
        path + "/sha256",
        "Exact original XML bytes differ from the trusted manifest",
      )
    }
  }
  if actual.archive_sha256 != expected.archive_sha256 {
    raise mismatch(
      "$/archive_sha256", "Canonical archive digest differs from the trusted manifest",
    )
  }
}