///| Read operations for Fs (RepoFileSystem trait implementation)

///|
fn is_tree_mode(mode : String) -> Bool {
  mode == "040000" || mode == "40000"
}

///|
pub fn Fs::read_file(
  self : Fs,
  backing_fs : &@bit.RepoFileSystem,
  path : String,
) -> Bytes raise @bit.GitError {
  let norm = normalize_path(path)
  if self.working.deleted.contains(norm) {
    raise @bit.GitError::IoError("File not found: \{path}")
  }
  if self.working.files.get(norm) is Some(data) {
    return data
  }
  if self.cache.get_blob(norm) is Some(data) {
    return data
  }
  let obj_id = self.resolve_path(backing_fs, norm)
  guard obj_id is Some(id) else {
    raise @bit.GitError::IoError("File not found: \{path}")
  }
  let db = self.get_db(backing_fs)
  let obj = db.get(backing_fs, id)
  guard obj is Some(o) else {
    raise @bit.GitError::IoError("Blob not found: \{id}")
  }
  if o.obj_type != @bit.ObjectType::Blob {
    raise @bit.GitError::InvalidObject("Not a file: \{path}")
  }
  self.cache.set_blob(norm, o.data)
  o.data
}

///|
pub fn Fs::readdir(
  self : Fs,
  backing_fs : &@bit.RepoFileSystem,
  path : String,
) -> Array[String] raise @bit.GitError {
  let norm = normalize_path(path)
  let entries : Map[String, Bool] = Map([])
  // Collect entries from working files/dirs under `norm`.
  for path in self.working.files.keys() {
    match readdir_relative_name(path, norm) {
      Some(name) => entries[name] = true
      None => ()
    }
  }
  for path in self.working.dirs.keys() {
    match readdir_relative_name(path, norm) {
      Some(name) => entries[name] = true
      None => ()
    }
  }
  self.readdir_from_layers(backing_fs, norm, entries)
  self.readdir_from_tree(backing_fs, norm, self.base_tree, entries)
  for del_path in self.working.deleted.keys() {
    if parent_dir(del_path) == norm ||
      (norm.length() == 0 && !del_path.contains("/")) {
      entries.remove(basename(del_path))
    }
  }
  let out = entries.keys().to_array()
  out.sort()
  out
}

///|
/// If `path` is `norm/X/...`, returns the first segment `X`; otherwise None.
/// At the root (norm=""), the first segment of `path` itself is returned.
fn readdir_relative_name(path : String, norm : String) -> String? {
  let rel = if norm.length() == 0 {
    path
  } else if path.has_prefix(norm + "/") {
    String::unsafe_substring(path, start=norm.length() + 1, end=path.length())
  } else {
    return None
  }
  let (name, _) = split_first(rel)
  Some(name)
}

///|
fn Fs::readdir_from_layers(
  self : Fs,
  backing_fs : &@bit.RepoFileSystem,
  norm : String,
  entries : Map[String, Bool],
) -> Unit {
  for layer in self.layers {
    self.readdir_from_tree(backing_fs, norm, layer.tree_id, entries) catch {
      _ => ()
    }
  }
}

///|
fn Fs::readdir_from_tree(
  self : Fs,
  backing_fs : &@bit.RepoFileSystem,
  path : String,
  tree_id : @bit.ObjectId,
  entries : Map[String, Bool],
) -> Unit raise @bit.GitError {
  if tree_id == @bit.ObjectId::zero() {
    return
  }
  let tree_entries = self.get_tree_entries(backing_fs, tree_id)
  if path.length() == 0 {
    for entry in tree_entries {
      entries[entry.name] = true
    }
    return
  }
  let parts = split_path(path)
  let mut current_entries = tree_entries
  for i = 0; i < parts.length(); i = i + 1 {
    let part = parts[i]
    let mut found = false
    for entry in current_entries {
      if entry.name == part {
        if is_tree_mode(entry.mode) {
          current_entries = self.get_tree_entries(backing_fs, entry.id)
          found = true
          break
        } else {
          return
        }
      }
    }
    if !found {
      return
    }
  }
  for entry in current_entries {
    entries[entry.name] = true
  }
}

///|
pub fn Fs::is_dir(
  self : Fs,
  backing_fs : &@bit.RepoFileSystem,
  path : String,
) -> Bool {
  let norm = normalize_path(path)
  if norm.length() == 0 {
    return true
  }
  if self.working.deleted.contains(norm) {
    return false
  }
  if self.working.dirs.contains(norm) {
    return true
  }
  for file_path in self.working.files.keys() {
    if file_path.has_prefix(norm + "/") {
      return true
    }
  }
  self.is_dir_in_tree(backing_fs, norm)
}

///|
fn Fs::is_dir_in_tree(
  self : Fs,
  backing_fs : &@bit.RepoFileSystem,
  path : String,
) -> Bool {
  for layer in self.layers {
    if self.check_dir_in_tree(backing_fs, layer.tree_id, path) {
      return true
    }
  }
  self.check_dir_in_tree(backing_fs, self.base_tree, path)
}

///|
fn Fs::check_dir_in_tree(
  self : Fs,
  backing_fs : &@bit.RepoFileSystem,
  tree_id : @bit.ObjectId,
  path : String,
) -> Bool {
  if tree_id == @bit.ObjectId::zero() {
    return false
  }
  let parts = split_path(path)
  if parts.length() == 0 {
    return true
  }
  let mut current_tree = tree_id
  for i = 0; i < parts.length(); i = i + 1 {
    let part = parts[i]
    let entries = self.get_tree_entries(backing_fs, current_tree) catch {
      _ => return false
    }
    let mut found = false
    for entry in entries {
      if entry.name == part {
        if is_tree_mode(entry.mode) {
          current_tree = entry.id
          found = true
          break
        } else {
          return false
        }
      }
    }
    if !found {
      return false
    }
  }
  true
}

///|
pub fn Fs::is_file(
  self : Fs,
  backing_fs : &@bit.RepoFileSystem,
  path : String,
) -> Bool {
  let norm = normalize_path(path)
  if self.working.deleted.contains(norm) {
    return false
  }
  if self.working.files.contains(norm) {
    return true
  }
  self.resolve_path(backing_fs, norm) is Some(_)
}

///|
/// Check if file exists in base tree (ignoring working layer)
pub fn Fs::exists_in_base(
  self : Fs,
  backing_fs : &@bit.RepoFileSystem,
  path : String,
) -> Bool {
  let norm = normalize_path(path)
  // Check layers and base_tree directly
  for layer in self.layers {
    if self.find_in_tree(backing_fs, layer.tree_id, norm) is Some(_) {
      return true
    }
  }
  self.find_in_tree(backing_fs, self.base_tree, norm) is Some(_)
}

///|
fn Fs::resolve_path(
  self : Fs,
  backing_fs : &@bit.RepoFileSystem,
  path : String,
) -> @bit.ObjectId? {
  if self.cache.get_path(path) is Some(cached) {
    return cached
  }
  for layer in self.layers {
    if self.find_in_tree(backing_fs, layer.tree_id, path) is Some(id) {
      self.cache.set_path(path, Some(id))
      return Some(id)
    }
  }
  let result = self.find_in_tree(backing_fs, self.base_tree, path)
  self.cache.set_path(path, result)
  result
}

///|
fn Fs::find_in_tree(
  self : Fs,
  backing_fs : &@bit.RepoFileSystem,
  tree_id : @bit.ObjectId,
  path : String,
) -> @bit.ObjectId? {
  if tree_id == @bit.ObjectId::zero() {
    return None
  }
  let parts = split_path(path)
  if parts.length() == 0 {
    return None
  }
  let mut current_tree = tree_id
  let mut prefix = ""
  for i = 0; i < parts.length(); i = i + 1 {
    let part = parts[i]
    let entries = self.get_tree_entries(backing_fs, current_tree) catch {
      _ => return None
    }
    let cache_prefix = if prefix.length() == 0 { "" } else { prefix + "/" }
    if self.layers.length() == 0 {
      for entry in entries {
        let cached_path = cache_prefix + entry.name
        if self.cache.get_path(cached_path) is None {
          if is_tree_mode(entry.mode) {
            self.cache.set_path(cached_path, None)
          } else {
            self.cache.set_path(cached_path, Some(entry.id))
          }
        }
      }
    }
    let mut found = false
    for entry in entries {
      if entry.name == part {
        let full_path = cache_prefix + part
        if i == parts.length() - 1 {
          if !(entry.mode |> is_tree_mode) {
            self.cache.set_path(full_path, Some(entry.id))
            return Some(entry.id)
          } else {
            self.cache.set_path(full_path, None)
            return None
          }
        } else if is_tree_mode(entry.mode) {
          current_tree = entry.id
          prefix = full_path
          found = true
          break
        } else {
          self.cache.set_path(full_path, None)
          return None
        }
      }
    }
    if !found {
      return None
    }
  }
  None
}

///|
fn Fs::get_tree_entries(
  self : Fs,
  backing_fs : &@bit.RepoFileSystem,
  tree_id : @bit.ObjectId,
) -> Array[@bit.TreeEntry] raise @bit.GitError {
  if self.cache.get_tree(tree_id) is Some(entries) {
    return entries
  }
  let db = self.get_db(backing_fs)
  let obj = db.get(backing_fs, tree_id)
  guard obj is Some(o) else {
    raise @bit.GitError::InvalidObject("Tree not found: \{tree_id}")
  }
  if o.obj_type != @bit.ObjectType::Tree {
    raise @bit.GitError::InvalidObject("Not a tree: \{tree_id}")
  }
  let entries = @bit.parse_tree(o.data)
  self.cache.set_tree(tree_id, entries)
  entries
}

// ============================================================================
// Async read operations with on-demand fetch support
// ============================================================================

///|
/// Read file content, fetching from remote if not available locally (async).
/// This is the primary method for partial clone repositories.
pub async fn Fs::read_file_async(
  self : Fs,
  read_fs : &@bit.RepoFileSystem,
  write_fs : &@bit.FileSystem,
  path : String,
) -> Bytes raise @bit.GitError {
  let norm = normalize_path(path)
  // Check working layer first
  if self.working.deleted.contains(norm) {
    raise @bit.GitError::IoError("File not found: \{path}")
  }
  if self.working.files.get(norm) is Some(data) {
    return data
  }
  // Check cache
  if self.cache.get_blob(norm) is Some(data) {
    return data
  }
  // Resolve path to object ID
  let obj_id = self.resolve_path(read_fs, norm)
  guard obj_id is Some(id) else {
    raise @bit.GitError::IoError("File not found: \{path}")
  }
  // Try to get object, using promisor if available
  let obj = self.get_object_async(read_fs, write_fs, id)
  guard obj is Some(o) else {
    raise @bit.GitError::IoError("Blob not found: \{id}")
  }
  if o.obj_type != @bit.ObjectType::Blob {
    raise @bit.GitError::InvalidObject("Not a file: \{path}")
  }
  self.cache.set_blob(norm, o.data)
  o.data
}

///|
/// Get an object, fetching from promisor remote if not available locally (async).
async fn Fs::get_object_async(
  self : Fs,
  read_fs : &@bit.RepoFileSystem,
  write_fs : &@bit.FileSystem,
  id : @bit.ObjectId,
) -> @bit.PackObject? raise @bit.GitError {
  // First try local via PromisorDb
  let promisor_db = self.get_promisor_db(read_fs)
  match promisor_db {
    Some(pdb) => {
      // Try local first
      let local_obj = pdb.get(read_fs, id)
      if local_obj is Some(_) {
        return local_obj
      }
      // If promisor remote available, fetch
      if pdb.has_promisor() {
        return pdb.fetch_one(write_fs, id)
      }
      None
    }
    None => {
      // Promisor disabled, use regular ObjectDb
      let db = self.get_db(read_fs)
      db.get(read_fs, id)
    }
  }
}

///|
/// Prefetch multiple objects from promisor remote (async).
/// Useful for batch fetching before accessing many files.
pub async fn Fs::prefetch(
  self : Fs,
  read_fs : &@bit.RepoFileSystem,
  write_fs : &@bit.FileSystem,
  paths : Array[String],
) -> Int raise @bit.GitError {
  let promisor_db = self.get_promisor_db(read_fs)
  guard promisor_db is Some(pdb) else { return 0 }
  if !pdb.has_promisor() {
    return 0
  }
  // Collect object IDs that need fetching
  let ids : Array[@bit.ObjectId] = []
  for path in paths {
    let norm = normalize_path(path)
    // Skip working layer files
    if self.working.files.contains(norm) {
      continue
    }
    if self.working.deleted.contains(norm) {
      continue
    }
    // Resolve path to object ID
    match self.resolve_path(read_fs, norm) {
      Some(id) =>
        // Check if not already cached
        if self.cache.get_blob(norm) is None {
          // Check if not locally available
          if !pdb.has_local(read_fs, id) {
            ids.push(id)
          }
        }
      None => ()
    }
  }
  if ids.length() == 0 {
    return 0
  }
  // Batch fetch
  let fetched = pdb.fetch_missing(write_fs, ids)
  fetched.length()
}

///|
/// Check if a file requires fetching from remote
pub fn Fs::needs_fetch(
  self : Fs,
  read_fs : &@bit.RepoFileSystem,
  path : String,
) -> Bool raise @bit.GitError {
  let norm = normalize_path(path)
  // Check working layer
  if self.working.files.contains(norm) || self.working.deleted.contains(norm) {
    return false
  }
  // Check cache
  if self.cache.get_blob(norm) is Some(_) {
    return false
  }
  // Resolve to object ID
  let obj_id = self.resolve_path(read_fs, norm)
  guard obj_id is Some(id) else { return false }
  // Check if object exists locally
  let promisor_db = self.get_promisor_db(read_fs)
  match promisor_db {
    Some(pdb) => !pdb.has_local(read_fs, id)
    None => {
      let db = self.get_db(read_fs)
      db.get(read_fs, id) is None
    }
  }
}

///|
/// Simple glob pattern matching.
/// Supports: * (any chars except /), ** (any path), ? (single char)
fn glob_match(pattern : String, path : String) -> Bool {
  glob_match_impl(pattern, 0, path, 0)
}

///|
fn glob_match_impl(pattern : String, pi : Int, path : String, si : Int) -> Bool {
  let plen = pattern.length()
  let slen = path.length()
  // Both exhausted
  if pi >= plen && si >= slen {
    return true
  }
  // Pattern exhausted but path remains
  if pi >= plen {
    return false
  }
  // Check for **
  if pi + 1 < plen && pattern[pi] == '*' && pattern[pi + 1] == '*' {
    // Skip trailing / after **
    let next_pi = if pi + 2 < plen && pattern[pi + 2] == '/' {
      pi + 3
    } else {
      pi + 2
    }
    // ** matches zero or more path segments
    for i in si..<=slen {
      if glob_match_impl(pattern, next_pi, path, i) {
        return true
      }
    }
    return false
  }
  // Path exhausted but pattern remains
  if si >= slen {
    // Only valid if remaining pattern is all * or **
    if pattern[pi] == '*' {
      return glob_match_impl(pattern, pi + 1, path, si)
    }
    return false
  }
  // Single * - match any chars except /
  if pattern[pi] == '*' {
    // Try matching zero chars
    if glob_match_impl(pattern, pi + 1, path, si) {
      return true
    }
    // Try matching one more char (not /)
    if path[si] != '/' {
      return glob_match_impl(pattern, pi, path, si + 1)
    }
    return false
  }
  // ? matches any single char except /
  if pattern[pi] == '?' {
    if path[si] != '/' {
      return glob_match_impl(pattern, pi + 1, path, si + 1)
    }
    return false
  }
  // Literal match
  if pattern[pi] == path[si] {
    return glob_match_impl(pattern, pi + 1, path, si + 1)
  }
  false
}

///|
/// Prefetch files matching a glob pattern.
/// Example patterns: "*.rs", "src/**/*.mbt", "README.*"
pub async fn Fs::prefetch_glob(
  self : Fs,
  read_fs : &@bit.RepoFileSystem,
  write_fs : &@bit.FileSystem,
  pattern : String,
) -> Int raise @bit.GitError {
  // Collect all files matching the pattern
  let matching : Array[String] = []
  self.collect_matching_files(read_fs, "", pattern, matching)
  if matching.length() == 0 {
    return 0
  }
  // Use existing prefetch
  self.prefetch(read_fs, write_fs, matching)
}

///|
/// Recursively collect files matching a glob pattern
fn Fs::collect_matching_files(
  self : Fs,
  read_fs : &@bit.RepoFileSystem,
  dir : String,
  pattern : String,
  result : Array[String],
) -> Unit raise @bit.GitError {
  let entries = self.readdir(read_fs, dir)
  for entry in entries {
    let full_path = if dir == "" { entry } else { dir + "/" + entry }
    if self.is_file(read_fs, full_path) {
      if glob_match(pattern, full_path) {
        result.push(full_path)
      }
    } else if self.is_dir(read_fs, full_path) {
      // Recurse into subdirectory
      self.collect_matching_files(read_fs, full_path, pattern, result)
    }
  }
}

///|
/// Collect files in breadth-first order up to a limit.
/// Useful for progressive preloading.
pub fn Fs::collect_files_bfs(
  self : Fs,
  read_fs : &@bit.RepoFileSystem,
  limit : Int,
) -> Array[String] raise @bit.GitError {
  let result : Array[String] = []
  let queue : Array[String] = [""]
  let mut head = 0
  while head < queue.length() && result.length() < limit {
    let dir = queue[head]
    head += 1
    let entries = self.readdir(read_fs, dir)
    for entry in entries {
      if result.length() >= limit {
        break
      }
      let full_path = if dir == "" { entry } else { dir + "/" + entry }
      if self.is_file(read_fs, full_path) {
        result.push(full_path)
      } else if self.is_dir(read_fs, full_path) {
        // Add directory to queue for later processing
        queue.push(full_path)
      }
    }
  }
  result
}

///|
/// Prefetch files in breadth-first order.
/// Prioritizes shallow files (root level first, then subdirs).
pub async fn Fs::prefetch_bfs(
  self : Fs,
  read_fs : &@bit.RepoFileSystem,
  write_fs : &@bit.FileSystem,
  limit : Int,
) -> Int raise @bit.GitError {
  let files = self.collect_files_bfs(read_fs, limit)
  self.prefetch(read_fs, write_fs, files)
}

///|
/// Get list of files that need fetching, in BFS order.
/// Useful for showing progress or prioritizing fetches.
pub fn Fs::get_pending_fetches(
  self : Fs,
  read_fs : &@bit.RepoFileSystem,
  limit : Int,
) -> Array[String] raise @bit.GitError {
  let files = self.collect_files_bfs(read_fs, limit)
  let pending : Array[String] = []
  for file in files {
    if self.needs_fetch(read_fs, file) {
      pending.push(file)
    }
  }
  pending
}