///|
/// Evidence content and provenance supplied when building a manifest.
pub(all) struct EvidenceDocument {
  id : String
  path : String
  source : EvidenceSource
  content : Bytes
} derive(Debug, Eq)

///|
/// A manifest entry binds evidence identity and source to its size and SHA-256 digest.
pub(all) struct EvidenceManifestEntry {
  id : String
  path : String
  source_id : String
  size_bytes : Int
  sha256 : String
} derive(Debug, Eq, ToJson)

///|
/// A deterministic manifest for a collection of evidence documents.
pub(all) struct EvidenceManifest {
  entries : Array[EvidenceManifestEntry]
} derive(Debug, Eq, ToJson)

///|
fn rotate_right(value : UInt, shift : Int) -> UInt {
  (value >> shift) | (value << (32 - shift))
}

///|
fn sha256(content : Bytes) -> String {
  let padded : Array[Byte] = []
  for byte in content {
    padded.push(byte)
  }
  padded.push(0x80)
  for index = padded.length(); index % 64 != 56; {
    padded.push(0)
    continue index + 1
  }

  let byte_length = content.length().reinterpret_as_uint()
  let bit_length_high = byte_length >> 29
  let bit_length_low = byte_length << 3
  for shift in [24, 16, 8, 0] {
    padded.push((bit_length_high >> shift).to_byte())
  }
  for shift in [24, 16, 8, 0] {
    padded.push(((bit_length_low >> shift) & 0xffU).to_byte())
  }

  let constants : Array[UInt] = [
    0x428a2f98U, 0x71374491U, 0xb5c0fbcfU, 0xe9b5dba5U, 0x3956c25bU, 0x59f111f1U,
    0x923f82a4U, 0xab1c5ed5U, 0xd807aa98U, 0x12835b01U, 0x243185beU, 0x550c7dc3U,
    0x72be5d74U, 0x80deb1feU, 0x9bdc06a7U, 0xc19bf174U, 0xe49b69c1U, 0xefbe4786U,
    0x0fc19dc6U, 0x240ca1ccU, 0x2de92c6fU, 0x4a7484aaU, 0x5cb0a9dcU, 0x76f988daU,
    0x983e5152U, 0xa831c66dU, 0xb00327c8U, 0xbf597fc7U, 0xc6e00bf3U, 0xd5a79147U,
    0x06ca6351U, 0x14292967U, 0x27b70a85U, 0x2e1b2138U, 0x4d2c6dfcU, 0x53380d13U,
    0x650a7354U, 0x766a0abbU, 0x81c2c92eU, 0x92722c85U, 0xa2bfe8a1U, 0xa81a664bU,
    0xc24b8b70U, 0xc76c51a3U, 0xd192e819U, 0xd6990624U, 0xf40e3585U, 0x106aa070U,
    0x19a4c116U, 0x1e376c08U, 0x2748774cU, 0x34b0bcb5U, 0x391c0cb3U, 0x4ed8aa4aU,
    0x5b9cca4fU, 0x682e6ff3U, 0x748f82eeU, 0x78a5636fU, 0x84c87814U, 0x8cc70208U,
    0x90befffaU, 0xa4506cebU, 0xbef9a3f7U, 0xc67178f2U,
  ]
  let state : Array[UInt] = [
    0x6a09e667U, 0xbb67ae85U, 0x3c6ef372U, 0xa54ff53aU, 0x510e527fU, 0x9b05688cU,
    0x1f83d9abU, 0x5be0cd19U,
  ]

  for block_start = 0; block_start < padded.length(); {
    let words : Array[UInt] = []
    for index = 0; index < 16; {
      let offset = block_start + index * 4
      words.push(
        (padded[offset].to_uint() << 24) |
        (padded[offset + 1].to_uint() << 16) |
        (padded[offset + 2].to_uint() << 8) |
        padded[offset + 3].to_uint(),
      )
      continue index + 1
    }
    for index = 16; index < 64; {
      let x = words[index - 15]
      let y = words[index - 2]
      let sigma0 = rotate_right(x, 7) ^ rotate_right(x, 18) ^ (x >> 3)
      let sigma1 = rotate_right(y, 17) ^ rotate_right(y, 19) ^ (y >> 10)
      words.push(words[index - 16] + sigma0 + words[index - 7] + sigma1)
      continue index + 1
    }

    let mut a = state[0]
    let mut b = state[1]
    let mut c = state[2]
    let mut d = state[3]
    let mut e = state[4]
    let mut f = state[5]
    let mut g = state[6]
    let mut h = state[7]
    for index = 0; index < 64; {
      let sum1 = rotate_right(e, 6) ^ rotate_right(e, 11) ^ rotate_right(e, 25)
      let choose = (e & f) ^ ((e ^ 0xffffffffU) & g)
      let temp1 = h + sum1 + choose + constants[index] + words[index]
      let sum0 = rotate_right(a, 2) ^ rotate_right(a, 13) ^ rotate_right(a, 22)
      let majority = (a & b) ^ (a & c) ^ (b & c)
      let temp2 = sum0 + majority
      h = g
      g = f
      f = e
      e = d + temp1
      d = c
      c = b
      b = a
      a = temp1 + temp2
      continue index + 1
    }
    state[0] = state[0] + a
    state[1] = state[1] + b
    state[2] = state[2] + c
    state[3] = state[3] + d
    state[4] = state[4] + e
    state[5] = state[5] + f
    state[6] = state[6] + g
    state[7] = state[7] + h
    continue block_start + 64
  }

  let digest_bytes : Array[Byte] = []
  for word in state {
    digest_bytes.push((word >> 24).to_byte())
    digest_bytes.push(((word >> 16) & 0xffU).to_byte())
    digest_bytes.push(((word >> 8) & 0xffU).to_byte())
    digest_bytes.push((word & 0xffU).to_byte())
  }
  let hex = "0123456789abcdef"
  let mut output = ""
  for byte in digest_bytes {
    let value = byte.to_uint()
    let high = (value >> 4).reinterpret_as_int()
    let low = (value & 0xfU).reinterpret_as_int()
    output = output +
      hex[high:high + 1].to_owned() +
      hex[low:low + 1].to_owned()
  }
  output
}

///|
fn manifest_entry(document : EvidenceDocument) -> EvidenceManifestEntry {
  {
    id: document.id,
    path: document.path,
    source_id: normalize_source_id(
      document.source.system,
      document.source.component,
      document.source.host,
    ),
    size_bytes: document.content.length(),
    sha256: sha256(document.content),
  }
}

///|
/// Builds ordered evidence metadata and SHA-256 content digests.
///
/// # Example
/// ```mbt check
/// test {
///   let source = EvidenceSource::{
///     system: "host",
///     component: "service",
///     host: None,
///   }
///   let document = EvidenceDocument::{
///     id: "e-1",
///     path: "service.log",
///     source,
///     content: b"abc",
///   }
///   let manifest = build_evidence_manifest([document])
///   assert_eq(manifest.entries[0].size_bytes, 3)
/// }
/// ```
pub fn build_evidence_manifest(
  documents : Array[EvidenceDocument],
) -> EvidenceManifest {
  let entries : Array[EvidenceManifestEntry] = []
  for document in documents {
    entries.push(manifest_entry(document))
  }
  { entries, }
}

///|
/// Checks evidence identity, source, path, byte size, and digest against a manifest.
///
/// # Example
/// ```mbt check
/// test {
///   let source = EvidenceSource::{
///     system: "host",
///     component: "service",
///     host: None,
///   }
///   let document = EvidenceDocument::{
///     id: "e-1",
///     path: "service.log",
///     source,
///     content: b"abc",
///   }
///   let manifest = build_evidence_manifest([document])
///   let changed = EvidenceDocument::{
///     id: "e-1",
///     path: "service.log",
///     source,
///     content: b"abd",
///   }
///   assert_false(verify_evidence_manifest(manifest, [changed]))
/// }
/// ```
pub fn verify_evidence_manifest(
  manifest : EvidenceManifest,
  documents : Array[EvidenceDocument],
) -> Bool {
  if manifest.entries.length() != documents.length() {
    return false
  }
  for index in 0..