package service

import (
	"context"
	"errors"
	"fmt"
	"strings"
	"time"

	"github.com/pingcap/agent-git-service/internal/db"
	"github.com/pingcap/agent-git-service/internal/gitstore"
	"github.com/pingcap/agent-git-service/internal/pagination"

	"gorm.io/gorm"
	"gorm.io/gorm/clause"
)

const (
	MinPullRequestStackSize = 2
	MaxPullRequestStackSize = 100
)

// PullRequestStackView is the fully hydrated public Stack resource. Entries
// are always ordered bottom-to-top.
type PullRequestStackView struct {
	db.PullRequestStack
	Entries []db.PullRequestStackEntry
}

type ListPullRequestStacksOptions struct {
	PullRequestNumber *int
	Page              int
	PerPage           int
}

// PullRequestStackMembership is the compact Stack projection embedded in
// every GitHub REST pull request response.
type PullRequestStackMembership struct {
	ID       uint
	Number   int
	Size     int
	Position int
	BaseRef  string
	BaseSHA  string
}

func (s *Service) ListPullRequestStackMemberships(ctx context.Context, pullRequestIDs []uint) (map[uint]PullRequestStackMembership, error) {
	out := make(map[uint]PullRequestStackMembership, len(pullRequestIDs))
	if len(pullRequestIDs) == 0 {
		return out, nil
	}
	gitStore, err := s.GitForCtx(ctx)
	if err != nil {
		return nil, err
	}
	type row struct {
		PullRequestID      uint
		StackID            uint
		Number             int
		Size               int
		Position           int
		BaseRef            string
		BaseSHA            string
		RepositoryFullName string
	}
	var rows []row
	err = s.DBForCtx(ctx).Raw(`
		SELECT e.pull_request_id, s.id AS stack_id, s.number, counts.size,
		       e.position, s.base_ref, s.base_sha, repositories.full_name AS repository_full_name
		FROM pull_request_stack_entries e
		JOIN pull_request_stacks s ON s.id = e.stack_id
		JOIN repositories ON repositories.id = s.repository_id
		JOIN (
			SELECT stack_id, COUNT(*) AS size
			FROM pull_request_stack_entries
			GROUP BY stack_id
		) counts ON counts.stack_id = s.id
		WHERE e.pull_request_id IN ?`, pullRequestIDs).Scan(&rows).Error
	if err != nil {
		return nil, err
	}
	baseHeads := make(map[uint]string)
	for _, row := range rows {
		baseSHA, ok := baseHeads[row.StackID]
		if !ok {
			baseSHA = row.BaseSHA
			if row.RepositoryFullName != "" {
				if current, headErr := gitStore.HeadSHA(ctx, row.RepositoryFullName, row.BaseRef); headErr == nil && current != "" {
					baseSHA = current
				}
			}
			baseHeads[row.StackID] = baseSHA
		}
		out[row.PullRequestID] = PullRequestStackMembership{
			ID: row.StackID, Number: row.Number, Size: row.Size, Position: row.Position,
			BaseRef: row.BaseRef, BaseSHA: baseSHA,
		}
	}
	return out, nil
}

// ListPullRequestStacks returns one page of repository Stacks newest-number
// first, plus the total number of Stacks matching the filter.
func (s *Service) ListPullRequestStacks(ctx context.Context, repoFullName string, opts ListPullRequestStacksOptions) ([]PullRequestStackView, int64, error) {
	repo, err := s.GetRepo(ctx, repoFullName)
	if err != nil {
		return nil, 0, err
	}
	if err := s.requireRepoPermission(ctx, repo.ID, RepoPermissionRead); err != nil {
		return nil, 0, err
	}
	if opts.PerPage == 0 {
		opts.PerPage = 30
	}
	if opts.PerPage < 1 || opts.PerPage > 100 || opts.Page < 0 {
		return nil, 0, fmt.Errorf("%w: page and per_page are invalid", ErrValidation)
	}
	if opts.Page == 0 {
		opts.Page = 1
	}

	stackQuery := func() *gorm.DB {
		q := s.DBForCtx(ctx).Model(&db.PullRequestStack{}).
			Where("pull_request_stacks.repository_id = ?", repo.ID)
		if opts.PullRequestNumber != nil {
			q = q.Joins("JOIN pull_request_stack_entries ON pull_request_stack_entries.stack_id = pull_request_stacks.id").
				Joins("JOIN pull_requests ON pull_requests.id = pull_request_stack_entries.pull_request_id").
				Where("pull_requests.repository_id = ? AND pull_requests.number = ?", repo.ID, *opts.PullRequestNumber)
		}
		return q
	}
	var total int64
	if err := stackQuery().Count(&total).Error; err != nil {
		return nil, 0, err
	}
	offset, ok := pagination.Offset(opts.Page, opts.PerPage)
	if !ok || int64(offset) >= total {
		return []PullRequestStackView{}, total, nil
	}
	var stacks []db.PullRequestStack
	if err := stackQuery().Order("pull_request_stacks.number DESC").
		Offset(offset).Limit(opts.PerPage).Find(&stacks).Error; err != nil {
		return nil, 0, err
	}
	views, err := s.hydratePullRequestStacks(ctx, stacks)
	if err != nil {
		return nil, 0, err
	}
	return views, total, nil
}

func (s *Service) GetPullRequestStack(ctx context.Context, repoFullName string, number int) (PullRequestStackView, error) {
	repo, err := s.GetRepo(ctx, repoFullName)
	if err != nil {
		return PullRequestStackView{}, err
	}
	if err := s.requireRepoPermission(ctx, repo.ID, RepoPermissionRead); err != nil {
		return PullRequestStackView{}, err
	}
	return s.getPullRequestStackByNumber(ctx, s.DBForCtx(ctx), repo.ID, number, false)
}

// GetPullRequestStackForPR returns nil when the pull request is not stacked.
func (s *Service) GetPullRequestStackForPR(ctx context.Context, prID uint) (*PullRequestStackView, error) {
	var entry db.PullRequestStackEntry
	if err := s.DBForCtx(ctx).Where("pull_request_id = ?", prID).First(&entry).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, nil
		}
		return nil, err
	}
	var stack db.PullRequestStack
	if err := s.DBForCtx(ctx).First(&stack, entry.StackID).Error; err != nil {
		return nil, wrapErr(err)
	}
	views, err := s.hydratePullRequestStacks(ctx, []db.PullRequestStack{stack})
	if err != nil {
		return nil, err
	}
	if len(views) == 0 {
		return nil, nil
	}
	return &views[0], nil
}

func (s *Service) CreatePullRequestStack(ctx context.Context, repoFullName string, pullRequestNumbers []int) (PullRequestStackView, error) {
	if len(pullRequestNumbers) < MinPullRequestStackSize || len(pullRequestNumbers) > MaxPullRequestStackSize {
		return PullRequestStackView{}, fmt.Errorf("%w: pull_requests must contain between %d and %d pull requests", ErrValidation, MinPullRequestStackSize, MaxPullRequestStackSize)
	}
	repo, err := s.GetRepo(ctx, repoFullName)
	if err != nil {
		return PullRequestStackView{}, err
	}
	if err := s.requireRepoPermission(ctx, repo.ID, RepoPermissionWrite); err != nil {
		return PullRequestStackView{}, err
	}

	var created db.PullRequestStack
	err = s.DBForCtx(ctx).Transaction(func(tx *gorm.DB) error {
		if err := lockRepoForStackTopology(tx, repo.ID); err != nil {
			return err
		}
		prs, err := loadAndValidateStackPRsTx(tx, repo.ID, pullRequestNumbers)
		if err != nil {
			return err
		}
		active, err := activePullRequestIDsTx(tx, pullRequestIDs(prs))
		if err != nil {
			return err
		}
		if len(active) > 0 {
			return fmt.Errorf("%w: one or more pull requests are being merged or queued", ErrConflict)
		}
		number, err := nextPullRequestStackNumberTx(tx, repo.ID)
		if err != nil {
			return err
		}
		created = db.PullRequestStack{
			RepositoryID: repo.ID,
			Number:       number,
			BaseRef:      prs[0].BaseRef,
			BaseSHA:      prs[0].BaseSHA,
		}
		if err := tx.Create(&created).Error; err != nil {
			if isDuplicateErr(err) {
				return fmt.Errorf("%w: a pull request is already in a stack", ErrValidation)
			}
			return err
		}
		entries := make([]db.PullRequestStackEntry, len(prs))
		for i := range prs {
			entries[i] = db.PullRequestStackEntry{StackID: created.ID, PullRequestID: prs[i].ID, Position: i + 1}
		}
		if err := tx.Create(&entries).Error; err != nil {
			if isDuplicateErr(err) {
				return fmt.Errorf("%w: a pull request is already in a stack", ErrValidation)
			}
			return err
		}
		return nil
	})
	if err != nil {
		return PullRequestStackView{}, fmt.Errorf("create pull request stack: %w", err)
	}
	return s.getPullRequestStackByNumber(ctx, s.DBForCtx(ctx), repo.ID, created.Number, false)
}

// AddPullRequestsToStack appends one or more pull requests above the current
// top entry. GitHub's API accepts only this bottom-to-top delta; it does not
// expose arbitrary insertion or reordering.
func (s *Service) AddPullRequestsToStack(ctx context.Context, repoFullName string, stackNumber int, pullRequestNumbers []int) (PullRequestStackView, error) {
	if len(pullRequestNumbers) == 0 {
		return PullRequestStackView{}, fmt.Errorf("%w: pull_requests is required", ErrValidation)
	}
	repo, err := s.GetRepo(ctx, repoFullName)
	if err != nil {
		return PullRequestStackView{}, err
	}
	if err := s.requireRepoPermission(ctx, repo.ID, RepoPermissionWrite); err != nil {
		return PullRequestStackView{}, err
	}

	err = s.DBForCtx(ctx).Transaction(func(tx *gorm.DB) error {
		if err := lockRepoForStackTopology(tx, repo.ID); err != nil {
			return err
		}
		current, err := s.getPullRequestStackByNumber(ctx, tx, repo.ID, stackNumber, true)
		if err != nil {
			return err
		}
		if len(current.Entries)+len(pullRequestNumbers) > MaxPullRequestStackSize {
			return fmt.Errorf("%w: a stack cannot contain more than %d pull requests", ErrValidation, MaxPullRequestStackSize)
		}
		active, err := activeStackPullRequestIDsTx(tx, current.Entries)
		if err != nil {
			return err
		}
		if len(active) > 0 {
			return fmt.Errorf("%w: stack is being merged or queued", ErrConflict)
		}
		allMerged := true
		for _, entry := range current.Entries {
			if !entry.PullRequest.Merged {
				allMerged = false
				break
			}
		}
		if allMerged {
			return fmt.Errorf("%w: cannot append to a fully merged stack", ErrValidation)
		}
		prs, err := loadAndValidateStackPRsTx(tx, repo.ID, pullRequestNumbers)
		if err != nil {
			return err
		}
		active, err = activePullRequestIDsTx(tx, pullRequestIDs(prs))
		if err != nil {
			return err
		}
		if len(active) > 0 {
			return fmt.Errorf("%w: one or more pull requests are being merged or queued", ErrConflict)
		}
		top := current.Entries[len(current.Entries)-1].PullRequest
		if prs[0].BaseRef != top.HeadRef {
			return fmt.Errorf("%w: pull request #%d must target %q", ErrValidation, prs[0].Number, top.HeadRef)
		}
		entries := make([]db.PullRequestStackEntry, len(prs))
		for i := range prs {
			entries[i] = db.PullRequestStackEntry{
				StackID: current.ID, PullRequestID: prs[i].ID, Position: len(current.Entries) + i + 1,
			}
		}
		if err := tx.Create(&entries).Error; err != nil {
			if isDuplicateErr(err) {
				return fmt.Errorf("%w: a pull request is already in a stack", ErrValidation)
			}
			return err
		}
		return tx.Model(&db.PullRequestStack{}).Where("id = ?", current.ID).Update("updated_at", time.Now().UTC()).Error
	})
	if err != nil {
		return PullRequestStackView{}, fmt.Errorf("add pull requests to stack: %w", err)
	}
	return s.getPullRequestStackByNumber(ctx, s.DBForCtx(ctx), repo.ID, stackNumber, false)
}

// UnstackPullRequestStack removes every unmerged, inactive pull request. It
// leaves PR bases and Git history untouched. A nil result means the Stack was
// dissolved and the REST endpoint must return 204.
func (s *Service) UnstackPullRequestStack(ctx context.Context, repoFullName string, stackNumber int) (*PullRequestStackView, error) {
	repo, err := s.GetRepo(ctx, repoFullName)
	if err != nil {
		return nil, err
	}
	if err := s.requireRepoPermission(ctx, repo.ID, RepoPermissionWrite); err != nil {
		return nil, err
	}
	var dissolved bool
	err = s.DBForCtx(ctx).Transaction(func(tx *gorm.DB) error {
		if err := lockRepoForStackTopology(tx, repo.ID); err != nil {
			return err
		}
		current, err := s.getPullRequestStackByNumber(ctx, tx, repo.ID, stackNumber, true)
		if err != nil {
			return err
		}
		active, err := activeStackPullRequestIDsTx(tx, current.Entries)
		if err != nil {
			return err
		}
		kept := make([]db.PullRequestStackEntry, 0, len(current.Entries))
		for _, entry := range current.Entries {
			if entry.PullRequest.Merged || active[entry.PullRequestID] {
				kept = append(kept, entry)
				continue
			}
			if err := tx.Delete(&db.PullRequestStackEntry{}, entry.ID).Error; err != nil {
				return err
			}
		}
		if len(kept) == len(current.Entries) {
			return fmt.Errorf("%w: every pull request in the stack is locked and cannot be removed", ErrValidation)
		}
		if len(kept) == 0 {
			dissolved = true
			return tx.Delete(&db.PullRequestStack{}, current.ID).Error
		}
		for i := range kept {
			if kept[i].Position == i+1 {
				continue
			}
			if err := tx.Model(&db.PullRequestStackEntry{}).Where("id = ?", kept[i].ID).Update("position", i+1).Error; err != nil {
				return err
			}
		}
		return nil
	})
	if err != nil {
		return nil, fmt.Errorf("unstack pull request stack: %w", err)
	}
	if dissolved {
		return nil, nil
	}
	view, err := s.getPullRequestStackByNumber(ctx, s.DBForCtx(ctx), repo.ID, stackNumber, false)
	return &view, err
}

func loadAndValidateStackPRsTx(tx *gorm.DB, repoID uint, numbers []int) ([]db.PullRequest, error) {
	seen := make(map[int]struct{}, len(numbers))
	for _, number := range numbers {
		if number <= 0 {
			return nil, fmt.Errorf("%w: pull request numbers must be positive", ErrValidation)
		}
		if _, exists := seen[number]; exists {
			return nil, fmt.Errorf("%w: duplicate pull request #%d", ErrValidation, number)
		}
		seen[number] = struct{}{}
	}
	var rows []db.PullRequest
	if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
		Where("repository_id = ? AND number IN ?", repoID, numbers).Find(&rows).Error; err != nil {
		return nil, err
	}
	if len(rows) != len(numbers) {
		return nil, fmt.Errorf("%w: one or more pull requests do not exist", ErrValidation)
	}
	byNumber := make(map[int]db.PullRequest, len(rows))
	for _, pr := range rows {
		byNumber[pr.Number] = pr
	}
	ordered := make([]db.PullRequest, len(numbers))
	for i, number := range numbers {
		pr := byNumber[number]
		if pr.HeadRepositoryID != 0 && pr.HeadRepositoryID != repoID {
			return nil, fmt.Errorf("%w: pull request #%d is from a fork", ErrValidation, number)
		}
		if pr.Merged {
			return nil, fmt.Errorf("%w: pull request #%d is already merged", ErrValidation, number)
		}
		ordered[i] = pr
		if i > 0 && pr.BaseRef != ordered[i-1].HeadRef {
			return nil, fmt.Errorf("%w: pull request #%d must target %q", ErrValidation, number, ordered[i-1].HeadRef)
		}
	}
	var memberships int64
	ids := make([]uint, len(ordered))
	for i := range ordered {
		ids[i] = ordered[i].ID
	}
	if err := tx.Model(&db.PullRequestStackEntry{}).Where("pull_request_id IN ?", ids).Count(&memberships).Error; err != nil {
		return nil, err
	}
	if memberships != 0 {
		return nil, fmt.Errorf("%w: a pull request is already in a stack", ErrValidation)
	}
	return ordered, nil
}

func (s *Service) getPullRequestStackByNumber(ctx context.Context, tx *gorm.DB, repoID uint, number int, lock bool) (PullRequestStackView, error) {
	var stack db.PullRequestStack
	q := tx.Where("repository_id = ? AND number = ?", repoID, number)
	if lock {
		q = q.Clauses(clause.Locking{Strength: "UPDATE", Options: "NOWAIT"})
	}
	if err := q.First(&stack).Error; err != nil {
		if lock && isDatabaseLockConflict(err) {
			return PullRequestStackView{}, fmt.Errorf("%w: stack is being modified by another request", ErrConflict)
		}
		return PullRequestStackView{}, wrapErrf(err, "pull request stack #%d", number)
	}
	views, err := s.hydratePullRequestStacksWithDB(ctx, tx, []db.PullRequestStack{stack}, lock)
	if err != nil {
		return PullRequestStackView{}, err
	}
	return views[0], nil
}

func (s *Service) getPullRequestStackByID(ctx context.Context, tx *gorm.DB, stackID uint, lock bool) (PullRequestStackView, error) {
	var stack db.PullRequestStack
	q := tx
	if lock {
		q = q.Clauses(clause.Locking{Strength: "UPDATE"})
	}
	if err := q.First(&stack, stackID).Error; err != nil {
		return PullRequestStackView{}, wrapErr(err)
	}
	views, err := s.hydratePullRequestStacksWithDB(ctx, tx, []db.PullRequestStack{stack}, lock)
	if err != nil {
		return PullRequestStackView{}, err
	}
	return views[0], nil
}

func isDatabaseLockConflict(err error) bool {
	if err == nil {
		return false
	}
	message := strings.ToLower(err.Error())
	return strings.Contains(message, "lock") || strings.Contains(message, "nowait") || strings.Contains(message, "3572")
}

func (s *Service) hydratePullRequestStacks(ctx context.Context, stacks []db.PullRequestStack) ([]PullRequestStackView, error) {
	return s.hydratePullRequestStacksWithDB(ctx, s.DBForCtx(ctx), stacks, false)
}

func (s *Service) hydratePullRequestStacksWithDB(_ context.Context, tx *gorm.DB, stacks []db.PullRequestStack, lock bool) ([]PullRequestStackView, error) {
	views := make([]PullRequestStackView, len(stacks))
	for i, stack := range stacks {
		var entries []db.PullRequestStackEntry
		q := tx.Where("stack_id = ?", stack.ID).Order("position ASC")
		if lock {
			q = q.Clauses(clause.Locking{Strength: "UPDATE"}).
				Preload("PullRequest", func(prs *gorm.DB) *gorm.DB {
					return prs.Clauses(clause.Locking{Strength: "UPDATE"})
				})
		}
		if err := q.
			Preload("PullRequest.Repository.Owner").
			Preload("PullRequest.HeadRepository.Owner").
			Preload("PullRequest.Author").
			Find(&entries).Error; err != nil {
			return nil, err
		}
		views[i] = PullRequestStackView{PullRequestStack: stack, Entries: entries}
	}
	return views, nil
}

func activeStackPullRequestIDsTx(tx *gorm.DB, entries []db.PullRequestStackEntry) (map[uint]bool, error) {
	ids := make([]uint, len(entries))
	for i := range entries {
		ids[i] = entries[i].PullRequestID
	}
	active, err := activePullRequestIDsTx(tx, ids)
	if err != nil {
		return nil, err
	}
	// A direct async request is stored only on its target PR, but execution
	// merges the entire Stack prefix through that target. Treat every lower
	// member as active so unstack cannot change the persisted request's plan.
	highestActivePosition := 0
	for _, entry := range entries {
		if active[entry.PullRequestID] && entry.Position > highestActivePosition {
			highestActivePosition = entry.Position
		}
	}
	for _, entry := range entries {
		if entry.Position <= highestActivePosition {
			active[entry.PullRequestID] = true
		}
	}
	return active, nil
}

func pullRequestIDs(prs []db.PullRequest) []uint {
	ids := make([]uint, len(prs))
	for i := range prs {
		ids[i] = prs[i].ID
	}
	return ids
}

func activePullRequestIDsTx(tx *gorm.DB, ids []uint) (map[uint]bool, error) {
	active := make(map[uint]bool)
	if len(ids) == 0 {
		return active, nil
	}
	var queueIDs []uint
	if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Model(&db.MergeQueueEntry{}).
		Where("pull_request_id IN ? AND state IN ?", ids, activeMergeQueueEntryStates).
		Pluck("pull_request_id", &queueIDs).Error; err != nil {
		return nil, err
	}
	for _, id := range queueIDs {
		active[id] = true
	}
	var asyncIDs []uint
	if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Model(&db.AsyncMergeRequest{}).
		Where("pull_request_id IN ? AND status IN ?", ids, recoverableAsyncMergeStatuses).
		Pluck("pull_request_id", &asyncIDs).Error; err != nil {
		return nil, err
	}
	for _, id := range asyncIDs {
		active[id] = true
	}
	return active, nil
}

func nextPullRequestStackNumberTx(tx *gorm.DB, repoID uint) (int, error) {
	if err := ensurePullRequestStackNumberCounterTx(tx, repoID); err != nil {
		return 0, err
	}
	now := time.Now().UTC()
	switch tx.Dialector.Name() {
	case "mysql":
		res := tx.Exec("UPDATE pull_request_stack_number_counters SET next_number = LAST_INSERT_ID(next_number + 1), updated_at = ? WHERE repository_id = ?", now, repoID)
		if res.Error != nil {
			return 0, res.Error
		}
		var next int
		if err := tx.Raw("SELECT LAST_INSERT_ID()").Scan(&next).Error; err != nil {
			return 0, err
		}
		return next - 1, nil
	default:
		var counter db.PullRequestStackNumberCounter
		if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&counter, "repository_id = ?", repoID).Error; err != nil {
			return 0, err
		}
		if err := tx.Model(&counter).Updates(map[string]any{"next_number": counter.NextNumber + 1, "updated_at": now}).Error; err != nil {
			return 0, err
		}
		return counter.NextNumber, nil
	}
}

func ensurePullRequestStackNumberCounterTx(tx *gorm.DB, repoID uint) error {
	var maxNumber int
	if err := tx.Model(&db.PullRequestStack{}).Where("repository_id = ?", repoID).Select("COALESCE(MAX(number), 0)").Scan(&maxNumber).Error; err != nil {
		return err
	}
	now := time.Now().UTC()
	counter := db.PullRequestStackNumberCounter{RepositoryID: repoID, NextNumber: maxNumber + 1, CreatedAt: now, UpdatedAt: now}
	if err := tx.Clauses(clause.OnConflict{DoNothing: true}).Create(&counter).Error; err != nil {
		return err
	}
	var persisted db.PullRequestStackNumberCounter
	if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&persisted, "repository_id = ?", repoID).Error; err != nil {
		return err
	}
	if persisted.NextNumber > maxNumber {
		return nil
	}
	return tx.Model(&persisted).Update("next_number", maxNumber+1).Error
}

// cascadeStackAfterMerge rebases each remaining branch across its original
// direct base boundary. The first remaining pull request is retargeted to the
// Stack's ultimate base, matching GitHub's post-merge Stack topology.
func (s *Service) cascadeStackAfterMerge(ctx context.Context, targetPullRequestID uint, newBaseSHA string, actor db.User) error {
	stack, err := s.GetPullRequestStackForPR(ctx, targetPullRequestID)
	if err != nil || stack == nil {
		return err
	}
	targetPosition := 0
	for _, entry := range stack.Entries {
		if entry.PullRequestID == targetPullRequestID {
			targetPosition = entry.Position
			break
		}
	}
	if targetPosition == 0 {
		return nil
	}
	completed, err := s.stackCascadeAlreadyFinalized(ctx, *stack, targetPosition, newBaseSHA)
	if err != nil {
		return err
	}
	if completed {
		return nil
	}
	gitStore, err := s.GitForCtx(ctx)
	if err != nil {
		return err
	}
	updated := make(map[string]string)
	if len(stack.Entries) > 0 {
		remaining := make([]gitstore.StackCascadeMember, 0, len(stack.Entries)-targetPosition)
		remainingPRs := make([]db.PullRequest, 0, len(stack.Entries)-targetPosition)
		for _, entry := range stack.Entries {
			pr := entry.PullRequest
			if entry.Position > targetPosition && !pr.Merged {
				remaining = append(remaining, gitstore.StackCascadeMember{
					BaseBranch: pr.BaseRef, HeadBranch: pr.HeadRef, OldBaseSHA: pr.BaseSHA,
				})
				remainingPRs = append(remainingPRs, pr)
			}
		}
		fullName := stack.Entries[0].PullRequest.Repository.FullName
		allOriginal := true
		observed := make(map[string]string, len(remainingPRs))
		for i := range remainingPRs {
			sha, headErr := gitStore.HeadSHA(ctx, fullName, remainingPRs[i].HeadRef)
			if headErr != nil {
				return headErr
			}
			observed[remainingPRs[i].HeadRef] = sha
			if sha != remainingPRs[i].HeadSHA {
				allOriginal = false
			}
		}
		if allOriginal {
			updated, err = gitStore.CascadeRebaseStack(ctx, gitstore.StackCascadeOptions{
				FullName: fullName, NewBaseSHA: newBaseSHA,
				Remaining: remaining, Committer: actor.Login, Email: actor.Email,
			})
			if err != nil {
				return err
			}
		} else {
			// CascadeRebaseStack publishes every remaining branch atomically. If
			// the process died after that push, the refs form the intended ancestry
			// chain and retain each old branch's final tree. Anything else is
			// ambiguous and must stay in the durable cascading state.
			parent := newBaseSHA
			for i := range remainingPRs {
				sha := observed[remainingPRs[i].HeadRef]
				ancestor, ancestryErr := gitStore.IsAncestor(ctx, fullName, parent, sha)
				if ancestryErr != nil || !ancestor {
					return fmt.Errorf("cannot prove recovered Stack cascade ancestry for %s", remainingPRs[i].HeadRef)
				}
				oldCommit, oldErr := gitStore.GetCommit(ctx, fullName, remainingPRs[i].HeadSHA)
				newCommit, newErr := gitStore.GetCommit(ctx, fullName, sha)
				if oldErr != nil || newErr != nil || oldCommit.TreeSHA != newCommit.TreeSHA {
					return fmt.Errorf("cannot prove recovered Stack cascade tree for %s", remainingPRs[i].HeadRef)
				}
				updated[remainingPRs[i].HeadRef] = sha
				parent = sha
			}
		}
	}
	return s.DBForCtx(ctx).Transaction(func(tx *gorm.DB) error {
		if err := tx.Model(&db.PullRequestStack{}).Where("id = ?", stack.ID).Update("base_sha", newBaseSHA).Error; err != nil {
			return err
		}
		retargetedFirstRemaining := false
		for _, entry := range stack.Entries {
			pr := entry.PullRequest
			updates := make(map[string]any)
			if entry.Position > targetPosition && !pr.Merged && !retargetedFirstRemaining {
				updates["base_ref"] = stack.BaseRef
				updates["base_sha"] = newBaseSHA
				retargetedFirstRemaining = true
			}
			if sha, ok := updated[pr.HeadRef]; ok && sha != "" {
				updates["head_sha"] = sha
			}
			if _, retargeted := updates["base_ref"]; retargeted {
				// The first unmerged pull request now targets the Stack base.
			} else if sha, ok := updated[pr.BaseRef]; ok && sha != "" {
				updates["base_sha"] = sha
			} else if pr.BaseRef == stack.BaseRef && newBaseSHA != "" {
				updates["base_sha"] = newBaseSHA
			}
			if len(updates) > 0 {
				if err := tx.Model(&db.PullRequest{}).Where("id = ?", pr.ID).Updates(updates).Error; err != nil {
					return err
				}
			}
		}
		return nil
	})
}

// stackCascadeAlreadyFinalized recognizes the crash window after the cascade
// Git refs and database topology committed but before the outer merge workflow
// recorded completion. Replaying a rebase there can manufacture a second set
// of commits, so recovery proves the full persisted chain and returns instead.
func (s *Service) stackCascadeAlreadyFinalized(ctx context.Context, stack PullRequestStackView, targetPosition int, newBaseSHA string) (bool, error) {
	if stack.BaseSHA != newBaseSHA {
		return false, nil
	}
	gitStore, err := s.GitForCtx(ctx)
	if err != nil {
		return false, err
	}
	expectedBaseRef := stack.BaseRef
	expectedBaseSHA := newBaseSHA
	for _, entry := range stack.Entries {
		pr := entry.PullRequest
		if entry.Position <= targetPosition || pr.Merged {
			continue
		}
		if pr.BaseRef != expectedBaseRef || pr.BaseSHA != expectedBaseSHA || pr.HeadSHA == "" {
			return false, nil
		}
		fullName := pr.Repository.FullName
		if fullName == "" && len(stack.Entries) > 0 {
			fullName = stack.Entries[0].PullRequest.Repository.FullName
		}
		actualSHA, err := gitStore.HeadSHA(ctx, fullName, pr.HeadRef)
		if err != nil {
			return false, err
		}
		if actualSHA != pr.HeadSHA {
			return false, nil
		}
		expectedBaseRef = pr.HeadRef
		expectedBaseSHA = pr.HeadSHA
	}
	return true, nil
}
