///| Hub notes sync operations (pure merge logic)

///|
pub struct PrSyncResult {
  success : Bool
  message : String
  local_commit : @bit.ObjectId?
  remote_commit : @bit.ObjectId?
}

///|
pub fn PrSyncResult::new(
  success : Bool,
  message : String,
  local_commit : @bit.ObjectId?,
  remote_commit : @bit.ObjectId?,
) -> PrSyncResult {
  { success, message, local_commit, remote_commit }
}

///|
pub fn PrSyncResult::success(self : PrSyncResult) -> Bool {
  self.success
}

///|
pub fn PrSyncResult::message(self : PrSyncResult) -> String {
  self.message
}

///|
pub fn PrSyncResult::local_commit(self : PrSyncResult) -> @bit.ObjectId? {
  self.local_commit
}

///|
pub fn PrSyncResult::remote_commit(self : PrSyncResult) -> @bit.ObjectId? {
  self.remote_commit
}

///|
pub fn write_object_bytes(
  fs : &@bit.FileSystem,
  git_dir : String,
  id : @bit.ObjectId,
  compressed : Bytes,
) -> Unit raise @bit.GitError {
  let hex = id.to_hex()
  let dir = join_path(
    git_dir,
    "objects/" + String::unsafe_substring(hex, start=0, end=2),
  )
  let path = join_path(dir, String::unsafe_substring(hex, start=2, end=40))
  fs.mkdir_p(dir)
  fs.write_file(path, compressed)
}

///|
fn join_path(a : String, b : String) -> String {
  if a.has_suffix("/") {
    a + b
  } else {
    a + "/" + b
  }
}

///|
fn get_commit_tree(
  objects : &@lib.ObjectStore,
  commit_id : @bit.ObjectId,
) -> @bit.ObjectId? raise @bit.GitError {
  let obj = objects.get(commit_id)
  guard obj is Some(commit_obj) else { return None }
  if commit_obj.obj_type != @bit.ObjectType::Commit {
    return None
  }
  let info = @bit.parse_commit(commit_obj.data)
  Some(info.tree)
}

///|
pub fn merge_notes_commits(
  prs : Hub,
  fs : &@bit.FileSystem,
  git_dir : String,
  objects : &@lib.ObjectStore,
  refs : &@lib.RefStore,
  local_id : @bit.ObjectId,
  remote_id : @bit.ObjectId,
  conflict_policy? : RecordMergePolicy = RecordMergePolicy::Lww,
) -> PrSyncResult raise @bit.GitError {
  // Fast-forward if possible
  if is_commit_ancestor_store(objects, local_id, remote_id) {
    refs.update(hub_notes_ref_name(), Some(remote_id))
    prs.store = HubStore::load(
      objects,
      refs,
      node_id=prs.store.node_id(),
      signing_key=prs.store.signing_key(),
      require_signed=prs.store.require_signed(),
    )
    return {
      success: true,
      message: "Fast-forward to remote",
      local_commit: Some(remote_id),
      remote_commit: Some(remote_id),
    }
  }
  if is_commit_ancestor_store(objects, remote_id, local_id) {
    return {
      success: true,
      message: "Local is ahead of remote",
      local_commit: Some(local_id),
      remote_commit: Some(remote_id),
    }
  }
  let local_tree = get_commit_tree(objects, local_id)
  guard local_tree is Some(ltree) else {
    raise @bit.GitError::InvalidObject("Cannot read local notes tree")
  }
  let remote_tree = get_commit_tree(objects, remote_id)
  guard remote_tree is Some(rtree) else {
    raise @bit.GitError::InvalidObject("Cannot read remote notes tree")
  }
  let merged_tree_id = merge_notes_trees_store(
    objects,
    fs,
    git_dir,
    ltree,
    rtree,
    conflict_policy~,
  )
  let commit = @bit.Commit::new(
    merged_tree_id,
    [local_id, remote_id],
    "Hub ",
    0L,
    "+0000",
    "Hub ",
    0L,
    "+0000",
    "Merge hub notes\n",
  )
  let (commit_id, commit_compressed) = @bit.create_commit(commit)
  write_object_bytes(fs, git_dir, commit_id, commit_compressed)
  refs.update(hub_notes_ref_name(), Some(commit_id))
  prs.store = HubStore::load(
    objects,
    refs,
    node_id=prs.store.node_id(),
    signing_key=prs.store.signing_key(),
    require_signed=prs.store.require_signed(),
  )
  {
    success: true,
    message: "Merged remote hub notes",
    local_commit: Some(commit_id),
    remote_commit: Some(remote_id),
  }
}

///|
fn is_commit_ancestor_store(
  objects : &@lib.ObjectStore,
  ancestor : @bit.ObjectId,
  commit : @bit.ObjectId,
) -> Bool raise @bit.GitError {
  if ancestor == commit {
    return true
  }
  let obj = objects.get(commit)
  guard obj is Some(commit_obj) else { return false }
  if commit_obj.obj_type != @bit.ObjectType::Commit {
    return false
  }
  let info = @bit.parse_commit(commit_obj.data)
  for parent in info.parents {
    if is_commit_ancestor_store(objects, ancestor, parent) {
      return true
    }
  }
  false
}

///|
fn merge_notes_trees_store(
  objects : &@lib.ObjectStore,
  fs : &@bit.FileSystem,
  git_dir : String,
  local_tree : @bit.ObjectId,
  remote_tree : @bit.ObjectId,
  conflict_policy? : RecordMergePolicy = RecordMergePolicy::Lww,
) -> @bit.ObjectId raise @bit.GitError {
  let local_entries = read_tree_entries_store(objects, local_tree)
  let remote_entries = read_tree_entries_store(objects, remote_tree)
  let local_map : Map[String, @bit.ObjectId] = Map([])
  for entry in local_entries {
    local_map[entry.name] = entry.id
  }
  let remote_map : Map[String, @bit.ObjectId] = Map([])
  for entry in remote_entries {
    remote_map[entry.name] = entry.id
  }
  let keys : Map[String, Bool] = Map([])
  for key in local_map.keys() {
    keys[key] = true
  }
  for key in remote_map.keys() {
    keys[key] = true
  }
  let merged_entries : Array[@bit.TreeEntry] = []
  for key in keys.keys() {
    match (local_map.get(key), remote_map.get(key)) {
      (Some(lid), Some(rid)) =>
        if lid == rid {
          merged_entries.push(@bit.TreeEntry::new("100644", key, lid))
        } else {
          let ours = objects.get(lid)
          let theirs = objects.get(rid)
          match (ours, theirs) {
            (Some(o), Some(t)) => {
              let merged_bytes = merge_record_bytes_with_policy(
                o.data,
                t.data,
                conflict_policy,
              )
              let merged_text = @utf8.decode_lossy(merged_bytes[:])
              let (blob_id, compressed) = @bit.create_blob_string(merged_text)
              write_object_bytes(fs, git_dir, blob_id, compressed)
              merged_entries.push(@bit.TreeEntry::new("100644", key, blob_id))
            }
            _ => merged_entries.push(@bit.TreeEntry::new("100644", key, lid))
          }
        }
      (Some(lid), None) =>
        merged_entries.push(@bit.TreeEntry::new("100644", key, lid))
      (None, Some(rid)) =>
        merged_entries.push(@bit.TreeEntry::new("100644", key, rid))
      _ => ()
    }
  }
  merged_entries.sort_by((a, b) => String::compare(a.name, b.name))
  let (new_tree_id, tree_compressed) = @bit.create_tree(merged_entries)
  write_object_bytes(fs, git_dir, new_tree_id, tree_compressed)
  new_tree_id
}

///|
fn read_tree_entries_store(
  objects : &@lib.ObjectStore,
  tree_id : @bit.ObjectId,
) -> Array[@bit.TreeEntry] {
  let obj = objects.get(tree_id) catch { _ => None }
  match obj {
    Some(tree_obj) => @bit.parse_tree(tree_obj.data) catch { _ => [] }
    None => []
  }
}

///|
pub fn find_remote_notes_ref(
  refs : Array[(@bit.ObjectId, String)],
) -> (@bit.ObjectId, String) {
  let ref_name = hub_notes_ref_name()
  for item in refs {
    let (id, name) = item
    if name == ref_name {
      return (id, name)
    }
  }
  (@bit.ObjectId::zero(), ref_name)
}

///|
pub fn find_upload_notes_ref(
  refs : Array[(@bit.ObjectId, String)],
) -> (@bit.ObjectId, String)? {
  let ref_name = hub_notes_ref_name()
  for item in refs {
    let (id, name) = item
    if name == ref_name {
      return Some((id, name))
    }
  }
  None
}

///|
pub fn short_ref_name(refname : String) -> String {
  if refname.has_prefix("refs/") {
    String::unsafe_substring(refname, start=5, end=refname.length())
  } else {
    refname
  }
}