///| Git fsck: connectivity check and object integrity verification (pure logic)

///|
pub struct FsckResult {
  errors : Int
  reachable : Map[String, Bool]
  missing : Map[String, Bool]
  root_commits : Array[String]
  tag_objects : Array[String]
}

///|
/// BFS connectivity walk from multiple tips.
/// Returns reachable set, missing set, root commits, tag objects, and error count.
/// This is the pure core of fsck — no filesystem access, no stderr output.
pub fn fsck_connectivity_check(
  db : ObjectDb,
  fs : &@bit.RepoFileSystem,
  tips : Array[@bit.ObjectId],
  collect_extra? : Bool = false,
) -> FsckResult {
  let reachable : Map[String, Bool] = Map([])
  let missing : Map[String, Bool] = Map([])
  let root_commits : Array[String] = []
  let tag_objects : Array[String] = []
  let mut errors = 0
  let queue : Array[@bit.ObjectId] = []
  for tip in tips {
    queue.push(tip)
  }
  let mut cursor = 0
  while cursor < queue.length() {
    let current = queue[cursor]
    cursor += 1
    let hex = current.to_hex()
    if reachable.contains(hex) {
      continue
    }
    reachable[hex] = true
    let obj = db.get(fs, current) catch {
      _ => {
        if !missing.contains(hex) {
          missing[hex] = true
          errors += 1
        }
        continue
      }
    }
    match obj {
      None =>
        if !missing.contains(hex) {
          missing[hex] = true
          errors += 1
        }
      Some(o) =>
        match o.obj_type {
          @bit.ObjectType::Commit => {
            let info = @bit.parse_commit(o.data) catch {
              _ => {
                errors += 1
                continue
              }
            }
            if collect_extra && info.parents.length() == 0 {
              root_commits.push(hex)
            }
            let tree_hex = info.tree.to_hex()
            if !reachable.contains(tree_hex) {
              queue.push(info.tree)
            }
            for parent in info.parents {
              let phex = parent.to_hex()
              if !reachable.contains(phex) {
                queue.push(parent)
              }
            }
          }
          @bit.ObjectType::Tree => {
            let entries = @bit.parse_tree(o.data) catch {
              _ => {
                errors += 1
                continue
              }
            }
            for entry in entries {
              let ehex = entry.id.to_hex()
              if !reachable.contains(ehex) {
                queue.push(entry.id)
              }
            }
          }
          @bit.ObjectType::Tag => {
            if collect_extra {
              tag_objects.push(hex)
            }
            fsck_walk_tag(o.data, reachable, queue)
          }
          @bit.ObjectType::Blob => ()
        }
    }
  }
  { errors, reachable, missing, root_commits, tag_objects }
}

///|
fn fsck_walk_tag(
  data : Bytes,
  reachable : Map[String, Bool],
  queue : Array[@bit.ObjectId],
) -> Unit {
  let text = @utf8.decode_lossy(data[:])
  for line_view in text.split("\n") {
    let line = line_view.to_owned()
    if line.length() == 0 {
      break
    }
    if line.has_prefix("object ") {
      let target_hex = String::unsafe_substring(
        line,
        start=7,
        end=line.length(),
      )
      let target = @bit.ObjectId::from_hex(target_hex) catch { _ => return }
      if !reachable.contains(target.to_hex()) {
        queue.push(target)
      }
      break
    }
  }
}

///|
/// Enumerate all object hex IDs from ObjectDb (loose + packed).
pub fn fsck_enumerate_objects(db : ObjectDb) -> Array[String] {
  let all : Array[String] = []
  let seen : Map[String, Bool] = Map([])
  for hex, _ in db.loose_paths {
    if !seen.contains(hex) {
      seen[hex] = true
      all.push(hex)
    }
  }
  for pack in db.packs {
    for id in pack.hex_ids() {
      if !seen.contains(id) {
        seen[id] = true
        all.push(id)
      }
    }
  }
  all
}

///|
/// Get the type name of an object by hex ID.
pub fn fsck_object_type(
  db : ObjectDb,
  fs : &@bit.RepoFileSystem,
  hex : String,
) -> String {
  let id = @bit.ObjectId::from_hex(hex) catch { _ => return "blob" }
  let obj = db.get(fs, id) catch { _ => return "blob" }
  match obj {
    None => "blob"
    Some(o) => o.obj_type.to_string()
  }
}

///|
/// Verify a single loose object's hash. Returns true if valid.
pub fn fsck_verify_loose_hash(raw : Bytes, expected_hex : String) -> Bool {
  let computed = @bit.sha1(raw)
  computed.to_hex() == expected_hex
}