///| Git cherry-pick implementation

///|
pub enum CherryPickStatus {
  Success
  Conflict
}

///|
pub struct CherryPickResult {
  status : CherryPickStatus
  commit_id : @bit.ObjectId?
  conflicts : Array[String]
}

///|
/// Cherry-pick a single commit onto current HEAD.
pub fn cherry_pick(
  fs : &@bit.FileSystem,
  rfs : &@bit.RepoFileSystem,
  root : String,
  commit_id : @bit.ObjectId,
  author : String,
  timestamp : Int64,
  allow_empty? : Bool = false,
  no_commit? : Bool = false,
  message_suffix? : String = "",
  signoff_committer? : String = "",
) -> CherryPickResult raise @bit.GitError {
  let git_marker = join_path(root, ".git")
  let git_dir = if rfs.is_file(git_marker) {
    resolve_gitdir(rfs, git_marker)
  } else {
    git_marker
  }
  let db = ObjectDb::load(rfs, git_dir)
  // Read the commit to cherry-pick
  let commit_obj = db.get(rfs, commit_id)
  guard commit_obj is Some(obj) else {
    raise @bit.GitError::InvalidObject("Commit not found")
  }
  if obj.obj_type != @bit.ObjectType::Commit {
    raise @bit.GitError::InvalidObject("Not a commit")
  }
  let commit_info = @bit.parse_commit(obj.data)
  let raw_message = cherry_pick_extract_message(obj.data, commit_id)
  let msg_with_suffix = if message_suffix.length() > 0 {
    let trimmed = cherry_pick_trim_trailing_newlines(raw_message)
    trimmed + "\n" + message_suffix + "\n"
  } else {
    raw_message
  }
  let message = if signoff_committer.length() > 0 {
    commit_apply_signoff(msg_with_suffix, committer=signoff_committer)
  } else {
    msg_with_suffix
  }
  let head = resolve_head_commit(rfs, git_dir)
  if head is None {
    if commit_info.parents.length() == 0 {
      let result_files = collect_tree_files(db, rfs, commit_info.tree)
      return cherry_pick_write_result_commit(
        db,
        fs,
        rfs,
        root,
        git_dir,
        result_files,
        [],
        author,
        timestamp,
        message,
      )
    }
    ignore(allow_empty)
    raise @bit.GitError::InvalidObject("HEAD not found")
  }
  guard head is Some(head_id) else {
    raise @bit.GitError::InvalidObject("HEAD not found")
  }
  // Get the parent of the cherry-picked commit
  if commit_info.parents.length() == 0 {
    let result_files = collect_tree_files(db, rfs, commit_info.tree)
    return cherry_pick_write_result_commit(
      db,
      fs,
      rfs,
      root,
      git_dir,
      result_files,
      [head_id],
      author,
      timestamp,
      message,
    )
  }
  let parent_id = commit_info.parents[0]
  // Get file trees
  let head_files = collect_tree_files_from_commit(db, rfs, head_id)
  let commit_files = collect_tree_files(db, rfs, commit_info.tree)
  let parent_files = collect_tree_files_from_commit(db, rfs, parent_id)
  // Compute the patch (diff between parent and commit)
  let added_files : Map[String, TreeFileEntry] = Map([])
  let modified_files : Map[String, TreeFileEntry] = Map([])
  let deleted_files : Array[String] = []
  // Find added and modified files
  for entry in commit_files.iter() {
    let (path, file_entry) = entry
    match parent_files.get(path) {
      None => added_files[path] = file_entry
      Some(parent_entry) =>
        if parent_entry.id != file_entry.id ||
          parent_entry.mode != file_entry.mode {
          modified_files[path] = file_entry
        }
    }
  }
  // Find deleted files
  for entry in parent_files.iter() {
    let (path, _) = entry
    if !commit_files.contains(path) {
      deleted_files.push(path)
    }
  }
  // Apply the patch to head_files
  let result_files : Map[String, TreeFileEntry] = Map([])
  let conflicts : Array[String] = []
  // Copy head files
  for entry in head_files.iter() {
    let (path, file_entry) = entry
    result_files[path] = file_entry
  }
  // Apply added files (check for conflicts)
  for entry in added_files.iter() {
    let (path, file_entry) = entry
    if result_files.contains(path) {
      // File already exists - conflict
      conflicts.push(path)
    } else {
      result_files[path] = file_entry
    }
  }
  // Apply modified files (check for conflicts)
  for entry in modified_files.iter() {
    let (path, file_entry) = entry
    match result_files.get(path) {
      None =>
        // File doesn't exist in head - add it
        result_files[path] = file_entry
      Some(head_entry) =>
        // Check if head has the same content and mode as parent
        match parent_files.get(path) {
          Some(parent_entry) if parent_entry.id == head_entry.id &&
            parent_entry.mode == head_entry.mode =>
            // No conflict - head hasn't changed the file
            result_files[path] = file_entry
          _ => {
            // Conflict - both sides modified
            conflicts.push(path)
            // Keep the cherry-picked version for now (simple approach)
            result_files[path] = file_entry
          }
        }
    }
  }
  // Apply deletions (check for conflicts)
  for path in deleted_files {
    match result_files.get(path) {
      Some(head_entry) =>
        // Check if head has the same content and mode as parent
        match parent_files.get(path) {
          Some(parent_entry) if parent_entry.id == head_entry.id &&
            parent_entry.mode == head_entry.mode => {
            // No conflict - safe to delete
            let _ = result_files.remove(path)
          }
          _ => {
            // Conflict - head modified a file that was deleted
            conflicts.push(path)
            let _ = result_files.remove(path)
          }
        }
      None => () // Already deleted
    }
  }
  if conflicts.length() > 0 {
    // Write the result to worktree but don't commit
    write_worktree_from_files(db, fs, rfs, root, git_dir, result_files)
    let result_entries = tree_files_to_index(db, rfs, result_files)
    write_index_entries(fs, git_dir, result_entries)
    return { status: CherryPickStatus::Conflict, commit_id: None, conflicts }
  }
  if no_commit {
    // Apply changes to worktree and index but skip creating a commit
    write_worktree_from_files(db, fs, rfs, root, git_dir, result_files)
    let result_entries = tree_files_to_index(db, rfs, result_files)
    write_index_entries(fs, git_dir, result_entries)
    return { status: CherryPickStatus::Success, commit_id: None, conflicts: [] }
  }
  cherry_pick_write_result_commit(
    db,
    fs,
    rfs,
    root,
    git_dir,
    result_files,
    [head_id],
    author,
    timestamp,
    message,
  )
}

///|
fn cherry_pick_extract_message(
  data : Bytes,
  commit_id : @bit.ObjectId,
) -> String {
  let commit_text = @utf8.decode_lossy(data[:])
  let msg_start = commit_text.find("\n\n")
  match msg_start {
    Some(idx) =>
      String::unsafe_substring(
        commit_text,
        start=idx + 2,
        end=commit_text.length(),
      )
    None => "Cherry-picked from \{commit_id.to_hex()}"
  }
}

///|
fn cherry_pick_write_result_commit(
  db : ObjectDb,
  fs : &@bit.FileSystem,
  rfs : &@bit.RepoFileSystem,
  root : String,
  git_dir : String,
  result_files : Map[String, TreeFileEntry],
  parents : Array[@bit.ObjectId],
  author : String,
  timestamp : Int64,
  message : String,
) -> CherryPickResult raise @bit.GitError {
  let object_git_dir = resolve_common_git_dir(rfs, git_dir)
  let result_entries = tree_files_to_index(db, rfs, result_files)
  write_index_entries(fs, git_dir, result_entries)
  let result_tree = write_tree_from_index(fs, rfs, git_dir, result_entries)
  let new_commit = @bit.Commit::new(
    result_tree, parents, author, timestamp, "+0000", author, timestamp, "+0000",
    message,
  )
  let (new_id, new_data) = @bit.create_commit(new_commit)
  write_object_bytes(fs, object_git_dir, new_id, new_data)
  update_head_ref(fs, rfs, git_dir, new_id)
  write_worktree_from_files(db, fs, rfs, root, git_dir, result_files)
  { status: CherryPickStatus::Success, commit_id: Some(new_id), conflicts: [] }
}

///|
fn cherry_pick_trim_trailing_newlines(s : String) -> String {
  let chars = s.to_array()
  let mut end = chars.length()
  while end > 0 && (chars[end - 1] == '\n' || chars[end - 1] == '\r') {
    end -= 1
  }
  let result = StringBuilder::new()
  for i in 0..