///|
/// A deterministic selection of manifest entries for incremental verification.
pub(all) struct ManifestSelection {
  files : Array[FileSnapshot]
  total_bytes : Int64
  source_manifest_root : String
}

///|
/// Select files below a repository-relative directory.
pub fn Manifest::select_directory(
  self : Manifest,
  directory : String,
) -> ManifestSelection {
  let selected = self.files_under(directory)
  selection_from_files(selected, self.merkle_root)
}

///|
/// Select files whose paths end in one of the supplied suffixes.
pub fn Manifest::select_suffixes(
  self : Manifest,
  suffixes : Array[String],
) -> ManifestSelection {
  let selected : Array[FileSnapshot] = []
  for file in self.files {
    if has_any_suffix(file.path, suffixes) {
      selected.push(file)
    }
  }
  selection_from_files(selected, self.merkle_root)
}

///|
/// Select a bounded batch for parallel or resumable verification.
pub fn Manifest::select_batch(
  self : Manifest,
  start : Int,
  limit : Int,
) -> Result[ManifestSelection, String] {
  if start < 0 || limit < 0 {
    return Err("Selection range cannot be negative")
  }
  if start > self.files.length() {
    return Err("Selection start is out of bounds")
  }
  let end = if start + limit > self.files.length() {
    self.files.length()
  } else {
    start + limit
  }
  let selected : Array[FileSnapshot] = []
  for i = start; i < end; i = i + 1 {
    selected.push(self.files[i])
  }
  Ok(selection_from_files(selected, self.merkle_root))
}

///|
/// Return true if a selection is empty.
pub fn ManifestSelection::is_empty(self : ManifestSelection) -> Bool {
  self.files.length() == 0
}

///|
/// Return the number of selected files.
pub fn ManifestSelection::file_count(self : ManifestSelection) -> Int {
  self.files.length()
}

///|
/// Return selected paths in stable manifest order.
pub fn ManifestSelection::paths(self : ManifestSelection) -> Array[String] {
  let paths : Array[String] = []
  for file in self.files {
    paths.push(file.path)
  }
  paths
}

///|
/// Compute a Merkle root over selected canonical file snapshots.
pub fn ManifestSelection::merkle_root(self : ManifestSelection) -> String {
  let leaves : Array[Bytes] = []
  for file in self.files {
    leaves.push(file.canonical_bytes())
  }
  @merkle.MerkleTree::new(leaves).root_hex()
}

///|
/// Verify that the selection still corresponds to the source manifest root.
pub fn ManifestSelection::source_is(
  self : ManifestSelection,
  manifest : Manifest,
) -> Bool {
  self.source_manifest_root == manifest.merkle_root
}

///|
/// Return a stable summary suitable for an incremental build log.
pub fn ManifestSelection::to_text(self : ManifestSelection) -> String {
  "files=" +
  self.file_count().to_string() +
  " bytes=" +
  self.total_bytes.to_string() +
  " root=" +
  self.merkle_root() +
  " source_root=" +
  self.source_manifest_root
}

///|
/// Find selected files by their exact SHA-256 digest.
pub fn ManifestSelection::with_sha256(
  self : ManifestSelection,
  digest : String,
) -> Array[FileSnapshot] {
  let result : Array[FileSnapshot] = []
  for file in self.files {
    if file.hash_sha256 == digest {
      result.push(file)
    }
  }
  result
}

///|
/// Return the largest selected file, if any.
pub fn ManifestSelection::largest(self : ManifestSelection) -> FileSnapshot? {
  if self.files.length() == 0 {
    return None
  }
  let mut largest = self.files[0]
  for i = 1; i < self.files.length(); i = i + 1 {
    if self.files[i].size > largest.size {
      largest = self.files[i]
    }
  }
  Some(largest)
}

///|
/// Return true when all selected snapshots have canonical digest records.
pub fn ManifestSelection::has_complete_digests(
  self : ManifestSelection,
) -> Bool {
  for file in self.files {
    if file.hash_sha256.length() != 64 {
      return false
    }
  }
  true
}

///|
/// Return a bounded sub-selection while preserving source-root provenance.
pub fn ManifestSelection::slice(
  self : ManifestSelection,
  start : Int,
  limit : Int,
) -> Result[ManifestSelection, String] {
  if start < 0 || limit < 0 || start > self.files.length() {
    return Err("Selection slice is out of bounds")
  }
  let end = if start + limit > self.files.length() {
    self.files.length()
  } else {
    start + limit
  }
  let selected : Array[FileSnapshot] = []
  for i = start; i < end; i = i + 1 {
    selected.push(self.files[i])
  }
  Ok(selection_from_files(selected, self.source_manifest_root))
}

///|
/// Select files at least as large as a configured threshold.
pub fn Manifest::select_large_files(
  self : Manifest,
  minimum_bytes : Int64,
) -> ManifestSelection {
  let selected : Array[FileSnapshot] = []
  for file in self.files {
    if file.size >= minimum_bytes {
      selected.push(file)
    }
  }
  selection_from_files(selected, self.merkle_root)
}

///|
/// Return whether a selection contains an exact path.
pub fn ManifestSelection::contains_path(
  self : ManifestSelection,
  path : String,
) -> Bool {
  for file in self.files {
    if file.path == path {
      return true
    }
  }
  false
}

///|
/// Return a selection containing only files with non-negative sizes.
pub fn ManifestSelection::valid_sizes(
  self : ManifestSelection,
) -> ManifestSelection {
  let selected : Array[FileSnapshot] = []
  for file in self.files {
    if file.size >= 0L {
      selected.push(file)
    }
  }
  selection_from_files(selected, self.source_manifest_root)
}

///|
/// Return the selected snapshot at an index without exposing the backing array.
pub fn ManifestSelection::at(
  self : ManifestSelection,
  index : Int,
) -> FileSnapshot? {
  if index < 0 || index >= self.files.length() {
    None
  } else {
    Some(self.files[index])
  }
}

///|
/// Return whether every selected path is unique.
pub fn ManifestSelection::has_unique_paths(self : ManifestSelection) -> Bool {
  for i = 0; i < self.files.length(); i = i + 1 {
    for j = i + 1; j < self.files.length(); j = j + 1 {
      if self.files[i].path == self.files[j].path {
        return false
      }
    }
  }
  true
}

///|
/// Return the inclusive total size range of selected files.
pub fn ManifestSelection::min_max_bytes(
  self : ManifestSelection,
) -> (Int64, Int64)? {
  if self.files.length() == 0 {
    return None
  }
  let mut minimum = self.files[0].size
  let mut maximum = self.files[0].size
  for i = 1; i < self.files.length(); i = i + 1 {
    if self.files[i].size < minimum {
      minimum = self.files[i].size
    }
    if self.files[i].size > maximum {
      maximum = self.files[i].size
    }
  }
  Some((minimum, maximum))
}

///|
/// Return whether the selection's byte total is zero.
pub fn ManifestSelection::is_zero_bytes(self : ManifestSelection) -> Bool {
  self.total_bytes == 0L
}

///|
fn selection_from_files(
  files : Array[FileSnapshot],
  source_manifest_root : String,
) -> ManifestSelection {
  let mut total_bytes : Int64 = 0L
  for file in files {
    total_bytes = total_bytes + file.size
  }
  { files, total_bytes, source_manifest_root }
}

///|
fn has_any_suffix(path : String, suffixes : Array[String]) -> Bool {
  for suffix in suffixes {
    if path.length() >= suffix.length() {
      let offset = path.length() - suffix.length()
      let mut matches = true
      for i = 0; i < suffix.length(); i = i + 1 {
        if path[offset + i] != suffix[i] {
          matches = false
          break
        }
      }
      if matches {
        return true
      }
    }
  }
  false
}