package service

import (
	"context"
	"errors"
	"fmt"

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

// ComparePR returns the ahead/behind/commit diff between two refs on a repo.
// Thin pass-through over gitstore.Compare — exists so GraphQL resolvers can
// call the service instead of reaching into Svc.Git directly (the documented
// tech-debt coupling in docs/module-contracts.md).
func (s *Service) ComparePR(ctx context.Context, repoFullName, base, head string) (gitstore.DiffResult, error) {
	return s.Git.Compare(ctx, repoFullName, base, head)
}

// CanMergePR returns the GitHub-style mergeability token for a PR's
// base/head refs ("MERGEABLE", "CONFLICTING", "UNKNOWN"). Non-error return
// matches gitstore's signature — merge conflicts are not errors here.
// A nil gitstore collapses to "UNKNOWN" so callers don't need their own
// guard; mirrors the same pattern in service/pr_merge.go.
func (s *Service) CanMergePR(ctx context.Context, repoFullName, baseBranch, headBranch string) string {
	if s.Git == nil {
		return "UNKNOWN"
	}
	return s.Git.CanMerge(ctx, repoFullName, baseBranch, headBranch)
}

// SimulatePRMerge performs a dry-run three-way merge and returns the
// resulting tree's SHA, used to populate GraphQL's potentialMergeCommit.
func (s *Service) SimulatePRMerge(ctx context.Context, repoFullName, baseSHA, headSHA string) (string, error) {
	repository, err := s.GetRepo(ctx, repoFullName)
	if err != nil {
		return "", err
	}
	if s.repositoryMutations == nil {
		return "", ErrRepositoryMigrationConflict
	}
	session, err := s.repositoryMutations.AcquireSession(ctx, repository.ID, repository.FullName)
	if err != nil {
		return "", repositoryMutationError(err)
	}
	defer func() { _ = session.Release() }()
	sha, err := session.SimulateMerge(ctx, baseSHA, headSHA)
	return sha, repositoryMutationError(err)
}

// UpdatePRBranch merges (or rebases) the PR's base into its head branch.
// Returns the new head SHA. Caller should persist this on the PR record.
func (s *Service) UpdatePRBranch(ctx context.Context, opts gitstore.UpdatePRBranchOptions) (string, error) {
	return s.UpdatePRBranchAndRecord(ctx, 0, opts)
}

// UpdatePRBranchAndRecord keeps the ref update and its PR head projection
// inside one stable-ordered private session set. A zero prID omits the projection
// for internal callers that do not own a PR row.
func (s *Service) UpdatePRBranchAndRecord(ctx context.Context, prID uint, opts gitstore.UpdatePRBranchOptions) (string, error) {
	if s == nil || s.repositoryMutations == nil {
		return "", ErrRepositoryMigrationConflict
	}
	headRepository, err := s.GetRepo(ctx, opts.FullName)
	if err != nil {
		return "", err
	}
	repositories := []db.Repository{headRepository}
	baseRepositoryID := headRepository.ID
	if prID != 0 {
		var pullRequest db.PullRequest
		if err := s.DBForCtx(ctx).Select("id", "repository_id", "head_repository_id").First(&pullRequest, prID).Error; err != nil {
			return "", wrapErr(err)
		}
		headRepositoryID := pullRequest.HeadRepositoryID
		if headRepositoryID == 0 {
			headRepositoryID = pullRequest.RepositoryID
		}
		if headRepositoryID != headRepository.ID {
			return "", ErrRepositoryMigrationConflict
		}
		var baseRepository db.Repository
		if err := s.DBForCtx(ctx).Select("id", "full_name").First(&baseRepository, pullRequest.RepositoryID).Error; err != nil {
			return "", wrapErr(err)
		}
		baseRepositoryID = baseRepository.ID
		repositories = append(repositories, baseRepository)
	}
	sessions, release, err := s.acquireOrderedRepositoryMutationSessions(ctx, repositories...)
	if err != nil {
		return "", err
	}
	defer release()
	headSession := sessions[headRepository.ID]
	baseSession := sessions[baseRepositoryID]
	if headSession == nil {
		return "", ErrRepositoryMigrationConflict
	}
	if baseSession == nil {
		return "", ErrRepositoryMigrationConflict
	}
	sha, rollback, err := headSession.UpdatePRBranch(ctx, baseSession, opts)
	if err != nil {
		return "", repositoryMutationError(err)
	}
	if prID == 0 {
		return sha, nil
	}
	result := s.DBForCtx(ctx).Model(&db.PullRequest{}).
		Where("id = ? AND repository_id = ?", prID, baseRepositoryID).
		Update("head_sha", sha)
	projectionErr := result.Error
	if projectionErr == nil && result.RowsAffected != 1 {
		projectionErr = ErrRepositoryMigrationConflict
	}
	if projectionErr != nil {
		restoreErr := headSession.RestoreBranchUpdate(context.WithoutCancel(ctx), rollback)
		if restoreErr != nil {
			projectionErr = errors.Join(projectionErr, fmt.Errorf("restore pull request head branch: %w", repositoryMutationError(restoreErr)))
		}
		return "", fmt.Errorf("update pull request head projection: %w", projectionErr)
	}
	return sha, nil
}

// RevertPRMerge creates a revert branch on the repo. Used by the
// revertPullRequest GraphQL mutation; the resulting branch is then handed
// to CreatePR to build the revert PR.
func (s *Service) RevertPRMerge(ctx context.Context, repoFullName, baseBranch, mergeCommitSHA, revertBranchName string) (string, error) {
	repository, err := s.GetRepo(ctx, repoFullName)
	if err != nil {
		return "", err
	}
	if s.repositoryMutations == nil {
		return "", ErrRepositoryMigrationConflict
	}
	session, err := s.repositoryMutations.AcquireSession(ctx, repository.ID, repository.FullName)
	if err != nil {
		return "", repositoryMutationError(err)
	}
	defer func() { _ = session.Release() }()
	sha, err := session.RevertMerge(ctx, baseBranch, mergeCommitSHA, revertBranchName)
	return sha, repositoryMutationError(err)
}
