///|
fn sorted_records(files : Array[FileRecord]) -> Array[FileRecord] {
  let sorted = files.copy()
  sorted.sort_by((a, b) => a.path.compare(b.path))
  sorted
}

///|
fn checked_algorithms(algorithms : Array[Algorithm]) -> Unit raise FormatError {
  guard algorithms.length() > 0 else {
    raise Invalid("at least one algorithm required")
  }
  for i in 0.. Array[Entry] raise FormatError {
  let entries : Array[Entry] = []
  let seen : Map[String, Bool] = Map([])
  for file in sorted_records(files) {
    check_path(file.path)
    guard file.size >= 0 else { raise Invalid("negative file size") }
    guard !seen.contains(file.path) else {
      raise Invalid("duplicate file record")
    }
    seen[file.path] = true
    guard file.digests.get(alg.name()) is Some(checksum) &&
      valid_checksum(checksum, alg) else {
      raise Invalid("missing or malformed digest: " + file.path)
    }
    entries.push({
      path: file.path,
      checksum: checksum[:].to_lower().to_owned(),
    })
  }
  entries
}

///|
pub fn payload_oxum(files : Array[FileRecord]) -> String raise FormatError {
  let mut bytes = 0L
  for file in files {
    guard file.size >= 0 && bytes <= 9223372036854775807L - file.size else {
      raise Invalid("payload byte count overflows Int64")
    }
    bytes = bytes + file.size
  }
  "\{bytes}.\{files.length()}"
}

///|
/// Returns initial tag text; callers then hash these bytes to build tag manifests.
pub fn create_tags(
  payload : Array[FileRecord],
  metadata? : Array[Metadata] = [],
  algorithms? : Array[Algorithm] = [Sha512],
) -> Map[String, String] raise FormatError {
  checked_algorithms(algorithms)
  for file in payload {
    guard file.path.has_prefix("data/") else {
      raise Invalid("payload paths must start with data/")
    }
  }
  let tags : Map[String, String] = Map([])
  tags["bagit.txt"] = "BagIt-Version: 1.0\nTag-File-Character-Encoding: UTF-8\n"
  let fields = metadata.copy()
  guard !fields.any(f => f.label[:].to_lower().to_owned() == "payload-oxum") else {
    raise Invalid("Payload-Oxum is computed, not supplied by the caller")
  }
  fields.push({ label: "Payload-Oxum", value: payload_oxum(payload), })
  tags["bag-info.txt"] = render_metadata(fields)
  for alg in algorithms {
    tags["manifest-" + alg.name() + ".txt"] = render_manifest(
      record_entries(payload, alg),
    )
  }
  tags
}

///|
pub fn create_tag_manifests(
  tags : Array[FileRecord],
  algorithms? : Array[Algorithm] = [Sha512],
) -> Map[String, String] raise FormatError {
  checked_algorithms(algorithms)
  for file in tags {
    guard !file.path.has_prefix("data/") && file.path != "data" else {
      raise Invalid("tag record is payload")
    }
    guard !(file.path.has_prefix("tagmanifest-") &&
      file.path.has_suffix(".txt") &&
      !file.path.contains("/")) else {
      raise Invalid("tag manifests cannot hash themselves")
    }
  }
  guard tags.any(f => {
    f.path.has_prefix("manifest-") &&
    f.path.has_suffix(".txt") &&
    !f.path.contains("/")
  }) else {
    raise Invalid("tag manifests must include payload manifests")
  }
  let result : Map[String, String] = Map([])
  for alg in algorithms {
    result["tagmanifest-" + alg.name() + ".txt"] = render_manifest(
      record_entries(tags, alg),
    )
  }
  result
}