package gitstore

import (
	"context"
	"fmt"
	"os"
	"os/exec"
	"strings"

	"github.com/go-git/go-git/v5/plumbing"
)

const (
	// defaultCommitName is the committer name used for system-generated commits.
	defaultCommitName = "gh-server"
	// defaultCommitEmail is the committer email used for system-generated commits.
	defaultCommitEmail = "gh-server@localhost"
)

// MergeOptions defines options for the Merge operation.
type MergeOptions struct {
	FullName        string
	BaseBranch      string
	HeadBranch      string
	Committer       string
	Email           string
	MergeMessage    string
	ExpectedHeadSHA string
}

// SquashMergeOptions defines options for the SquashMerge operation.
type SquashMergeOptions struct {
	FullName        string
	BaseBranch      string
	HeadBranch      string
	Committer       string
	Email           string
	SquashMessage   string
	ExpectedHeadSHA string
}

// RebaseOptions defines options for the Rebase operation.
type RebaseOptions struct {
	FullName        string
	BaseBranch      string
	HeadBranch      string
	Committer       string
	Email           string
	ExpectedHeadSHA string
}

// StackMergeMember describes one selected Stack position. Members must be
// bottom-to-top. BaseBranch is the member's direct parent branch.
type StackMergeMember struct {
	Number          int
	BaseBranch      string
	HeadBranch      string
	Message         string
	ExpectedHeadSHA string
}

// StackMergeOptions defines an atomic Stack merge. All selected positions are
// prepared in one clone and the ultimate base ref is pushed exactly once.
type StackMergeOptions struct {
	FullName    string
	BaseBranch  string
	Members     []StackMergeMember
	MergeMethod string
	Committer   string
	Email       string
}

type StackMergeResult struct {
	FinalSHA   string
	MemberSHAs []string
}

type StackCascadeMember struct {
	BaseBranch string
	HeadBranch string
	OldBaseSHA string
}

type StackCascadeOptions struct {
	FullName   string
	NewBaseSHA string
	Remaining  []StackCascadeMember
	Committer  string
	Email      string
}

// UpdatePRBranchOptions defines options for the UpdatePRBranch operation.
type UpdatePRBranchOptions struct {
	FullName     string
	BaseBranch   string
	HeadBranch   string
	Committer    string
	Email        string
	UpdateMethod string
}

// cloneToTmp creates a temporary working-tree clone of repoDir.
// The returned cleanup function removes the temporary directory.
func cloneToTmp(ctx context.Context, repoDir, prefix string) (string, func(), error) {
	tmpDir, err := os.MkdirTemp("", prefix)
	if err != nil {
		return "", nil, err
	}
	cleanup := func() { _ = os.RemoveAll(tmpDir) }
	if out, err := exec.CommandContext(ctx, "git", "clone", repoDir, tmpDir).CombinedOutput(); err != nil {
		cleanup()
		return "", nil, fmt.Errorf("git clone failed: %v\n%s", err, string(out))
	}
	return tmpDir, cleanup, nil
}

// configUser sets git user.name and user.email in the given repo dir.
func configUser(ctx context.Context, dir, name, email string) error {
	if err := exec.CommandContext(ctx, "git", "-C", dir, "config", "user.name", name).Run(); err != nil {
		return fmt.Errorf("git config user.name: %w", err)
	}
	if err := exec.CommandContext(ctx, "git", "-C", dir, "config", "user.email", email).Run(); err != nil {
		return fmt.Errorf("git config user.email: %w", err)
	}
	return nil
}

type tempCloneOptions struct {
	opName         string
	prefix         string
	committer      string
	email          string
	pushBranch     string
	allowForcePush bool
}

func (s *Store) withTempClone(ctx context.Context, fullName string, opts tempCloneOptions, fn func(tmpDir string) error) (string, error) {
	mu, err := s.repoLock(ctx, fullName)
	if err != nil {
		return "", err
	}
	mu.Lock()
	defer mu.Unlock()

	repoDir, err := s.repoPath(ctx, fullName)
	if err != nil {
		return "", err
	}
	if _, err := os.Stat(repoDir); err != nil {
		return "", fmt.Errorf("gitstore %s: repo not found: %w", opts.opName, err)
	}

	tmpDir, cleanup, err := cloneToTmp(ctx, repoDir, opts.prefix)
	if err != nil {
		return "", err
	}
	defer cleanup()

	committer := opts.committer
	email := opts.email
	if committer == "" {
		committer = defaultCommitName
	}
	if email == "" {
		email = defaultCommitEmail
	}
	if err := configUser(ctx, tmpDir, committer, email); err != nil {
		return "", err
	}

	if err := fn(tmpDir); err != nil {
		return "", err
	}

	out, err := exec.CommandContext(ctx, "git", "-C", tmpDir, "rev-parse", "HEAD").Output()
	if err != nil {
		return "", fmt.Errorf("git rev-parse HEAD failed: %v", err)
	}
	sha := strings.TrimSpace(string(out))

	if opts.pushBranch != "" {
		if pushOut, err := exec.CommandContext(ctx, "git", "-C", tmpDir, "push", "origin", opts.pushBranch).CombinedOutput(); err != nil {
			if opts.allowForcePush {
				if pushOut2, err2 := exec.CommandContext(ctx, "git", "-C", tmpDir, "push", "--force", "origin", opts.pushBranch).CombinedOutput(); err2 != nil {
					return "", fmt.Errorf("git push failed: %v\n%s", err2, string(pushOut2))
				}
			} else {
				return "", fmt.Errorf("git push failed: %v\n%s", err, string(pushOut))
			}
		}
	}

	return sha, nil
}

// CanMerge checks whether headBranch can be cleanly merged into baseBranch
// using git merge-tree (no worktree needed). Returns "MERGEABLE", "CONFLICTING", or "UNKNOWN".
func (s *Store) CanMerge(ctx context.Context, fullName, baseBranch, headBranch string) string {
	repoDir, err := s.repoPath(ctx, fullName)
	if err != nil {
		return "UNKNOWN"
	}
	cmd := exec.CommandContext(ctx, "git", "-C", repoDir, "merge-tree",
		"--write-tree", "--no-messages",
		plumbing.NewBranchReferenceName(baseBranch).String(),
		plumbing.NewBranchReferenceName(headBranch).String())
	if err := cmd.Run(); err != nil {
		return "CONFLICTING"
	}
	return "MERGEABLE"
}

// IsEmpty reports whether the repository has no commits.
func (s *Store) IsEmpty(ctx context.Context, fullName string) bool {
	repoDir, err := s.repoPath(ctx, fullName)
	if err != nil {
		return true
	}
	cmd := exec.CommandContext(ctx, "git", "-C", repoDir, "cat-file", "-t", "HEAD")
	return cmd.Run() != nil
}

// DiskUsageKB returns the approximate size of a repository in kilobytes.
func (s *Store) DiskUsageKB(ctx context.Context, fullName string) int {
	repoDir, err := s.repoPath(ctx, fullName)
	if err != nil {
		return 0
	}
	cmd := exec.CommandContext(ctx, "du", "-sk", repoDir)
	out, err := cmd.Output()
	if err != nil {
		return 0
	}
	var kb int
	fmt.Sscanf(strings.TrimSpace(string(out)), "%d", &kb)
	return kb
}

// Merge performs a real merge of headBranch into baseBranch creating a proper merge commit.
// Returns the resulting merge commit SHA.
func (s *Store) Merge(ctx context.Context, opts MergeOptions) (string, error) {
	return s.withTempClone(ctx, opts.FullName, tempCloneOptions{
		opName:     "merge",
		prefix:     "gh-server-merge-*",
		committer:  opts.Committer,
		email:      opts.Email,
		pushBranch: opts.BaseBranch,
	}, func(tmpDir string) error {
		if err := verifyExpectedHead(ctx, tmpDir, opts.HeadBranch, opts.ExpectedHeadSHA); err != nil {
			return err
		}
		// Checkout base branch
		if out, err := exec.CommandContext(ctx, "git", "-C", tmpDir, "checkout", opts.BaseBranch).CombinedOutput(); err != nil {
			return fmt.Errorf("git checkout %s failed: %v\n%s", opts.BaseBranch, err, string(out))
		}

		// Merge head branch into base
		if out, err := exec.CommandContext(ctx, "git", "-C", tmpDir, "merge", "origin/"+opts.HeadBranch, "-m", opts.MergeMessage, "--no-ff").CombinedOutput(); err != nil {
			return fmt.Errorf("git merge failed: %v\n%s", err, string(out))
		}

		return nil
	})
}

// MergeStackAtomically merges a selected Stack prefix bottom-to-top and
// updates the ultimate base branch only after every position succeeds.
func (s *Store) MergeStackAtomically(ctx context.Context, opts StackMergeOptions) (string, error) {
	result, err := s.MergeStackAtomicallyDetailed(ctx, opts)
	return result.FinalSHA, err
}

// MergeStackAtomicallyDetailed also returns the commit representing each
// selected pull request so its public merge_commit_sha remains exact.
func (s *Store) MergeStackAtomicallyDetailed(ctx context.Context, opts StackMergeOptions) (StackMergeResult, error) {
	if len(opts.Members) == 0 {
		return StackMergeResult{}, fmt.Errorf("gitstore stack merge: no members")
	}
	method := strings.ToLower(opts.MergeMethod)
	if method == "" {
		method = "merge"
	}
	memberSHAs := make([]string, 0, len(opts.Members))
	finalSHA, err := s.withTempClone(ctx, opts.FullName, tempCloneOptions{
		opName:     "stack-merge",
		prefix:     "gh-server-stack-merge-*",
		committer:  opts.Committer,
		email:      opts.Email,
		pushBranch: opts.BaseBranch,
	}, func(tmpDir string) error {
		if out, err := exec.CommandContext(ctx, "git", "-C", tmpDir, "checkout", opts.BaseBranch).CombinedOutput(); err != nil {
			return fmt.Errorf("git checkout %s failed: %v\n%s", opts.BaseBranch, err, string(out))
		}
		for _, member := range opts.Members {
			if err := verifyExpectedHead(ctx, tmpDir, member.HeadBranch, member.ExpectedHeadSHA); err != nil {
				return fmt.Errorf("gitstore Stack PR #%d: %w", member.Number, err)
			}
			message := member.Message
			if message == "" {
				message = fmt.Sprintf("Merge pull request #%d", member.Number)
			}
			switch method {
			case "merge":
				if out, err := exec.CommandContext(ctx, "git", "-C", tmpDir, "merge", "origin/"+member.HeadBranch, "-m", message, "--no-ff").CombinedOutput(); err != nil {
					return fmt.Errorf("git merge Stack PR #%d failed: %v\n%s", member.Number, err, string(out))
				}
			case "squash":
				if out, err := exec.CommandContext(ctx, "git", "-C", tmpDir, "merge", "--squash", "origin/"+member.HeadBranch).CombinedOutput(); err != nil {
					return fmt.Errorf("git squash Stack PR #%d failed: %v\n%s", member.Number, err, string(out))
				}
				if out, err := exec.CommandContext(ctx, "git", "-C", tmpDir, "commit", "-m", message).CombinedOutput(); err != nil {
					return fmt.Errorf("git commit Stack PR #%d failed: %v\n%s", member.Number, err, string(out))
				}
			case "rebase":
				currentBaseOut, err := exec.CommandContext(ctx, "git", "-C", tmpDir, "rev-parse", "HEAD").Output()
				if err != nil {
					return fmt.Errorf("git resolve Stack base for PR #%d: %w", member.Number, err)
				}
				currentBase := strings.TrimSpace(string(currentBaseOut))
				workBranch := fmt.Sprintf("ags-stack-%d", member.Number)
				if out, err := exec.CommandContext(ctx, "git", "-C", tmpDir, "checkout", "-B", workBranch, "origin/"+member.HeadBranch).CombinedOutput(); err != nil {
					return fmt.Errorf("git checkout Stack PR #%d failed: %v\n%s", member.Number, err, string(out))
				}
				if out, err := exec.CommandContext(ctx, "git", "-C", tmpDir, "rebase", "--onto", currentBase, "origin/"+member.BaseBranch, workBranch).CombinedOutput(); err != nil {
					return fmt.Errorf("git rebase Stack PR #%d failed: %v\n%s", member.Number, err, string(out))
				}
				if out, err := exec.CommandContext(ctx, "git", "-C", tmpDir, "checkout", opts.BaseBranch).CombinedOutput(); err != nil {
					return fmt.Errorf("git restore Stack base failed: %v\n%s", err, string(out))
				}
				if out, err := exec.CommandContext(ctx, "git", "-C", tmpDir, "merge", "--ff-only", workBranch).CombinedOutput(); err != nil {
					return fmt.Errorf("git fast-forward Stack PR #%d failed: %v\n%s", member.Number, err, string(out))
				}
			default:
				return fmt.Errorf("gitstore stack merge: unsupported method %q", method)
			}
			out, err := exec.CommandContext(ctx, "git", "-C", tmpDir, "rev-parse", "HEAD").Output()
			if err != nil {
				return fmt.Errorf("git resolve merged Stack PR #%d: %w", member.Number, err)
			}
			memberSHAs = append(memberSHAs, strings.TrimSpace(string(out)))
		}
		return nil
	})
	if err != nil {
		return StackMergeResult{}, err
	}
	return StackMergeResult{FinalSHA: finalSHA, MemberSHAs: memberSHAs}, nil
}

// CascadeRebaseStack rewrites the remaining Stack branches after a selected
// prefix merge. Every ref is prepared locally and published with one atomic
// force-push so clients never observe a half-rebased Stack.
func (s *Store) CascadeRebaseStack(ctx context.Context, opts StackCascadeOptions) (map[string]string, error) {
	mu, err := s.repoLock(ctx, opts.FullName)
	if err != nil {
		return nil, err
	}
	mu.Lock()
	defer mu.Unlock()
	repoDir, err := s.repoPath(ctx, opts.FullName)
	if err != nil {
		return nil, err
	}
	tmpDir, cleanup, err := cloneToTmp(ctx, repoDir, "gh-server-stack-cascade-*")
	if err != nil {
		return nil, err
	}
	defer cleanup()
	if err := configUser(ctx, tmpDir, opts.Committer, opts.Email); err != nil {
		return nil, err
	}
	updated := make(map[string]string, len(opts.Remaining))
	refspecs := make([]string, 0, len(opts.Remaining))
	for i, member := range opts.Remaining {
		local := fmt.Sprintf("ags-stack-rebase-%d", i)
		if out, err := exec.CommandContext(ctx, "git", "-C", tmpDir, "checkout", "-B", local, "origin/"+member.HeadBranch).CombinedOutput(); err != nil {
			return nil, fmt.Errorf("git checkout Stack branch %s failed: %v\n%s", member.HeadBranch, err, string(out))
		}
		baseTarget := opts.NewBaseSHA
		if i > 0 {
			baseTarget = "origin/" + member.BaseBranch
		}
		if sha, ok := updated[member.BaseBranch]; ok {
			baseTarget = sha
		}
		oldBase := member.OldBaseSHA
		if oldBase == "" {
			oldBase = "origin/" + member.BaseBranch
		}
		if out, err := exec.CommandContext(ctx, "git", "-C", tmpDir, "rebase", "--onto", baseTarget, oldBase, local).CombinedOutput(); err != nil {
			return nil, fmt.Errorf("git cascade rebase Stack branch %s failed: %v\n%s", member.HeadBranch, err, string(out))
		}
		out, err := exec.CommandContext(ctx, "git", "-C", tmpDir, "rev-parse", "HEAD").Output()
		if err != nil {
			return nil, fmt.Errorf("git resolve rebased Stack branch %s: %w", member.HeadBranch, err)
		}
		sha := strings.TrimSpace(string(out))
		updated[member.HeadBranch] = sha
		refspecs = append(refspecs, "+refs/heads/"+local+":refs/heads/"+member.HeadBranch)
	}
	if len(refspecs) == 0 {
		return updated, nil
	}
	args := []string{"-C", tmpDir, "push", "--atomic", "origin"}
	args = append(args, refspecs...)
	if out, err := exec.CommandContext(ctx, "git", args...).CombinedOutput(); err != nil {
		return nil, fmt.Errorf("git atomic Stack cascade push failed: %v\n%s", err, string(out))
	}
	return updated, nil
}

// SquashMerge squashes all commits from headBranch into a single commit on baseBranch.
// Unlike Merge (which creates a 2-parent merge commit), this creates a 1-parent commit
// containing the combined diff of the entire head branch. Returns the new HEAD SHA.
func (s *Store) SquashMerge(ctx context.Context, opts SquashMergeOptions) (string, error) {
	return s.withTempClone(ctx, opts.FullName, tempCloneOptions{
		opName:     "squash-merge",
		prefix:     "gh-server-squash-*",
		committer:  opts.Committer,
		email:      opts.Email,
		pushBranch: opts.BaseBranch,
	}, func(tmpDir string) error {
		if err := verifyExpectedHead(ctx, tmpDir, opts.HeadBranch, opts.ExpectedHeadSHA); err != nil {
			return err
		}
		// Checkout base branch
		if out, err := exec.CommandContext(ctx, "git", "-C", tmpDir, "checkout", opts.BaseBranch).CombinedOutput(); err != nil {
			return fmt.Errorf("git checkout %s failed: %v\n%s", opts.BaseBranch, err, string(out))
		}

		// Squash-merge: merge with --squash stages all changes without committing
		if out, err := exec.CommandContext(ctx, "git", "-C", tmpDir, "merge", "--squash", "origin/"+opts.HeadBranch).CombinedOutput(); err != nil {
			return fmt.Errorf("git merge --squash failed: %v\n%s", err, string(out))
		}

		// Commit the squashed changes as a single 1-parent commit
		if out, err := exec.CommandContext(ctx, "git", "-C", tmpDir, "commit", "-m", opts.SquashMessage).CombinedOutput(); err != nil {
			return fmt.Errorf("git commit (squash) failed: %v\n%s", err, string(out))
		}

		return nil
	})
}

// Rebase performs a git rebase of headBranch onto baseBranch fast-forward-style.
// Returns the new HEAD SHA of baseBranch after rebase.
func (s *Store) Rebase(ctx context.Context, opts RebaseOptions) (string, error) {
	return s.withTempClone(ctx, opts.FullName, tempCloneOptions{
		opName:     "rebase",
		prefix:     "gh-server-rebase-*",
		committer:  opts.Committer,
		email:      opts.Email,
		pushBranch: opts.BaseBranch,
	}, func(tmpDir string) error {
		if err := verifyExpectedHead(ctx, tmpDir, opts.HeadBranch, opts.ExpectedHeadSHA); err != nil {
			return err
		}
		// Checkout the head branch and rebase it onto base
		if out, err := exec.CommandContext(ctx, "git", "-C", tmpDir, "checkout", opts.HeadBranch).CombinedOutput(); err != nil {
			// Try with origin/ prefix
			if out2, err2 := exec.CommandContext(ctx, "git", "-C", tmpDir, "checkout", "-b", opts.HeadBranch, "origin/"+opts.HeadBranch).CombinedOutput(); err2 != nil {
				return fmt.Errorf("git checkout %s failed: %v\n%s", opts.HeadBranch, err, string(out)+string(out2))
			}
		}

		if out, err := exec.CommandContext(ctx, "git", "-C", tmpDir, "rebase", "origin/"+opts.BaseBranch).CombinedOutput(); err != nil {
			return fmt.Errorf("git rebase failed: %v\n%s", err, string(out))
		}

		// Checkout base branch and fast-forward merge rebased head
		if out, err := exec.CommandContext(ctx, "git", "-C", tmpDir, "checkout", opts.BaseBranch).CombinedOutput(); err != nil {
			return fmt.Errorf("git checkout %s failed: %v\n%s", opts.BaseBranch, err, string(out))
		}
		if out, err := exec.CommandContext(ctx, "git", "-C", tmpDir, "merge", "--ff-only", opts.HeadBranch).CombinedOutput(); err != nil {
			return fmt.Errorf("git ff-only merge failed: %v\n%s", err, string(out))
		}

		return nil
	})
}

func verifyExpectedHead(ctx context.Context, dir, branch, expected string) error {
	if expected == "" {
		return nil
	}
	out, err := exec.CommandContext(ctx, "git", "-C", dir, "rev-parse", "origin/"+branch).Output()
	if err != nil {
		return fmt.Errorf("git resolve head branch %s: %w", branch, err)
	}
	actual := strings.TrimSpace(string(out))
	if !strings.EqualFold(actual, expected) {
		return fmt.Errorf("head SHA changed: expected %s, got %s", expected, actual)
	}
	return nil
}

// RevertMerge creates a revert branch that reverts a merge commit on baseBranch.
// Returns the new branch name and its HEAD SHA.
func (s *Store) RevertMerge(ctx context.Context, fullName, baseBranch, mergeCommitSHA, revertBranchName string) (string, error) {
	mu, err := s.repoLock(ctx, fullName)
	if err != nil {
		return "", err
	}
	mu.Lock()
	defer mu.Unlock()
	repoDir, err := s.repoPath(ctx, fullName)
	if err != nil {
		return "", err
	}
	if _, err := os.Stat(repoDir); err != nil {
		return "", fmt.Errorf("gitstore revert: repo not found: %w", err)
	}

	tmpDir, cleanup, err := cloneToTmp(ctx, repoDir, "gh-server-revert-*")
	if err != nil {
		return "", err
	}
	defer cleanup()

	if err := configUser(ctx, tmpDir, defaultCommitName, defaultCommitEmail); err != nil {
		return "", err
	}

	// Checkout base branch
	if out, err := exec.CommandContext(ctx, "git", "-C", tmpDir, "checkout", baseBranch).CombinedOutput(); err != nil {
		return "", fmt.Errorf("git checkout %s failed: %v\n%s", baseBranch, err, string(out))
	}

	// Create the revert branch from base
	if out, err := exec.CommandContext(ctx, "git", "-C", tmpDir, "checkout", "-b", revertBranchName).CombinedOutput(); err != nil {
		return "", fmt.Errorf("git checkout -b %s failed: %v\n%s", revertBranchName, err, string(out))
	}

	// Revert the merge commit (use -m 1 for merge commits to specify mainline parent)
	revertArgs := []string{"-C", tmpDir, "revert", "--no-edit", mergeCommitSHA}
	// Inspect the commit to determine if it's a merge commit.
	// Error is intentionally discarded: if cat-file fails, parentCount
	// stays 0 and revert proceeds without -m 1 (correct for non-merge commits).
	cmd := exec.CommandContext(ctx, "git", "-C", tmpDir, "cat-file", "-p", mergeCommitSHA)
	catOut, _ := cmd.Output() //nolint:errcheck // see above
	parentCount := strings.Count(string(catOut), "\nparent ")
	if parentCount > 1 {
		revertArgs = []string{"-C", tmpDir, "revert", "--no-edit", "-m", "1", mergeCommitSHA}
	}

	if out, err := exec.CommandContext(ctx, "git", revertArgs...).CombinedOutput(); err != nil {
		return "", fmt.Errorf("git revert failed: %v\n%s", err, string(out))
	}

	out, err := exec.CommandContext(ctx, "git", "-C", tmpDir, "rev-parse", "HEAD").Output()
	if err != nil {
		return "", fmt.Errorf("git rev-parse HEAD failed: %v", err)
	}
	sha := strings.TrimSpace(string(out))

	// Push the revert branch to the bare repo
	if pushOut, err := exec.CommandContext(ctx, "git", "-C", tmpDir, "push", "origin", revertBranchName).CombinedOutput(); err != nil {
		return "", fmt.Errorf("git push failed: %v\n%s", err, string(pushOut))
	}

	return sha, nil
}

// UpdatePRBranch merges baseBranch into headBranch (or rebases headBranch onto baseBranch),
// effectively updating the PR branch with the latest changes from its base.
// Returns the new HEAD SHA of the head branch.
func (s *Store) UpdatePRBranch(ctx context.Context, opts UpdatePRBranchOptions) (string, error) {
	allowForcePush := strings.EqualFold(opts.UpdateMethod, "rebase")
	return s.withTempClone(ctx, opts.FullName, tempCloneOptions{
		opName:         "update-branch",
		prefix:         "gh-server-update-branch-*",
		committer:      opts.Committer,
		email:          opts.Email,
		pushBranch:     opts.HeadBranch,
		allowForcePush: allowForcePush,
	}, func(tmpDir string) error {
		// Checkout the head (PR) branch
		if out, err := exec.CommandContext(ctx, "git", "-C", tmpDir, "checkout", opts.HeadBranch).CombinedOutput(); err != nil {
			if out2, err2 := exec.CommandContext(ctx, "git", "-C", tmpDir, "checkout", "-b", opts.HeadBranch, "origin/"+opts.HeadBranch).CombinedOutput(); err2 != nil {
				return fmt.Errorf("git checkout %s failed: %v\n%s", opts.HeadBranch, err, string(out)+string(out2))
			}
		}

		if strings.EqualFold(opts.UpdateMethod, "rebase") {
			// Rebase head onto base
			if out, err := exec.CommandContext(ctx, "git", "-C", tmpDir, "rebase", "origin/"+opts.BaseBranch).CombinedOutput(); err != nil {
				return fmt.Errorf("merge conflict between base and head (updatePullRequestBranch): %v\n%s", err, string(out))
			}
		} else {
			// Merge base into head
			msg := fmt.Sprintf("Merge branch '%s' into %s", opts.BaseBranch, opts.HeadBranch)
			if out, err := exec.CommandContext(ctx, "git", "-C", tmpDir, "merge", "origin/"+opts.BaseBranch, "-m", msg, "--no-ff").CombinedOutput(); err != nil {
				return fmt.Errorf("merge conflict between base and head (updatePullRequestBranch): %v\n%s", err, string(out))
			}
		}

		return nil
	})
}

// SimulateMerge simulates a merge of headSHA into baseSHA using git merge-tree
// and creates a dangling merge commit using git commit-tree.
// Returns the resulting merge commit SHA without altering any branches.
func (s *Store) SimulateMerge(ctx context.Context, fullName, baseSHA, headSHA string) (string, error) {
	repoDir, err := s.repoPath(ctx, fullName)
	if err != nil {
		return "", err
	}

	// 1. git merge-tree --write-tree
	cmd := exec.CommandContext(ctx, "git", "-C", repoDir, "merge-tree", "--write-tree", "--no-messages", baseSHA, headSHA)
	out, err := cmd.Output()
	if err != nil {
		return "", fmt.Errorf("git merge-tree failed: %w", err)
	}
	treeSHA := strings.TrimSpace(string(out))
	if treeSHA == "" {
		return "", fmt.Errorf("git merge-tree returned empty tree")
	}

	// 2. git commit-tree
	msg := fmt.Sprintf("Merge commit simulator for PR: %s into %s", headSHA, baseSHA)
	cmd = exec.CommandContext(ctx, "git", "-C", repoDir, "commit-tree", treeSHA, "-p", baseSHA, "-p", headSHA, "-m", msg)

	cmd.Env = append(os.Environ(),
		fmt.Sprintf("GIT_AUTHOR_NAME=%s", defaultCommitName),
		fmt.Sprintf("GIT_AUTHOR_EMAIL=%s", defaultCommitEmail),
		fmt.Sprintf("GIT_COMMITTER_NAME=%s", defaultCommitName),
		fmt.Sprintf("GIT_COMMITTER_EMAIL=%s", defaultCommitEmail),
	)

	out, err = cmd.Output()
	if err != nil {
		return "", fmt.Errorf("git commit-tree failed: %w (tree=%s, base=%s, head=%s)", err, treeSHA, baseSHA, headSHA)
	}
	return strings.TrimSpace(string(out)), nil
}
