///| PR Merge operations

///|
pub(all) enum PrMergeStrategy {
  Merge // Create merge commit
  Squash // Squash all commits
  FastForward // Fast-forward if possible
}

///|
pub struct PrMergeResult {
  success : Bool
  commit_id : @bit.ObjectId?
  conflicts : Array[String]
  message : String
}

///|
pub struct PrMergePolicy {
  required_approvals : Int
  allow_request_changes : Bool
  require_signed_records : Bool
  required_workflows : Array[String]
}

///|
pub fn PrMergePolicy::new(
  required_approvals : Int,
  allow_request_changes : Bool,
  require_signed_records : Bool,
  required_workflows? : Array[String] = [],
) -> PrMergePolicy {
  {
    required_approvals,
    allow_request_changes,
    require_signed_records,
    required_workflows,
  }
}

///|
pub fn PrMergePolicy::default() -> PrMergePolicy {
  PrMergePolicy::new(0, true, false)
}

///|
pub fn PrMergePolicy::required_approvals(self : PrMergePolicy) -> Int {
  self.required_approvals
}

///|
pub fn PrMergePolicy::allow_request_changes(self : PrMergePolicy) -> Bool {
  self.allow_request_changes
}

///|
pub fn PrMergePolicy::require_signed_records(self : PrMergePolicy) -> Bool {
  self.require_signed_records
}

///|
pub fn PrMergePolicy::required_workflows(self : PrMergePolicy) -> Array[String] {
  self.required_workflows
}

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

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

///|
pub fn PrMergeResult::conflicts(self : PrMergeResult) -> Array[String] {
  self.conflicts
}

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

///|
fn summarize_latest_review_state(reviews : Array[PrReview]) -> (Int, Bool) {
  let latest_verdicts : Map[String, ReviewVerdict] = Map([])
  for review in reviews {
    latest_verdicts[review.author] = review.verdict
  }
  let mut approvals = 0
  let mut has_request_changes = false
  for item in latest_verdicts.to_array() {
    let (_, verdict) = item
    match verdict {
      ReviewVerdict::Approved => approvals += 1
      ReviewVerdict::RequestChanges => has_request_changes = true
      ReviewVerdict::Comment => ()
    }
  }
  (approvals, has_request_changes)
}

///|
fn find_pr_workflow_result(
  workflows : Array[PrWorkflowResult],
  task : String,
) -> PrWorkflowResult? {
  let mut found : PrWorkflowResult? = None
  for workflow in workflows {
    if workflow.task() == task {
      found = Some(workflow)
      break
    }
  }
  found
}

///|
fn ensure_required_workflows_success(
  workflows : Array[PrWorkflowResult],
  required_workflows : Array[String],
) -> Unit raise @bit.GitError {
  for task in required_workflows {
    match find_pr_workflow_result(workflows, task) {
      Some(workflow) =>
        if workflow.status() != PrWorkflowStatus::Success {
          raise @bit.GitError::InvalidObject(
            "Merge blocked by policy: required workflow '\{task}' status=\{workflow.status().to_string()}",
          )
        }
      None =>
        raise @bit.GitError::InvalidObject(
          "Merge blocked by policy: required workflow '\{task}' has no result",
        )
    }
  }
}

///|
pub fn Hub::check_merge_policy(
  self : Hub,
  objects : &@lib.ObjectStore,
  refs : &@lib.RefStore,
  pr_id : String,
  policy : PrMergePolicy,
  signing_key? : String? = None,
) -> Unit raise @bit.GitError {
  let pr = self.get_pr(objects, pr_id)
  guard pr is Some(existing_pr) else { return () }
  if existing_pr.state != PrState::Open {
    return ()
  }
  let reviews = self.list_reviews(objects, pr_id)
  let (approved_count, has_request_changes) = summarize_latest_review_state(
    reviews,
  )
  if !policy.allow_request_changes && has_request_changes {
    raise @bit.GitError::InvalidObject(
      "Merge blocked by policy: request-changes review is present",
    )
  }
  if approved_count < policy.required_approvals {
    raise @bit.GitError::InvalidObject(
      "Merge blocked by policy: required approvals=\{policy.required_approvals}, current approvals=\{approved_count}",
    )
  }
  let mut workflows = self.list_pr_workflows(objects, pr_id)
  if policy.require_signed_records {
    guard signing_key is Some(sign_key) else {
      raise @bit.GitError::InvalidObject(
        "Merge blocked by policy: require_signed_records=true requires signing key (BIT_COLLAB_SIGN_KEY for CLI)",
      )
    }
    let strict_hub = Hub::load(
      objects,
      refs,
      signing_key=Some(sign_key),
      require_signed=true,
    )
    if strict_hub.get_pr(objects, pr_id) is None {
      raise @bit.GitError::InvalidObject(
        "Merge blocked by policy: PR metadata is unsigned or has invalid signature",
      )
    }
    let strict_reviews = strict_hub.list_reviews(objects, pr_id)
    if strict_reviews.length() != reviews.length() {
      raise @bit.GitError::InvalidObject(
        "Merge blocked by policy: unsigned or invalid PR reviews are present",
      )
    }
    workflows = strict_hub.list_pr_workflows(objects, pr_id)
  }
  if policy.required_workflows.length() > 0 {
    ensure_required_workflows_success(workflows, policy.required_workflows)
  }
}

///|
/// Check if a PR can be merged (no conflicts, PR is open)
pub fn Hub::can_merge(
  self : Hub,
  objects : &@lib.ObjectStore,
  refs : &@lib.RefStore,
  pr_id : String,
) -> Bool raise @bit.GitError {
  let pr = self.get_pr(objects, pr_id)
  guard pr is Some(existing) else { return false }
  if existing.state != PrState::Open {
    return false
  }
  let source_commit = refs.resolve(existing.source_branch)
  let target_commit = refs.resolve(existing.target_branch)
  guard source_commit is Some(_) else { return false }
  guard target_commit is Some(_) else { return false }
  true
}

///|
/// Merge a Pull Request
pub fn Hub::merge_pr(
  self : Hub,
  objects : &@lib.ObjectStore,
  refs : &@lib.RefStore,
  clock : &@lib.Clock,
  pr_id : String,
  merger : String,
  strategy? : PrMergeStrategy = PrMergeStrategy::Merge,
) -> PrMergeResult raise @bit.GitError {
  let timestamp = clock.now()
  // Get and validate PR
  let pr = self.get_pr(objects, pr_id)
  guard pr is Some(existing) else {
    return {
      success: false,
      commit_id: None,
      conflicts: [],
      message: "PR not found: \{pr_id}",
    }
  }
  if existing.state != PrState::Open {
    return {
      success: false,
      commit_id: None,
      conflicts: [],
      message: "PR is not open: \{pr_id}",
    }
  }
  // Resolve source commit from source_ref/source_branch, then fallback to stored source_commit
  let source_id = match resolve_pr_source_commit(objects, refs, existing) {
    Ok(id) => id
    Err(message) =>
      return { success: false, commit_id: None, conflicts: [], message }
  }
  let target_commit = refs.resolve(existing.target_branch)
  guard target_commit is Some(target_id) else {
    return {
      success: false,
      commit_id: None,
      conflicts: [],
      message: "Target branch not found: \{existing.target_branch}",
    }
  }
  // Create merge message
  let merge_message = "Merge PR #\{pr_id}: \{existing.title}\n\nMerged-by: \{merger}\n"
  // Check for fast-forward possibility
  if is_ancestor(objects, target_id, source_id) {
    // Fast-forward is possible
    match strategy {
      FastForward => {
        update_branch_ref(refs, existing.target_branch, source_id)
        mark_pr_merged(self, objects, refs, clock, pr_id, existing, source_id)
        return {
          success: true,
          commit_id: Some(source_id),
          conflicts: [],
          message: "Fast-forward merge completed",
        }
      }
      Merge => {
        let source_tree = get_commit_tree_id(objects, source_id)
        guard source_tree is Some(tree_id) else {
          return {
            success: false,
            commit_id: None,
            conflicts: [],
            message: "Cannot read source tree",
          }
        }
        let commit = @bit.Commit::new(
          tree_id,
          [target_id, source_id],
          merger,
          timestamp,
          "+0000",
          merger,
          timestamp,
          "+0000",
          merge_message,
        )
        let commit_bytes = @bit.serialize_commit_content(commit)
        let commit_id = objects.put(@bit.ObjectType::Commit, commit_bytes)
        update_branch_ref(refs, existing.target_branch, commit_id)
        mark_pr_merged(self, objects, refs, clock, pr_id, existing, commit_id)
        return {
          success: true,
          commit_id: Some(commit_id),
          conflicts: [],
          message: "Merge commit created",
        }
      }
      Squash => {
        let source_tree = get_commit_tree_id(objects, source_id)
        guard source_tree is Some(tree_id) else {
          return {
            success: false,
            commit_id: None,
            conflicts: [],
            message: "Cannot read source tree",
          }
        }
        let squash_message = "Squash PR #\{pr_id}: \{existing.title}\n\nSquashed-by: \{merger}\n"
        let commit = @bit.Commit::new(
          tree_id,
          [target_id],
          merger,
          timestamp,
          "+0000",
          merger,
          timestamp,
          "+0000",
          squash_message,
        )
        let commit_bytes = @bit.serialize_commit_content(commit)
        let commit_id = objects.put(@bit.ObjectType::Commit, commit_bytes)
        update_branch_ref(refs, existing.target_branch, commit_id)
        mark_pr_merged(self, objects, refs, clock, pr_id, existing, commit_id)
        return {
          success: true,
          commit_id: Some(commit_id),
          conflicts: [],
          message: "Squash merge completed",
        }
      }
    }
  }
  // Check if already up to date
  if is_ancestor(objects, source_id, target_id) {
    return {
      success: false,
      commit_id: None,
      conflicts: [],
      message: "Already up to date",
    }
  }
  // Find merge base
  let base = find_merge_base(objects, target_id, source_id)
  // Collect files from each tree
  let base_files = match base {
    None => Map([])
    Some(id) => collect_files_from_commit(objects, id)
  }
  let target_files = collect_files_from_commit(objects, target_id)
  let source_files = collect_files_from_commit(objects, source_id)
  // Perform three-way merge
  let (merged_files, conflicts) = three_way_merge(
    base_files, target_files, source_files,
  )
  if conflicts.length() > 0 {
    return {
      success: false,
      commit_id: None,
      conflicts,
      message: "Merge conflicts detected",
    }
  }
  // Create merged tree
  let tree_id = create_tree_from_files(objects, merged_files)
  // Create merge commit
  let parents = match strategy {
    Squash => [target_id]
    _ => [target_id, source_id]
  }
  let commit = @bit.Commit::new(
    tree_id, parents, merger, timestamp, "+0000", merger, timestamp, "+0000", merge_message,
  )
  let commit_bytes = @bit.serialize_commit_content(commit)
  let commit_id = objects.put(@bit.ObjectType::Commit, commit_bytes)
  // Update target branch
  update_branch_ref(refs, existing.target_branch, commit_id)
  // Mark PR as merged
  mark_pr_merged(self, objects, refs, clock, pr_id, existing, commit_id)
  {
    success: true,
    commit_id: Some(commit_id),
    conflicts: [],
    message: "Merge completed",
  }
}

///|
fn get_commit_tree_id(
  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)
}

///|
fn is_ancestor(
  objects : &@lib.ObjectStore,
  ancestor : @bit.ObjectId,
  commit_id : @bit.ObjectId,
) -> Bool raise @bit.GitError {
  if ancestor == commit_id {
    return true
  }
  let stack : Array[@bit.ObjectId] = [commit_id]
  let seen : Map[String, Bool] = Map([])
  while stack.length() > 0 {
    let id = stack.pop()
    guard id is Some(current) else { break }
    let hex = current.to_hex()
    if seen.contains(hex) {
      continue
    }
    seen[hex] = true
    if current == ancestor {
      return true
    }
    let obj = objects.get(current)
    guard obj is Some(commit_obj) else { continue }
    if commit_obj.obj_type != @bit.ObjectType::Commit {
      continue
    }
    let info = @bit.parse_commit(commit_obj.data)
    for parent in info.parents {
      stack.push(parent)
    }
  }
  false
}

///|
fn find_merge_base(
  objects : &@lib.ObjectStore,
  a : @bit.ObjectId,
  b : @bit.ObjectId,
) -> @bit.ObjectId? raise @bit.GitError {
  let seen_a : Map[String, Bool] = Map([])
  let stack_a : Array[@bit.ObjectId] = [a]
  while stack_a.length() > 0 {
    let id = stack_a.pop()
    guard id is Some(current) else { break }
    let hex = current.to_hex()
    if seen_a.contains(hex) {
      continue
    }
    seen_a[hex] = true
    let obj = objects.get(current)
    guard obj is Some(commit_obj) else { continue }
    if commit_obj.obj_type != @bit.ObjectType::Commit {
      continue
    }
    let info = @bit.parse_commit(commit_obj.data)
    for parent in info.parents {
      stack_a.push(parent)
    }
  }
  let stack_b : Array[@bit.ObjectId] = [b]
  while stack_b.length() > 0 {
    let id = stack_b.pop()
    guard id is Some(current) else { break }
    let hex = current.to_hex()
    if seen_a.contains(hex) {
      return Some(current)
    }
    let obj = objects.get(current)
    guard obj is Some(commit_obj) else { continue }
    if commit_obj.obj_type != @bit.ObjectType::Commit {
      continue
    }
    let info = @bit.parse_commit(commit_obj.data)
    for parent in info.parents {
      stack_b.push(parent)
    }
  }
  None
}

///|
priv struct FileEntry {
  id : @bit.ObjectId
  mode : String
}

///|
fn collect_files_from_commit(
  objects : &@lib.ObjectStore,
  commit_id : @bit.ObjectId,
) -> Map[String, FileEntry] raise @bit.GitError {
  let result : Map[String, FileEntry] = Map([])
  let obj = objects.get(commit_id)
  guard obj is Some(commit_obj) else { return result }
  if commit_obj.obj_type != @bit.ObjectType::Commit {
    return result
  }
  let info = @bit.parse_commit(commit_obj.data)
  collect_tree_files(objects, info.tree, "", result)
  result
}

///|
fn collect_tree_files(
  objects : &@lib.ObjectStore,
  tree_id : @bit.ObjectId,
  prefix : String,
  out : Map[String, FileEntry],
) -> Unit raise @bit.GitError {
  let obj = objects.get(tree_id)
  guard obj is Some(tree_obj) else { return () }
  if tree_obj.obj_type != @bit.ObjectType::Tree {
    return ()
  }
  let entries = @bit.parse_tree(tree_obj.data)
  for entry in entries {
    let path = if prefix.length() == 0 {
      entry.name
    } else {
      prefix + "/" + entry.name
    }
    if entry.mode.has_prefix("04") {
      collect_tree_files(objects, entry.id, path, out)
    } else {
      out[path] = { id: entry.id, mode: entry.mode }
    }
  }
}

///|
fn three_way_merge(
  base : Map[String, FileEntry],
  ours : Map[String, FileEntry],
  theirs : Map[String, FileEntry],
) -> (Map[String, FileEntry], Array[String]) {
  let merged : Map[String, FileEntry] = Map([])
  let conflicts : Array[String] = []
  let all_paths : Map[String, Bool] = Map([])
  for path in base.keys() {
    all_paths[path] = true
  }
  for path in ours.keys() {
    all_paths[path] = true
  }
  for path in theirs.keys() {
    all_paths[path] = true
  }
  for path in all_paths.keys() {
    let b = base.get(path)
    let o = ours.get(path)
    let t = theirs.get(path)
    if entry_eq(o, t) {
      match o {
        None => ()
        Some(v) => merged[path] = v
      }
    } else if entry_eq(o, b) {
      match t {
        None => ()
        Some(v) => merged[path] = v
      }
    } else if entry_eq(t, b) {
      match o {
        None => ()
        Some(v) => merged[path] = v
      }
    } else {
      conflicts.push(path)
    }
  }
  (merged, conflicts)
}

///|
fn entry_eq(a : FileEntry?, b : FileEntry?) -> Bool {
  match (a, b) {
    (None, None) => true
    (Some(x), Some(y)) => x.id == y.id && x.mode == y.mode
    _ => false
  }
}

///|
fn create_tree_from_files(
  objects : &@lib.ObjectStore,
  files : Map[String, FileEntry],
) -> @bit.ObjectId raise @bit.GitError {
  let tree_map : Map[String, Array[(String, FileEntry)]] = Map([])
  for path, entry in files {
    let slash = path.rev_find("/")
    let (dir, name) = match slash {
      None => ("", path)
      Some(idx) =>
        (
          String::unsafe_substring(path, start=0, end=idx),
          String::unsafe_substring(path, start=idx + 1, end=path.length()),
        )
    }
    if !tree_map.contains(dir) {
      tree_map[dir] = []
    }
    let arr = tree_map.get(dir)
    match arr {
      Some(a) => a.push((name, entry))
      None => ()
    }
  }
  let tree_ids : Map[String, @bit.ObjectId] = Map([])
  let dirs = tree_map.keys().to_array()
  dirs.sort_by(fn(a, b) {
    let a_depth = count_slashes(a)
    let b_depth = count_slashes(b)
    b_depth - a_depth
  })
  for dir in dirs {
    let entries_arr = tree_map.get(dir)
    guard entries_arr is Some(file_entries) else { continue }
    let tree_entries : Array[@bit.TreeEntry] = []
    for pair in file_entries {
      let (name, entry) = pair
      tree_entries.push(@bit.TreeEntry::new(entry.mode, name, entry.id))
    }
    for subdir, id in tree_ids {
      let parent_dir = get_parent_dir(subdir)
      if parent_dir == dir {
        let name = get_base_name(subdir)
        tree_entries.push(@bit.TreeEntry::new("040000", name, id))
      }
    }
    tree_entries.sort_by((a, b) => String::compare(a.name, b.name))
    let tree_bytes = @bit.serialize_tree(tree_entries)
    let tree_id = objects.put(@bit.ObjectType::Tree, tree_bytes)
    tree_ids[dir] = tree_id
  }
  tree_ids.get("") |> Option::unwrap()
}

///|
fn count_slashes(s : String) -> Int {
  let mut count = 0
  for c in s {
    if c == '/' {
      count = count + 1
    }
  }
  count
}

///|
fn get_parent_dir(path : String) -> String {
  match path.rev_find("/") {
    None => ""
    Some(idx) => String::unsafe_substring(path, start=0, end=idx)
  }
}

///|
fn get_base_name(path : String) -> String {
  match path.rev_find("/") {
    None => path
    Some(idx) =>
      String::unsafe_substring(path, start=idx + 1, end=path.length())
  }
}

///|
fn normalize_head_ref(ref_name : String) -> String {
  if ref_name.has_prefix("refs/") {
    ref_name
  } else {
    "refs/heads/" + ref_name
  }
}

///|
fn resolve_ref_with_head_fallback(
  refs : &@lib.RefStore,
  ref_name : String,
) -> @bit.ObjectId? raise @bit.GitError {
  let direct = refs.resolve(ref_name)
  match direct {
    Some(id) => Some(id)
    None =>
      if ref_name.has_prefix("refs/") {
        None
      } else {
        refs.resolve("refs/heads/" + ref_name)
      }
  }
}

///|
fn source_missing_hint(pr : PullRequest) -> String {
  let source_ref = match pr.source_ref {
    Some(ref_name) => normalize_head_ref(ref_name)
    None => normalize_head_ref(pr.source_branch)
  }
  let commit_hex = pr.source_commit.to_hex()
  match pr.source_repo {
    Some(repo) =>
      "Source commit \{commit_hex} is not available locally. Fetch \{repo} \{source_ref} and retry."
    None =>
      "Source commit \{commit_hex} is not available locally. Fetch \{source_ref} and retry."
  }
}

///|
fn resolve_pr_source_commit(
  objects : &@lib.ObjectStore,
  refs : &@lib.RefStore,
  pr : PullRequest,
) -> Result[@bit.ObjectId, String] raise @bit.GitError {
  match pr.source_ref {
    Some(ref_name) => {
      let from_source_ref = resolve_ref_with_head_fallback(refs, ref_name)
      if from_source_ref is Some(id) {
        return Ok(id)
      }
    }
    None => ()
  }
  let from_source_branch = resolve_ref_with_head_fallback(
    refs,
    pr.source_branch,
  )
  if from_source_branch is Some(id) {
    return Ok(id)
  }
  if objects.has(pr.source_commit) {
    return Ok(pr.source_commit)
  }
  Err(source_missing_hint(pr))
}

///|
fn update_branch_ref(
  refs : &@lib.RefStore,
  branch : String,
  commit_id : @bit.ObjectId,
) -> Unit raise @bit.GitError {
  let ref_name = if branch.has_prefix("refs/") {
    branch
  } else {
    "refs/heads/" + branch
  }
  refs.update(ref_name, Some(commit_id))
}

///|
fn mark_pr_merged(
  prs : Hub,
  objects : &@lib.ObjectStore,
  refs : &@lib.RefStore,
  clock : &@lib.Clock,
  pr_id : String,
  existing : PullRequest,
  merge_commit_id : @bit.ObjectId,
) -> Unit raise @bit.GitError {
  let updated = PullRequest::new(
    existing.id,
    existing.title,
    existing.body,
    existing.source_branch,
    existing.source_commit,
    existing.target_branch,
    existing.target_commit,
    existing.author,
    existing.created_at,
    clock.now(),
    PrState::Merged,
    existing.labels,
    merge_commit=Some(merge_commit_id),
    source_repo=existing.source_repo,
    source_ref=existing.source_ref,
  )
  ignore(
    prs.store.put_record(
      objects,
      refs,
      clock,
      work_item_meta_key(pr_id),
      canonical_work_item_record_kind(),
      updated.to_work_item().serialize(),
      existing.author,
    ),
  )
}