///|
/// One integrity-checked source in an in-memory deployment bundle.
pub(all) struct VerifiedModule {
  name : String
  source : String
  checksum : String
}

///|
pub fn VerifiedModule::new(name : String, source : String) -> VerifiedModule {
  { name, source, checksum: module_checksum(source), }
}

///|
/// Stable FNV-1a checksum used to detect accidental bundle corruption. This is
/// not intended to replace a cryptographic signature at a trust boundary.
pub fn module_checksum(source : String) -> String {
  let mut hash = 2166136261U
  for character in source.to_array() {
    hash = (hash ^ character.to_int().reinterpret_as_uint()) * 16777619U
  }
  hash.to_string(radix=16)
}

///|
fn safe_module_specifier(name : String) -> Bool {
  name.length() > 0 &&
  !name.has_prefix("/") &&
  !name.has_prefix("\\") &&
  !name.contains("..") &&
  !name.contains(":")
}

///|
/// Build a sandboxed loader from a checked source bundle. Absolute paths and
/// traversal segments are rejected before lookup.
pub fn verified_module_loader(
  modules : Array[VerifiedModule],
) -> (String, String) -> ModuleLoadResult {
  fn(_importer : String, specifier : String) {
    if !safe_module_specifier(specifier) {
      return ModuleLoadFailed("unsafe module specifier '{specifier}'")
    }
    let mut found : VerifiedModule? = None
    for entry in modules {
      if entry.name == specifier {
        if found is Some(_) {
          return ModuleLoadFailed("duplicate bundled module '{specifier}'")
        }
        found = Some(entry)
      }
    }
    match found {
      None => ModuleLoadFailed("bundled module '{specifier}' was not found")
      Some(entry) =>
        if module_checksum(entry.source) != entry.checksum {
          ModuleLoadFailed("checksum mismatch for bundled module '{specifier}'")
        } else {
          ModuleLoaded(entry.name, entry.source)
        }
    }
  }
}

///|
/// Content snapshot used by polling file watchers and hot-reload hosts.
pub(all) struct ModuleSnapshot {
  names : Array[String]
  checksums : Array[String]
}

///|
pub fn ModuleSnapshot::from_modules(
  modules : Array[VerifiedModule],
) -> ModuleSnapshot {
  let names : Array[String] = []
  let checksums : Array[String] = []
  for entry in modules {
    names.push(entry.name)
    checksums.push(module_checksum(entry.source))
  }
  { names, checksums, }
}

///|
/// Return added, changed and removed names between two snapshots.
pub fn ModuleSnapshot::changes(
  self : ModuleSnapshot,
  newer : ModuleSnapshot,
) -> Array[String] {
  let changed : Array[String] = []
  for index = 0; index < newer.names.length(); index = index + 1 {
    let name = newer.names[index]
    let mut previous = -1
    for old_index = 0
        old_index < self.names.length()
        old_index = old_index + 1 {
      if self.names[old_index] == name {
        previous = old_index
      }
    }
    if previous < 0 || self.checksums[previous] != newer.checksums[index] {
      changed.push(name)
    }
  }
  for name in self.names {
    let mut remains = false
    for current in newer.names {
      if current == name {
        remains = true
      }
    }
    if !remains {
      changed.push(name)
    }
  }
  changed
}

///|
/// Result of decoding a deterministic MoonWren module archive.
pub(all) enum ModuleArchiveResult {
  ModuleArchiveDecoded(Array[VerifiedModule])
  ModuleArchiveFailed(String)
}

///|
fn archive_character_length(value : String) -> Int {
  value.to_array().length()
}

///|
fn archive_read_number(characters : Array[Char], start : Int) -> (Int, Int)? {
  if start < 0 || start >= characters.length() {
    return None
  }
  let mut cursor = start
  let mut value = 0
  let mut digits = 0
  while cursor < characters.length() && characters[cursor].to_int() != 10 {
    let code = characters[cursor].to_int()
    if code < 48 || code > 57 {
      return None
    }
    value = value * 10 + code - 48
    digits = digits + 1
    cursor = cursor + 1
  }
  if digits == 0 || cursor >= characters.length() {
    None
  } else {
    Some((value, cursor + 1))
  }
}

///|
fn archive_take_text(
  characters : Array[Char],
  start : Int,
  count : Int,
) -> (String, Int)? {
  if count < 0 || start < 0 || start + count > characters.length() {
    return None
  }
  let result : Array[Char] = []
  for index = start; index < start + count; index = index + 1 {
    result.push(characters[index])
  }
  Some((String::from_array(result), start + count))
}

///|
/// Encode modules into the deterministic `MWA1` archive format. Lengths count
/// Unicode characters, so sources may contain newlines, NULs and non-ASCII
/// text without escaping or platform-dependent byte conversions.
pub fn encode_module_archive(modules : Array[VerifiedModule]) -> String {
  let mut result = "MWA1\n" + modules.length().to_string() + "\n"
  for entry in modules {
    result = result +
      archive_character_length(entry.name).to_string() +
      "\n" +
      entry.name +
      archive_character_length(entry.source).to_string() +
      "\n" +
      entry.source +
      archive_character_length(entry.checksum).to_string() +
      "\n" +
      entry.checksum
  }
  result
}

///|
/// Decode an `MWA1` archive and reject malformed lengths, unsafe or duplicate
/// module names, trailing data, and checksum mismatches.
pub fn decode_module_archive(archive : String) -> ModuleArchiveResult {
  let characters = archive.to_array()
  let header = ['M', 'W', 'A', '1', '\n']
  if characters.length() < header.length() {
    return ModuleArchiveFailed("module archive header is missing")
  }
  for index = 0; index < header.length(); index = index + 1 {
    if characters[index] != header[index] {
      return ModuleArchiveFailed("unsupported module archive format")
    }
  }
  let (count, after_count) = match archive_read_number(characters, 5) {
    Some(value) => value
    None => return ModuleArchiveFailed("invalid module count")
  }
  let modules : Array[VerifiedModule] = []
  let mut cursor = after_count
  for _entry_index = 0; _entry_index < count; _entry_index = _entry_index + 1 {
    let (name_length, after_name_length) = match
      archive_read_number(characters, cursor) {
      Some(value) => value
      None => return ModuleArchiveFailed("invalid module name length")
    }
    let (name, after_name) = match
      archive_take_text(characters, after_name_length, name_length) {
      Some(value) => value
      None => return ModuleArchiveFailed("truncated module name")
    }
    if !safe_module_specifier(name) {
      return ModuleArchiveFailed("unsafe module name '\{name}'")
    }
    for existing in modules {
      if existing.name == name {
        return ModuleArchiveFailed("duplicate module name '\{name}'")
      }
    }
    let (source_length, after_source_length) = match
      archive_read_number(characters, after_name) {
      Some(value) => value
      None => return ModuleArchiveFailed("invalid source length for '\{name}'")
    }
    let (source, after_source) = match
      archive_take_text(characters, after_source_length, source_length) {
      Some(value) => value
      None => return ModuleArchiveFailed("truncated source for '\{name}'")
    }
    let (checksum_length, after_checksum_length) = match
      archive_read_number(characters, after_source) {
      Some(value) => value
      None =>
        return ModuleArchiveFailed("invalid checksum length for '\{name}'")
    }
    let (checksum, after_checksum) = match
      archive_take_text(characters, after_checksum_length, checksum_length) {
      Some(value) => value
      None => return ModuleArchiveFailed("truncated checksum for '\{name}'")
    }
    if module_checksum(source) != checksum {
      return ModuleArchiveFailed("checksum mismatch for '\{name}'")
    }
    modules.push({ name, source, checksum, })
    cursor = after_checksum
  }
  if cursor != characters.length() {
    ModuleArchiveFailed("unexpected trailing module archive data")
  } else {
    ModuleArchiveDecoded(modules)
  }
}