package service

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

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

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

type MergeQueueView struct {
	Queue         db.MergeQueue
	Configuration MergeQueueConfiguration
	Entries       []db.MergeQueueEntry
}

// MergeQueueTarget identifies the branch and optional Stack whose rules decide
// whether a pull request must enter a merge queue. It intentionally contains
// only compact topology metadata; read paths must not hydrate every Stack
// member merely to resolve the governing branch.
type MergeQueueTarget struct {
	BaseRef string
	StackID uint
}

// MergeQueueTargetForPullRequest resolves the one branch whose rules govern a
// pull request's merge with one compact membership query. Callers must use it
// instead of inspecting BaseRef directly so Stack submission, enqueue, status
// lookup, and GraphQL agree.
func (s *Service) MergeQueueTargetForPullRequest(ctx context.Context, pr db.PullRequest) (MergeQueueTarget, error) {
	return s.mergeQueueTargetForPullRequestDB(s.DBForCtx(ctx), pr, false)
}

func (s *Service) mergeQueueTargetForPullRequestDB(tx *gorm.DB, pr db.PullRequest, lock bool) (MergeQueueTarget, error) {
	target := MergeQueueTarget{BaseRef: pr.BaseRef}
	var row struct {
		StackID uint
		BaseRef string
	}
	q := tx.Table("pull_request_stack_entries AS stack_entries").
		Select("stack_entries.stack_id AS stack_id, stacks.base_ref AS base_ref").
		Joins("JOIN pull_request_stacks AS stacks ON stacks.id = stack_entries.stack_id").
		Where("stack_entries.pull_request_id = ?", pr.ID)
	if lock {
		q = q.Clauses(clause.Locking{Strength: "UPDATE"})
	}
	err := q.Take(&row).Error
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return target, nil
	}
	if err != nil {
		return MergeQueueTarget{}, err
	}
	target.BaseRef = row.BaseRef
	target.StackID = row.StackID
	return target, nil
}

// mergeQueueSelectionForPullRequest resolves the governing branch and the
// exact Stack prefix from the same database handle. Mutating callers pass
// lock=true after acquiring lockRepoForStackTopology so every topology read is
// a current locking read even when the transaction waited on the repository
// lock. Read-only workers pass false because an active request blocks mutation.
func (s *Service) mergeQueueSelectionForPullRequest(ctx context.Context, tx *gorm.DB, pr db.PullRequest, lock bool) (MergeQueueTarget, []db.PullRequest, error) {
	target, err := s.mergeQueueTargetForPullRequestDB(tx, pr, lock)
	if err != nil {
		return MergeQueueTarget{}, nil, err
	}
	selected, err := s.mergeQueueSelectionForTarget(ctx, tx, pr, target, lock)
	if err != nil {
		return MergeQueueTarget{}, nil, err
	}
	return target, selected, nil
}

func (s *Service) mergeQueueSelectionForTarget(ctx context.Context, tx *gorm.DB, pr db.PullRequest, target MergeQueueTarget, lock bool) ([]db.PullRequest, error) {
	selected := []db.PullRequest{pr}
	if target.StackID == 0 {
		return selected, nil
	}
	stack, err := s.getPullRequestStackByID(ctx, tx, target.StackID, lock)
	if err != nil {
		return nil, err
	}
	selected = selectedStackPrefix(stack, pr.ID)
	if len(selected) == 0 {
		return nil, fmt.Errorf("%w: target pull request is no longer in its Stack", ErrConflict)
	}
	return selected, nil
}

func (s *Service) lockPullRequestForMergeQueueTx(tx *gorm.DB, pullRequestID uint) (db.PullRequest, error) {
	var pr db.PullRequest
	if err := preloadPRFull(tx.Clauses(clause.Locking{Strength: "UPDATE"})).First(&pr, pullRequestID).Error; err != nil {
		return db.PullRequest{}, wrapErr(err)
	}
	return pr, nil
}

func (s *Service) EnqueuePullRequest(ctx context.Context, pullRequestID uint, expectedHeadSHA string, jump bool) ([]db.MergeQueueEntry, error) {
	actor, err := s.GetCurrentUser(ctx)
	if err != nil {
		return nil, fmt.Errorf("authentication required for merge queue: %w", err)
	}
	pr, err := s.GetPRByID(ctx, pullRequestID)
	if err != nil {
		return nil, err
	}
	if pr.Merged || pr.State != db.StateOpen || pr.Draft {
		return nil, fmt.Errorf("%w: pull request must be open and not draft", ErrInvalidState)
	}
	if expectedHeadSHA != "" && expectedHeadSHA != pr.HeadSHA {
		return nil, fmt.Errorf("%w: expected head oid does not match", ErrConflict)
	}
	if err := s.requireRepoPermission(ctx, pr.RepositoryID, RepoPermissionWrite); err != nil {
		return nil, err
	}
	var entries []db.MergeQueueEntry
	var enqueuedPullRequestIDs []uint
	err = s.Transaction(ctx, func(txCtx context.Context, tx *gorm.DB) error {
		if err := lockRepoForStackTopology(tx, pr.RepositoryID); err != nil {
			return err
		}
		lockedPR, err := s.lockPullRequestForMergeQueueTx(tx, pr.ID)
		if err != nil {
			return err
		}
		if lockedPR.Merged || lockedPR.State != db.StateOpen || lockedPR.Draft {
			return fmt.Errorf("%w: pull request must be open and not draft", ErrInvalidState)
		}
		if expectedHeadSHA != "" && expectedHeadSHA != lockedPR.HeadSHA {
			return fmt.Errorf("%w: expected head oid does not match", ErrConflict)
		}
		target, selected, err := s.mergeQueueSelectionForPullRequest(txCtx, tx, lockedPR, true)
		if err != nil {
			return err
		}
		_, enabled, err := s.MergeQueueConfigurationForBranch(txCtx, lockedPR.RepositoryID, target.BaseRef)
		if err != nil {
			return err
		}
		if !enabled {
			return fmt.Errorf("%w: merge queue is not configured for %s", ErrValidation, target.BaseRef)
		}
		entries, enqueuedPullRequestIDs, err = s.enqueueAsyncMergeTx(tx, db.AsyncMergeRequest{}, lockedPR, actor, target, selected)
		if err != nil {
			return err
		}
		for i := range entries {
			if err := tx.Model(&db.MergeQueueEntry{}).Where("id = ?", entries[i].ID).
				Update("jump", jump).Error; err != nil {
				return err
			}
			entries[i].Jump = jump
		}
		if jump {
			if err := jumpMergeQueueEntriesTx(tx, entries, actor.ID); err != nil {
				return err
			}
		}
		return nil
	})
	if err == nil && len(entries) > 0 {
		if jump {
			_ = s.cleanupTerminalMergeGroupRefs(ctx, entries[0].QueueID)
		}
		s.notifyPullRequestQueueEvent(ctx, enqueuedPullRequestIDs, "enqueued")
		s.startMergeQueueProcessor(ctx, entries[0].QueueID, actor)
	}
	return entries, err
}

// DequeuePullRequest removes the requested entry and every descendant in the
// same Stack batch. Lower Stack positions remain queued.
func (s *Service) DequeuePullRequest(ctx context.Context, pullRequestID uint) ([]db.MergeQueueEntry, error) {
	actor, err := s.GetCurrentUser(ctx)
	if err != nil {
		return nil, fmt.Errorf("authentication required for merge queue: %w", err)
	}
	pr, err := s.GetPRByID(ctx, pullRequestID)
	if err != nil {
		return nil, err
	}
	if err := s.requireRepoPermission(ctx, pr.RepositoryID, RepoPermissionWrite); err != nil {
		return nil, err
	}
	_ = actor
	var removed []db.MergeQueueEntry
	var queueID uint
	err = s.DBForCtx(ctx).Transaction(func(tx *gorm.DB) error {
		var target db.MergeQueueEntry
		if err := tx.Preload("PullRequest.Repository.Owner").Preload("PullRequest.HeadRepository.Owner").
			Preload("PullRequest.Author").Preload("EnqueuedBy").
			Where("pull_request_id = ? AND state IN ?", pullRequestID, activeMergeQueueEntryStates).First(&target).Error; err != nil {
			return wrapErr(err)
		}
		queueID = target.QueueID
		q := tx.Preload("PullRequest.Repository.Owner").Preload("PullRequest.HeadRepository.Owner").
			Preload("PullRequest.Author").Preload("EnqueuedBy").Where("id = ?", target.ID)
		if target.StackBatchID != "" {
			q = tx.Preload("PullRequest.Repository.Owner").Preload("PullRequest.HeadRepository.Owner").
				Preload("PullRequest.Author").Preload("EnqueuedBy").
				Where("stack_batch_id = ? AND stack_position >= ?", target.StackBatchID, target.StackPosition)
		}
		if err := q.Order("stack_position ASC").Find(&removed).Error; err != nil {
			return err
		}
		ids := make([]uint, len(removed))
		var invalidatedRemovedIDs []uint
		for i := range removed {
			ids[i] = removed[i].ID
			if removed[i].ID != target.ID {
				invalidatedRemovedIDs = append(invalidatedRemovedIDs, removed[i].ID)
			}
		}
		now := time.Now().UTC()
		if err := terminalizeMergeGroupsForEntriesTx(tx, target.QueueID, []uint{target.ID},
			[]string{mergeGroupPublishing, mergeGroupChecksRequested, mergeGroupPassed, mergeGroupMerging, mergeGroupCascading},
			mergeGroupStale, mergeGroupDestroyedDequeued, actor.ID); err != nil {
			return err
		}
		if err := terminalizeMergeGroupsForEntriesTx(tx, target.QueueID, invalidatedRemovedIDs,
			[]string{mergeGroupPublishing, mergeGroupChecksRequested, mergeGroupPassed, mergeGroupMerging, mergeGroupCascading},
			mergeGroupStale, mergeGroupDestroyedInvalidated, actor.ID); err != nil {
			return err
		}
		if err := tx.Model(&db.MergeQueueEntry{}).Where("id IN ?", ids).Updates(map[string]any{"state": mergeQueueEntryDequeued, "updated_at": now}).Error; err != nil {
			return err
		}
		for _, entry := range removed {
			if err := tx.Create(&db.PullRequestMergeQueueEvent{
				PullRequestID: entry.PullRequestID, QueueID: entry.QueueID, QueueEntryID: entry.ID,
				ActorID: actor.ID, EventType: "removed", BeforeSHA: entry.BaseSHA,
				Reason: "removed from merge queue",
			}).Error; err != nil {
				return err
			}
		}
		if err := tx.Model(&db.AsyncMergeRequest{}).Where("merge_queue_entry_id IN ? AND status = ?", ids, AsyncMergeEnqueued).
			Updates(map[string]any{"status": AsyncMergeFailed, "failure_message": "removed from merge queue", "updated_at": now, "expires_at": now.Add(asyncMergeRetention)}).Error; err != nil {
			return err
		}
		if err := invalidateMergeQueueFromPositionTx(tx, target.QueueID, target.Position, actor.ID); err != nil {
			return err
		}
		return compactMergeQueuePositionsTx(tx, target.QueueID)
	})
	if err == nil && queueID != 0 {
		_ = s.cleanupTerminalMergeGroupRefs(ctx, queueID)
		removedPullRequestIDs := make([]uint, len(removed))
		for i := range removed {
			removedPullRequestIDs[i] = removed[i].PullRequestID
		}
		s.notifyPullRequestQueueEvent(ctx, removedPullRequestIDs, "dequeued")
		s.startMergeQueueProcessor(ctx, queueID, actor)
	}
	return removed, err
}

func (s *Service) GetMergeQueueForPullRequest(ctx context.Context, pullRequestID uint) (*MergeQueueView, *db.MergeQueueEntry, error) {
	if view, entry, found, err := s.getActiveMergeQueueForPullRequest(ctx, pullRequestID); err != nil || found {
		return view, entry, err
	}
	pr, err := s.GetPRByID(ctx, pullRequestID)
	if err != nil {
		return nil, nil, err
	}
	target, err := s.MergeQueueTargetForPullRequest(ctx, pr)
	if err != nil {
		return nil, nil, err
	}
	return s.getOrCreateMergeQueueForPullRequestTarget(ctx, pr, target)
}

// GetMergeQueueForPullRequestAtTarget reuses a target already resolved by a
// transport that also needs merge-queue configuration. This avoids resolving
// and hydrating the same Stack repeatedly while active entries still remain
// authoritative through their persisted QueueID.
func (s *Service) GetMergeQueueForPullRequestAtTarget(ctx context.Context, pr db.PullRequest, target MergeQueueTarget) (*MergeQueueView, *db.MergeQueueEntry, error) {
	if view, entry, found, err := s.getActiveMergeQueueForPullRequest(ctx, pr.ID); err != nil || found {
		return view, entry, err
	}
	return s.getOrCreateMergeQueueForPullRequestTarget(ctx, pr, target)
}

func (s *Service) getActiveMergeQueueForPullRequest(ctx context.Context, pullRequestID uint) (*MergeQueueView, *db.MergeQueueEntry, bool, error) {
	var entry db.MergeQueueEntry
	err := s.DBForCtx(ctx).Preload("PullRequest.Repository.Owner").Preload("PullRequest.HeadRepository.Owner").Preload("PullRequest.Author").Preload("EnqueuedBy").
		Where("pull_request_id = ? AND state IN ?", pullRequestID, activeMergeQueueEntryStates).First(&entry).Error
	if err == nil {
		view, err := s.GetMergeQueue(ctx, entry.QueueID)
		if err != nil {
			return nil, nil, false, err
		}
		return &view, &entry, true, nil
	}
	if !errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, nil, false, err
	}
	return nil, nil, false, nil
}

func (s *Service) getOrCreateMergeQueueForPullRequestTarget(ctx context.Context, pr db.PullRequest, target MergeQueueTarget) (*MergeQueueView, *db.MergeQueueEntry, error) {
	_, enabled, err := s.MergeQueueConfigurationForBranch(ctx, pr.RepositoryID, target.BaseRef)
	if err != nil || !enabled {
		return nil, nil, err
	}
	queue := db.MergeQueue{RepositoryID: pr.RepositoryID, BaseRef: target.BaseRef}
	if err := s.DBForCtx(ctx).Clauses(clause.OnConflict{DoNothing: true}).Create(&queue).Error; err != nil {
		return nil, nil, err
	}
	if err := s.DBForCtx(ctx).Where("repository_id = ? AND base_ref = ?", pr.RepositoryID, target.BaseRef).First(&queue).Error; err != nil {
		return nil, nil, err
	}
	view, err := s.GetMergeQueue(ctx, queue.ID)
	if err != nil {
		return nil, nil, err
	}
	return &view, nil, nil
}

func (s *Service) GetMergeQueue(ctx context.Context, queueID uint) (MergeQueueView, error) {
	var queue db.MergeQueue
	if err := s.DBForCtx(ctx).Preload("Repository.Owner").First(&queue, queueID).Error; err != nil {
		return MergeQueueView{}, wrapErr(err)
	}
	if err := s.requireRepoPermission(ctx, queue.RepositoryID, RepoPermissionRead); err != nil {
		return MergeQueueView{}, err
	}
	var entries []db.MergeQueueEntry
	if err := s.DBForCtx(ctx).Where("queue_id = ? AND state IN ?", queue.ID, activeMergeQueueEntryStates).
		Order("position ASC").Preload("PullRequest.Repository.Owner").Preload("PullRequest.HeadRepository.Owner").Preload("PullRequest.Author").Preload("EnqueuedBy").Find(&entries).Error; err != nil {
		return MergeQueueView{}, err
	}
	config, _, err := s.MergeQueueConfigurationForBranch(ctx, queue.RepositoryID, queue.BaseRef)
	if err != nil {
		return MergeQueueView{}, err
	}
	return MergeQueueView{Queue: queue, Configuration: config, Entries: entries}, nil
}

func jumpMergeQueueEntriesTx(tx *gorm.DB, jumped []db.MergeQueueEntry, actorID uint) error {
	if len(jumped) == 0 {
		return nil
	}
	queueID := jumped[0].QueueID
	var locked int64
	if err := tx.Model(&db.MergeQueueEntry{}).Where("queue_id = ? AND state = ?", queueID, mergeQueueEntryLocked).Count(&locked).Error; err != nil {
		return err
	}
	if locked > 0 {
		return fmt.Errorf("%w: merge queue is currently merging", ErrConflict)
	}
	var active []db.MergeQueueEntry
	if err := tx.Where("queue_id = ? AND state IN ?", queueID, activeMergeQueueEntryStates).Order("position ASC").Find(&active).Error; err != nil {
		return err
	}
	jumpedIDs := make(map[uint]bool, len(jumped))
	ordered := make([]db.MergeQueueEntry, 0, len(active))
	for _, entry := range jumped {
		jumpedIDs[entry.ID] = true
		ordered = append(ordered, entry)
	}
	for _, entry := range active {
		if !jumpedIDs[entry.ID] {
			ordered = append(ordered, entry)
		}
	}
	activeIDs := make([]uint, len(active))
	for i := range active {
		activeIDs[i] = active[i].ID
	}
	if err := terminalizeMergeGroupsForEntriesTx(tx, queueID, activeIDs,
		[]string{mergeGroupPublishing, mergeGroupChecksRequested, mergeGroupPassed},
		mergeGroupStale, mergeGroupDestroyedInvalidated, actorID); err != nil {
		return err
	}
	if err := tx.Model(&db.MergeQueueEntry{}).Where("queue_id = ? AND state IN ?", queueID, activeMergeQueueEntryStates).
		Updates(map[string]any{"state": mergeQueueEntryQueued, "merge_group_id": nil, "updated_at": time.Now().UTC()}).Error; err != nil {
		return err
	}
	for i := range ordered {
		if err := tx.Model(&db.MergeQueueEntry{}).Where("id = ?", ordered[i].ID).Update("position", i+1).Error; err != nil {
			return err
		}
	}
	return nil
}

func invalidateMergeQueueFromPositionTx(tx *gorm.DB, queueID uint, position int, actorID uint) error {
	var entries []db.MergeQueueEntry
	if err := tx.Where("queue_id = ? AND position >= ? AND state IN ?", queueID, position, activeMergeQueueEntryStates).
		Find(&entries).Error; err != nil {
		return err
	}
	if len(entries) == 0 {
		return nil
	}
	ids := make([]uint, len(entries))
	for i := range entries {
		ids[i] = entries[i].ID
	}
	if err := terminalizeMergeGroupsForEntriesTx(tx, queueID, ids,
		[]string{mergeGroupPublishing, mergeGroupChecksRequested, mergeGroupPassed},
		mergeGroupStale, mergeGroupDestroyedInvalidated, actorID); err != nil {
		return err
	}
	return tx.Model(&db.MergeQueueEntry{}).Where("id IN ?", ids).
		Updates(map[string]any{"state": mergeQueueEntryQueued, "merge_group_id": nil, "updated_at": time.Now().UTC()}).Error
}

func (s *Service) GetMergeQueueEntryByPullRequest(ctx context.Context, pullRequestID uint) (*db.MergeQueueEntry, error) {
	_, entry, err := s.GetMergeQueueForPullRequest(ctx, pullRequestID)
	return entry, err
}

func (s *Service) ListPullRequestMergeQueueEvents(ctx context.Context, pullRequestID uint) ([]db.PullRequestMergeQueueEvent, error) {
	var events []db.PullRequestMergeQueueEvent
	err := s.DBForCtx(ctx).Where("pull_request_id = ?", pullRequestID).Order("created_at ASC").
		Preload("Actor").Preload("Queue.Repository.Owner").Preload("PullRequest.Repository.Owner").
		Preload("PullRequest.HeadRepository.Owner").Preload("PullRequest.Author").Find(&events).Error
	return events, err
}
