///|
/// Cryptographic SHA-256 digest encoded as lowercase hexadecimal. Deployment
/// bundles use UTF-8 bytes so the result is stable across native and Wasm
/// targets.
pub fn sha256_hex(text : String) -> String {
  let source = @utf8.encode(text)
  let bytes = source.to_array()
  let bit_length = source.length().to_uint64() * 8UL
  bytes.push(b'\x80')
  while bytes.length() % 64 != 56 {
    bytes.push(b'\x00')
  }
  for shift = 56; shift >= 0; shift = shift - 8 {
    bytes.push((bit_length >> shift).to_byte())
  }
  let state : Array[UInt] = [
    0x6a09e667U, 0xbb67ae85U, 0x3c6ef372U, 0xa54ff53aU, 0x510e527fU, 0x9b05688cU,
    0x1f83d9abU, 0x5be0cd19U,
  ]
  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,
  ]
  for chunk = 0; chunk < bytes.length(); chunk = chunk + 64 {
    let words = Array::make(64, 0U)
    for index = 0; index < 16; index = index + 1 {
      let offset = chunk + index * 4
      words[index] = (bytes[offset].to_uint() << 24) |
        (bytes[offset + 1].to_uint() << 16) |
        (bytes[offset + 2].to_uint() << 8) |
        bytes[offset + 3].to_uint()
    }
    for index = 16; index < 64; index = index + 1 {
      let previous = words[index - 15]
      let s0 = rotate_right(previous, 7) ^
        rotate_right(previous, 18) ^
        (previous >> 3)
      let prior = words[index - 2]
      let s1 = rotate_right(prior, 17) ^ rotate_right(prior, 19) ^ (prior >> 10)
      words[index] = words[index - 16] + s0 + words[index - 7] + s1
    }
    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; index = index + 1 {
      let sum1 = rotate_right(e, 6) ^ rotate_right(e, 11) ^ rotate_right(e, 25)
      let choose = (e & f) ^ (e.lnot() & g)
      let temporary1 = 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 temporary2 = sum0 + majority
      h = g
      g = f
      f = e
      e = d + temporary1
      d = c
      c = b
      b = a
      a = temporary1 + temporary2
    }
    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
  }
  let mut digest = ""
  for word in state {
    let hex = word.to_string(radix=16)
    let mut padding = hex.length()
    while padding < 8 {
      digest = digest + "0"
      padding = padding + 1
    }
    digest = digest + hex
  }
  digest
}

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

///|
/// Metadata for one source that was validated by the hybrid compiler before
/// packaging. It deliberately describes, rather than serializes, the current
/// in-memory instruction format so bundles remain portable across targets.
pub(all) struct CompiledModuleMetadata {
  name : String
  source_sha256 : String
  instruction_count : Int
  semantic_instruction_count : Int
} derive(Eq, Debug)

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

///|
pub extend CompiledModuleMetadata with Debug::{to_repr}

///|
pub(all) struct DeploymentManifest {
  format_version : Int
  wren_version : String
  compiler_abi : String
  entry_module : String
  capabilities : Array[String]
  compiled_modules : Array[CompiledModuleMetadata]
}

///|
pub(all) struct SignedDeploymentBundle {
  manifest : DeploymentManifest
  modules : Array[VerifiedModule]
  payload_sha256 : String
  key_id : String
  signature : String
}

///|
pub(all) enum DeploymentBundleResult {
  DeploymentBundleReady(SignedDeploymentBundle)
  DeploymentBundleFailed(String)
}

///|
fn deployment_read_text(
  characters : Array[Char],
  start : Int,
) -> (String, Int)? {
  let (length, after_length) = match archive_read_number(characters, start) {
    Some(value) => value
    None => return None
  }
  archive_take_text(characters, after_length, length)
}

///|
fn sorted_unique_strings(values : Array[String]) -> Array[String]? {
  let result : Array[String] = []
  for value in values {
    if value.length() == 0 {
      return None
    }
    let mut index = 0
    while index < result.length() && result[index].lexical_compare(value) < 0 {
      index = index + 1
    }
    if index < result.length() && result[index] == value {
      return None
    }
    result.insert(index, value)
  }
  Some(result)
}

///|
fn deployment_manifest_text(manifest : DeploymentManifest) -> String {
  let mut text = "MWD1\n" +
    manifest.format_version.to_string() +
    "\n" +
    manifest.wren_version.length().to_string() +
    "\n" +
    manifest.wren_version +
    manifest.compiler_abi.length().to_string() +
    "\n" +
    manifest.compiler_abi +
    manifest.entry_module.length().to_string() +
    "\n" +
    manifest.entry_module +
    manifest.capabilities.length().to_string() +
    "\n"
  for capability in manifest.capabilities {
    text = text + capability.length().to_string() + "\n" + capability
  }
  text = text + manifest.compiled_modules.length().to_string() + "\n"
  for entry in manifest.compiled_modules {
    text = text +
      entry.name.length().to_string() +
      "\n" +
      entry.name +
      entry.source_sha256.length().to_string() +
      "\n" +
      entry.source_sha256 +
      entry.instruction_count.to_string() +
      "\n" +
      entry.semantic_instruction_count.to_string() +
      "\n"
  }
  text
}

///|
fn deployment_unsigned_payload(
  manifest : DeploymentManifest,
  modules : Array[VerifiedModule],
) -> String {
  let manifest_text = deployment_manifest_text(manifest)
  let archive = encode_module_archive(modules)
  manifest_text.length().to_string() +
  "\n" +
  manifest_text +
  archive.length().to_string() +
  "\n" +
  archive
}

///|
fn decode_deployment_manifest(text : String) -> DeploymentManifest? {
  let characters = text.to_array()
  let header = ['M', 'W', 'D', '1', '\n']
  if characters.length() < header.length() {
    return None
  }
  for index = 0; index < header.length(); index = index + 1 {
    if characters[index] != header[index] {
      return None
    }
  }
  let (format_version, after_version) = match
    archive_read_number(characters, header.length()) {
    Some(value) => value
    None => return None
  }
  let (wren_version, after_wren) = match
    deployment_read_text(characters, after_version) {
    Some(value) => value
    None => return None
  }
  let (compiler_abi, after_abi) = match
    deployment_read_text(characters, after_wren) {
    Some(value) => value
    None => return None
  }
  let (entry_module, after_entry) = match
    deployment_read_text(characters, after_abi) {
    Some(value) => value
    None => return None
  }
  let (capability_count, after_capability_count) = match
    archive_read_number(characters, after_entry) {
    Some(value) => value
    None => return None
  }
  let capabilities : Array[String] = []
  let mut cursor = after_capability_count
  for _index = 0; _index < capability_count; _index = _index + 1 {
    let (capability, next) = match deployment_read_text(characters, cursor) {
      Some(value) => value
      None => return None
    }
    capabilities.push(capability)
    cursor = next
  }
  let (module_count, after_module_count) = match
    archive_read_number(characters, cursor) {
    Some(value) => value
    None => return None
  }
  cursor = after_module_count
  let compiled_modules : Array[CompiledModuleMetadata] = []
  for _index = 0; _index < module_count; _index = _index + 1 {
    let (name, after_name) = match deployment_read_text(characters, cursor) {
      Some(value) => value
      None => return None
    }
    let (digest, after_digest) = match
      deployment_read_text(characters, after_name) {
      Some(value) => value
      None => return None
    }
    let (instruction_count, after_instruction_count) = match
      archive_read_number(characters, after_digest) {
      Some(value) => value
      None => return None
    }
    let (semantic_count, next) = match
      archive_read_number(characters, after_instruction_count) {
      Some(value) => value
      None => return None
    }
    compiled_modules.push({
      name,
      source_sha256: digest,
      instruction_count,
      semantic_instruction_count: semantic_count,
    })
    cursor = next
  }
  if cursor != characters.length() {
    return None
  }
  Some({
    format_version,
    wren_version,
    compiler_abi,
    entry_module,
    capabilities,
    compiled_modules,
  })
}

///|
/// Encode a signed bundle to the deterministic MWDS1 transport format.
pub fn encode_signed_deployment_bundle(
  bundle : SignedDeploymentBundle,
) -> String {
  let unsigned = deployment_unsigned_payload(bundle.manifest, bundle.modules)
  "MWDS1\n" +
  unsigned.length().to_string() +
  "\n" +
  unsigned +
  bundle.payload_sha256.length().to_string() +
  "\n" +
  bundle.payload_sha256 +
  bundle.key_id.length().to_string() +
  "\n" +
  bundle.key_id +
  bundle.signature.length().to_string() +
  "\n" +
  bundle.signature
}

///|
/// Decode and integrity-check MWDS1. Signature trust is intentionally a
/// separate verification step so the host selects keys.
pub fn decode_signed_deployment_bundle(
  encoded : String,
) -> DeploymentBundleResult {
  let characters = encoded.to_array()
  let header = ['M', 'W', 'D', 'S', '1', '\n']
  if characters.length() < header.length() {
    return DeploymentBundleFailed("signed bundle header is missing")
  }
  for index = 0; index < header.length(); index = index + 1 {
    if characters[index] != header[index] {
      return DeploymentBundleFailed("unsupported signed bundle format")
    }
  }
  let (unsigned, after_unsigned) = match
    deployment_read_text(characters, header.length()) {
    Some(value) => value
    None => return DeploymentBundleFailed("invalid signed bundle payload")
  }
  let (payload_digest, after_digest) = match
    deployment_read_text(characters, after_unsigned) {
    Some(value) => value
    None => return DeploymentBundleFailed("invalid signed bundle digest")
  }
  let (key_id, after_key) = match
    deployment_read_text(characters, after_digest) {
    Some(value) => value
    None => return DeploymentBundleFailed("invalid signed bundle key id")
  }
  let (signature, after_signature) = match
    deployment_read_text(characters, after_key) {
    Some(value) => value
    None => return DeploymentBundleFailed("invalid signed bundle signature")
  }
  if after_signature != characters.length() {
    return DeploymentBundleFailed("unexpected trailing signed bundle data")
  }
  if sha256_hex(unsigned) != payload_digest {
    return DeploymentBundleFailed("signed bundle payload digest mismatch")
  }
  let unsigned_characters = unsigned.to_array()
  let (manifest_text, after_manifest) = match
    deployment_read_text(unsigned_characters, 0) {
    Some(value) => value
    None => return DeploymentBundleFailed("invalid deployment manifest")
  }
  let (archive, after_archive) = match
    deployment_read_text(unsigned_characters, after_manifest) {
    Some(value) => value
    None => return DeploymentBundleFailed("invalid deployment module archive")
  }
  if after_archive != unsigned_characters.length() {
    return DeploymentBundleFailed("unexpected deployment payload data")
  }
  let manifest = match decode_deployment_manifest(manifest_text) {
    Some(value) => value
    None => return DeploymentBundleFailed("invalid deployment manifest")
  }
  let modules = match decode_module_archive(archive) {
    ModuleArchiveDecoded(value) => value
    ModuleArchiveFailed(message) => return DeploymentBundleFailed(message)
  }
  DeploymentBundleReady({
    manifest,
    modules,
    payload_sha256: payload_digest,
    key_id,
    signature,
  })
}

///|
/// Validate every module with the compiler, canonicalize capabilities, and
/// create a signed deployment bundle. `signer` receives the canonical payload
/// SHA-256 and may use Ed25519, a hardware key, or a remote signing service.
pub fn build_signed_deployment_bundle(
  entry_module : String,
  modules : Array[VerifiedModule],
  capabilities : Array[String],
  compiler_abi : String,
  key_id : String,
  signer : (String) -> String,
) -> DeploymentBundleResult {
  if !safe_module_specifier(entry_module) {
    return DeploymentBundleFailed("unsafe entry module")
  }
  if compiler_abi.length() == 0 || key_id.length() == 0 {
    return DeploymentBundleFailed(
      "compiler ABI and signing key id are required",
    )
  }
  let canonical_capabilities = match sorted_unique_strings(capabilities) {
    Some(value) => value
    None =>
      return DeploymentBundleFailed(
        "capabilities must be non-empty and may not contain duplicates",
      )
  }
  let metadata : Array[CompiledModuleMetadata] = []
  let mut found_entry = false
  for entry in modules {
    if entry.name == entry_module {
      found_entry = true
    }
    if module_checksum(entry.source) != entry.checksum {
      return DeploymentBundleFailed("checksum mismatch for '\{entry.name}'")
    }
    let program = match compile_module_bytecode(entry.name, entry.source) {
      BytecodeCompiled(value) => value
      BytecodeCompileFailed(issues) =>
        return DeploymentBundleFailed(
          "module '\{entry.name}' does not compile: \{issues[0].message}",
        )
    }
    let mut semantic_count = 0
    for instruction in program.instructions {
      match instruction.op {
        EvaluateSemantic(_) | ExecuteSemantic(_) =>
          semantic_count = semantic_count + 1
        _ => ()
      }
    }
    metadata.push({
      name: entry.name,
      source_sha256: sha256_hex(entry.source),
      instruction_count: program.instructions.length(),
      semantic_instruction_count: semantic_count,
    })
  }
  if !found_entry {
    return DeploymentBundleFailed("entry module '\{entry_module}' is missing")
  }
  let manifest = {
    format_version: 1,
    wren_version: "0.4",
    compiler_abi,
    entry_module,
    capabilities: canonical_capabilities,
    compiled_modules: metadata,
  }
  let digest = sha256_hex(deployment_unsigned_payload(manifest, modules))
  DeploymentBundleReady({
    manifest,
    modules,
    payload_sha256: digest,
    key_id,
    signature: signer(digest),
  })
}

///|
/// Verify module checksums, compiler metadata, canonical payload digest, and
/// the external signature before returning a loader.
pub fn verify_signed_deployment_bundle(
  bundle : SignedDeploymentBundle,
  verifier : (String, String, String) -> Bool,
) -> DeploymentBundleResult {
  let rebuilt = match
    build_signed_deployment_bundle(
      bundle.manifest.entry_module,
      bundle.modules,
      bundle.manifest.capabilities,
      bundle.manifest.compiler_abi,
      bundle.key_id,
      fn(_) { bundle.signature },
    ) {
    DeploymentBundleReady(value) => value
    DeploymentBundleFailed(message) => return DeploymentBundleFailed(message)
  }
  if rebuilt.manifest.compiled_modules != bundle.manifest.compiled_modules {
    return DeploymentBundleFailed("compiled module metadata mismatch")
  }
  if rebuilt.payload_sha256 != bundle.payload_sha256 {
    return DeploymentBundleFailed("deployment payload digest mismatch")
  }
  if !verifier(bundle.key_id, bundle.payload_sha256, bundle.signature) {
    return DeploymentBundleFailed("deployment signature was rejected")
  }
  DeploymentBundleReady(bundle)
}

///|
pub fn deployment_bundle_loader(
  bundle : SignedDeploymentBundle,
) -> (String, String) -> ModuleLoadResult {
  verified_module_loader(bundle.modules)
}