package service

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"log/slog"
	"strings"
	"time"

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

type SetPRAutoMergeInput struct {
	Enabled         bool
	MergeMethod     string
	CommitHeadline  string
	CommitBody      string
	AuthorEmail     string
	ExpectedHeadSHA string
}

type branchProtectionRequiredStatusChecks struct {
	Strict   bool                         `json:"strict"`
	Contexts []string                     `json:"contexts"`
	Checks   []branchProtectionCheckEntry `json:"checks"`
}

type branchProtectionCheckEntry struct {
	Context string `json:"context"`
	Name    string `json:"name"`
	AppID   *int64 `json:"app_id"`
}

// rulesetRequiredStatusChecks is the GitHub repository-ruleset shape. The
// effective policy for a branch is the union of every matching active ruleset
// and the legacy branch-protection rule.
type rulesetRequiredStatusChecks struct {
	RequiredStatusChecks []struct {
		Context       string `json:"context"`
		IntegrationID *int64 `json:"integration_id"`
	} `json:"required_status_checks"`
	StrictRequiredStatusChecksPolicy bool `json:"strict_required_status_checks_policy"`
	DoNotEnforceOnCreate             bool `json:"do_not_enforce_on_create"`
}

type branchProtectionRequiredReviews struct {
	RequiredApprovingReviewCount int                              `json:"required_approving_review_count"`
	BypassPullRequestAllowances  branchProtectionBypassAllowances `json:"bypass_pull_request_allowances"`
}

type branchProtectionBypassAllowances struct {
	Users []string `json:"users"`
	Teams []string `json:"teams"`
	Apps  []string `json:"apps"`
}

type mergeStatusState struct {
	Passed    bool
	Completed bool
	UpdatedAt time.Time
}

type requiredStatusCheck struct {
	Context       string
	IntegrationID *int64
}

func (s *Service) SetPRAutoMerge(ctx context.Context, prID uint, in SetPRAutoMergeInput) (db.PullRequest, error) {
	currentUser, err := s.GetCurrentUser(ctx)
	if err != nil {
		return db.PullRequest{}, fmt.Errorf("authentication required for auto-merge: %w", err)
	}

	pr, err := s.GetPRByID(ctx, prID)
	if err != nil {
		return db.PullRequest{}, err
	}
	if pr.Merged || pr.State != db.StateOpen {
		return db.PullRequest{}, fmt.Errorf("%w: pull request is not open", ErrInvalidState)
	}
	if stack, stackErr := s.GetPullRequestStackForPR(ctx, pr.ID); stackErr != nil {
		return db.PullRequest{}, stackErr
	} else if stack != nil && in.Enabled {
		return db.PullRequest{}, fmt.Errorf("%w: auto-merge is not available for stacked pull requests", ErrInvalidState)
	}

	canMerge, err := s.CanCreatePR(ctx, pr.RepositoryID, currentUser.ID)
	if err != nil {
		return db.PullRequest{}, err
	}
	if !canMerge {
		return db.PullRequest{}, fmt.Errorf("%w: auto-merge requires repository write access", ErrForbidden)
	}

	updates := map[string]any{
		"auto_merge":                   in.Enabled,
		"auto_merge_method":            "",
		"auto_merge_commit_headline":   "",
		"auto_merge_commit_body":       "",
		"auto_merge_author_email":      "",
		"auto_merge_expected_head_sha": "",
		"auto_merge_enabled_by_login":  "",
	}

	if in.Enabled {
		if !pr.Repository.AllowAutoMerge {
			return db.PullRequest{}, fmt.Errorf("%w: auto-merge is not allowed for this repository", ErrInvalidState)
		}
		if expected := strings.TrimSpace(in.ExpectedHeadSHA); expected != "" && !strings.EqualFold(expected, pr.HeadSHA) {
			return db.PullRequest{}, fmt.Errorf("%w: expected head oid does not match the current pull request head", ErrInvalidState)
		}

		mergeMethod := strings.ToUpper(strings.TrimSpace(in.MergeMethod))
		if mergeMethod == "" {
			mergeMethod = "MERGE"
		}
		updates["auto_merge_method"] = mergeMethod
		updates["auto_merge_commit_headline"] = strings.TrimSpace(in.CommitHeadline)
		updates["auto_merge_commit_body"] = strings.TrimSpace(in.CommitBody)
		updates["auto_merge_author_email"] = strings.TrimSpace(in.AuthorEmail)
		updates["auto_merge_expected_head_sha"] = strings.TrimSpace(in.ExpectedHeadSHA)
		updates["auto_merge_enabled_by_login"] = currentUser.Login
	}

	if err := s.UpdatePRFields(ctx, prID, updates); err != nil {
		return db.PullRequest{}, err
	}
	return s.GetPRByID(ctx, prID)
}

func (s *Service) enforceMergePolicy(ctx context.Context, currentUser db.User, pr *db.PullRequest) error {
	if pr == nil {
		return fmt.Errorf("%w: pull request is required", ErrValidation)
	}

	canMerge, err := s.CanCreatePR(ctx, pr.RepositoryID, currentUser.ID)
	if err != nil {
		return err
	}
	if !canMerge {
		return fmt.Errorf("%w: merge requires repository write access", ErrForbidden)
	}

	bp, err := s.GetBranchProtection(ctx, pr.RepositoryID, pr.BaseRef)
	if err != nil {
		if errors.Is(err, ErrNotFound) {
			return nil
		}
		return err
	}
	return s.enforceBranchProtectionForMerge(ctx, currentUser, *pr, *bp)
}

// enforceStackMergePolicy applies the ultimate-base policy without any review
// or administrator bypass. GitHub intentionally disables rule bypass for
// Stack merges so a selected prefix cannot create mixed policy outcomes.
func (s *Service) enforceStackMergePolicy(ctx context.Context, currentUser db.User, pr *db.PullRequest) error {
	if pr == nil {
		return fmt.Errorf("%w: pull request is required", ErrValidation)
	}
	canMerge, err := s.CanCreatePR(ctx, pr.RepositoryID, currentUser.ID)
	if err != nil {
		return err
	}
	if !canMerge {
		return fmt.Errorf("%w: merge requires repository write access", ErrForbidden)
	}
	bp, err := s.GetBranchProtection(ctx, pr.RepositoryID, pr.BaseRef)
	if errors.Is(err, ErrNotFound) {
		return nil
	}
	if err != nil {
		return err
	}
	reviews, err := decodeBranchProtectionRequiredReviews(bp.RequiredPullRequestJSON)
	if err != nil {
		return fmt.Errorf("%w: invalid required pull request reviews configuration", ErrInvalidState)
	}
	reviews.BypassPullRequestAllowances = branchProtectionBypassAllowances{}
	encoded, err := json.Marshal(reviews)
	if err != nil {
		return err
	}
	bp.RequiredPullRequestJSON = string(encoded)
	bp.EnforceAdmins = true
	currentUser.SiteAdmin = false
	return s.enforceBranchProtectionForMerge(ctx, currentUser, *pr, *bp)
}

func (s *Service) enforceBranchProtectionForMerge(ctx context.Context, currentUser db.User, pr db.PullRequest, bp db.BranchProtection) error {
	requiredReviews, err := decodeBranchProtectionRequiredReviews(bp.RequiredPullRequestJSON)
	if err != nil {
		return fmt.Errorf("%w: invalid required pull request reviews configuration", ErrInvalidState)
	}
	if !bp.EnforceAdmins && currentUser.SiteAdmin {
		return nil
	}

	bypassReviews := isBranchProtectionBypassUser(currentUser.Login, requiredReviews.BypassPullRequestAllowances.Users)
	if requiredReviews.RequiredApprovingReviewCount > 0 && !bypassReviews {
		approvals, changesRequested, err := s.currentPRReviewState(ctx, pr.ID)
		if err != nil {
			return err
		}
		if changesRequested {
			return fmt.Errorf("%w: branch protection blocks merge while changes are requested", ErrInvalidState)
		}
		if approvals < requiredReviews.RequiredApprovingReviewCount {
			return fmt.Errorf("%w: branch protection requires %d approving review(s)", ErrInvalidState, requiredReviews.RequiredApprovingReviewCount)
		}
	}

	requiredChecks, err := decodeBranchProtectionRequiredStatusChecks(bp.RequiredStatusChecksJSON)
	if err != nil {
		return fmt.Errorf("%w: invalid required status checks configuration", ErrInvalidState)
	}
	if requiredChecks.Strict {
		return fmt.Errorf("%w: strict required status checks are not supported", ErrInvalidState)
	}
	resolvedChecks, err := resolvedBranchProtectionStatusChecks(bp, requiredChecks)
	if err != nil {
		return err
	}
	if err := s.enforceRequiredStatusChecks(ctx, pr, resolvedChecks); err != nil {
		return err
	}
	return nil
}

func resolvedBranchProtectionStatusChecks(bp db.BranchProtection, fallback branchProtectionRequiredStatusChecks) (branchProtectionRequiredStatusChecks, error) {
	if bp.RequiredStatusChecksProviderBindingPending {
		return branchProtectionRequiredStatusChecks{}, fmt.Errorf("%w: required status check provider binding must be rewritten", ErrInvalidState)
	}
	if strings.TrimSpace(bp.ResolvedRequiredStatusChecksJSON) == "" {
		if len(fallback.Contexts) == 0 && len(fallback.Checks) == 0 {
			return fallback, nil
		}
		return branchProtectionRequiredStatusChecks{}, fmt.Errorf("%w: required status check provider binding is missing", ErrInvalidState)
	}
	resolved, err := decodeBranchProtectionRequiredStatusChecks(bp.ResolvedRequiredStatusChecksJSON)
	if err != nil {
		return branchProtectionRequiredStatusChecks{}, fmt.Errorf("%w: invalid required status check provider binding", ErrInvalidState)
	}
	return resolved, nil
}

func decodeBranchProtectionRequiredReviews(raw string) (branchProtectionRequiredReviews, error) {
	var parsed branchProtectionRequiredReviews
	if strings.TrimSpace(raw) == "" {
		return parsed, nil
	}
	if err := json.Unmarshal([]byte(raw), &parsed); err != nil {
		return parsed, err
	}
	return parsed, nil
}

func decodeBranchProtectionRequiredStatusChecks(raw string) (branchProtectionRequiredStatusChecks, error) {
	var parsed branchProtectionRequiredStatusChecks
	if strings.TrimSpace(raw) == "" {
		return parsed, nil
	}
	if err := json.Unmarshal([]byte(raw), &parsed); err != nil {
		return parsed, err
	}
	return parsed, nil
}

func isBranchProtectionBypassUser(login string, users []string) bool {
	for _, candidate := range users {
		if strings.EqualFold(strings.TrimSpace(candidate), login) {
			return true
		}
	}
	return false
}

func (s *Service) currentPRReviewState(ctx context.Context, prID uint) (approvals int, changesRequested bool, err error) {
	var reviews []db.PullRequestReview
	err = s.DBForCtx(ctx).
		Where("pull_request_id = ?", prID).
		Order("created_at asc").
		Find(&reviews).Error
	if err != nil {
		return 0, false, err
	}

	latestByAuthor := make(map[string]db.PullRequestReview)
	for _, review := range reviews {
		if review.AuthorLogin == "" {
			continue
		}
		latestByAuthor[review.AuthorLogin] = review
	}
	for _, review := range latestByAuthor {
		switch review.State {
		case db.ReviewApproved:
			approvals++
		case db.ReviewChangesRequested:
			changesRequested = true
		}
	}
	return approvals, changesRequested, nil
}

func (s *Service) enforceRequiredStatusChecks(ctx context.Context, pr db.PullRequest, required branchProtectionRequiredStatusChecks) error {
	checks := collectRequiredStatusChecks(required)
	if len(checks) == 0 {
		return nil
	}

	statuses, err := s.latestStatusChecksForPR(ctx, pr)
	if err != nil {
		return err
	}
	for _, check := range checks {
		state, ok := statuses[requiredStatusCheckKey(check.Context, check.IntegrationID)]
		if !ok || !state.Completed || !state.Passed {
			return fmt.Errorf("%w: required status checks have not passed", ErrInvalidState)
		}
	}
	return nil
}

func collectRequiredStatusChecks(required branchProtectionRequiredStatusChecks) []requiredStatusCheck {
	seen := make(map[string]struct{})
	var checks []requiredStatusCheck
	add := func(name string, integrationID *int64) {
		name = strings.TrimSpace(name)
		if name == "" {
			return
		}
		key := requiredStatusCheckKey(name, integrationID)
		if _, ok := seen[key]; ok {
			return
		}
		seen[key] = struct{}{}
		checks = append(checks, requiredStatusCheck{Context: name, IntegrationID: integrationID})
	}
	for _, name := range required.Contexts {
		add(name, nil)
	}
	for _, check := range required.Checks {
		appID := check.AppID
		// GitHub documents -1 as the explicit "any app" sentinel for
		// legacy branch-protection checks.
		if appID != nil && *appID == -1 {
			appID = nil
		}
		if check.Context != "" {
			add(check.Context, appID)
			continue
		}
		add(check.Name, appID)
	}
	return checks
}

func requiredStatusCheckKey(contextName string, integrationID *int64) string {
	contextName = strings.TrimSpace(contextName)
	if integrationID == nil {
		return contextName + "\x00*"
	}
	return fmt.Sprintf("%s\x00%d", contextName, *integrationID)
}

func (s *Service) effectiveRequiredStatusChecks(ctx context.Context, repositoryID uint, branch string) ([]requiredStatusCheck, error) {
	seen := make(map[string]struct{})
	checks := make([]requiredStatusCheck, 0)
	add := func(name string, integrationID *int64) {
		name = strings.TrimSpace(name)
		if name == "" {
			return
		}
		key := requiredStatusCheckKey(name, integrationID)
		if _, ok := seen[key]; ok {
			return
		}
		seen[key] = struct{}{}
		checks = append(checks, requiredStatusCheck{Context: name, IntegrationID: integrationID})
	}

	bp, err := s.GetBranchProtection(ctx, repositoryID, branch)
	if err == nil {
		required, decodeErr := decodeBranchProtectionRequiredStatusChecks(bp.RequiredStatusChecksJSON)
		if decodeErr != nil {
			return nil, fmt.Errorf("%w: invalid required status checks configuration", ErrInvalidState)
		}
		resolved, resolveErr := resolvedBranchProtectionStatusChecks(*bp, required)
		if resolveErr != nil {
			return nil, resolveErr
		}
		for _, check := range collectRequiredStatusChecks(resolved) {
			add(check.Context, check.IntegrationID)
		}
	} else if !errors.Is(err, ErrNotFound) {
		return nil, err
	}

	var repository db.Repository
	if err := s.DBForCtx(ctx).Select("id", "default_branch").First(&repository, repositoryID).Error; err != nil {
		return nil, wrapErr(err)
	}
	var rulesets []db.Ruleset
	if err := s.DBForCtx(ctx).Where("repository_id = ? AND target = ? AND enforcement = ?", repositoryID, "branch", "active").Find(&rulesets).Error; err != nil {
		return nil, err
	}
	for _, ruleset := range rulesets {
		if !rulesetMatchesBranch(ruleset.ConditionsJSON, branch, repository.DefaultBranch) {
			continue
		}
		var rules []struct {
			Type       string                      `json:"type"`
			Parameters rulesetRequiredStatusChecks `json:"parameters"`
		}
		if err := json.Unmarshal([]byte(ruleset.RulesJSON), &rules); err != nil {
			return nil, fmt.Errorf("%w: invalid ruleset %d", ErrInvalidState, ruleset.ID)
		}
		for _, rule := range rules {
			if rule.Type != "required_status_checks" {
				continue
			}
			for _, check := range rule.Parameters.RequiredStatusChecks {
				add(check.Context, check.IntegrationID)
			}
		}
	}
	return checks, nil
}

func (s *Service) latestStatusChecksForPR(ctx context.Context, pr db.PullRequest) (map[string]mergeStatusState, error) {
	states := make(map[string]mergeStatusState)

	runs, err := s.ListWorkflowRunsBySHA(ctx, pr.RepositoryID, pr.HeadSHA)
	if err != nil {
		return nil, err
	}
	for _, run := range runs {
		name := strings.TrimSpace(run.Name)
		if name == "" {
			continue
		}
		state := mergeStatusState{
			Passed:    run.Status == db.RunCompleted && run.Conclusion == db.ConclusionSuccess,
			Completed: run.Status == db.RunCompleted,
			UpdatedAt: run.UpdatedAt,
		}
		recordLatestProviderStatus(states, name, run.IntegrationID, state)
	}

	commitStatuses, err := s.ListCommitStatuses(ctx, pr.RepositoryID, pr.HeadSHA)
	if err != nil {
		return nil, err
	}
	for _, status := range commitStatuses {
		name := strings.TrimSpace(status.Context)
		if name == "" {
			continue
		}
		state := mergeStatusState{
			Passed:    status.State == "success",
			Completed: status.State != "pending",
			UpdatedAt: status.UpdatedAt,
		}
		recordLatestProviderStatus(states, name, status.IntegrationID, state)
	}
	return states, nil
}

func recordLatestProviderStatus(states map[string]mergeStatusState, name string, integrationID *int64, candidate mergeStatusState) {
	// Provider-bound requirements only observe that provider. Unbound
	// requirements observe the newest result for the context from any source.
	recordLatestStatus(states, requiredStatusCheckKey(name, integrationID), candidate)
	recordLatestStatus(states, requiredStatusCheckKey(name, nil), candidate)
}

func recordLatestStatus(states map[string]mergeStatusState, name string, candidate mergeStatusState) {
	if existing, ok := states[name]; ok && !candidate.UpdatedAt.After(existing.UpdatedAt) {
		return
	}
	states[name] = candidate
}

func (s *Service) ReevaluateAutoMergeForSHA(ctx context.Context, repoID uint, sha string) {
	sha = strings.TrimSpace(sha)
	if sha == "" {
		return
	}

	var prs []db.PullRequest
	if err := preloadPRFull(s.DBForCtx(ctx)).
		Where("repository_id = ? AND head_sha = ? AND auto_merge = ? AND merged = false AND state = ?", repoID, sha, true, db.StateOpen).
		Find(&prs).Error; err != nil {
		slog.WarnContext(ctx, "auto-merge candidate lookup failed", "repo_id", repoID, "head_sha", sha, "error", err)
		return
	}
	for i := range prs {
		if err := s.tryAutoMergePR(ctx, &prs[i]); err != nil &&
			!errors.Is(err, ErrInvalidState) &&
			!errors.Is(err, ErrForbidden) &&
			!errors.Is(err, ErrConflict) {
			slog.WarnContext(ctx, "auto-merge evaluation failed", "pr_id", prs[i].ID, "pr_number", prs[i].Number, "error", err)
		}
	}
}

func (s *Service) tryAutoMergePR(ctx context.Context, pr *db.PullRequest) error {
	if pr == nil || !pr.AutoMerge || pr.Merged || pr.State != db.StateOpen {
		return nil
	}
	if !pr.Repository.AllowAutoMerge {
		return fmt.Errorf("%w: auto-merge is not allowed for this repository", ErrInvalidState)
	}
	if expected := strings.TrimSpace(pr.AutoMergeExpectedHeadSHA); expected != "" && !strings.EqualFold(expected, pr.HeadSHA) {
		return fmt.Errorf("%w: expected head oid does not match the current pull request head", ErrInvalidState)
	}

	actor, err := s.autoMergeActor(ctx, *pr)
	if err != nil {
		return err
	}
	if err := s.enforceBranchProtectionAndAutoMergePolicy(ctx, actor, *pr); err != nil {
		return err
	}

	commitMsg := strings.TrimSpace(pr.AutoMergeCommitHeadline)
	if body := strings.TrimSpace(string(pr.AutoMergeCommitBody)); body != "" {
		if commitMsg != "" {
			commitMsg += "\n\n" + body
		} else {
			commitMsg = body
		}
	}
	return s.mergePRRecordWithActor(ctx, actor, pr, strings.ToLower(pr.AutoMergeMethod), commitMsg)
}

func (s *Service) enforceBranchProtectionAndAutoMergePolicy(ctx context.Context, actor db.User, pr db.PullRequest) error {
	if err := s.enforceMergePolicy(ctx, actor, &pr); err != nil {
		return err
	}
	return nil
}

func (s *Service) autoMergeActor(ctx context.Context, pr db.PullRequest) (db.User, error) {
	login := strings.TrimSpace(pr.AutoMergeEnabledByLogin)
	if login == "" {
		login = pr.Author.Login
	}
	if login == "" {
		return db.User{}, fmt.Errorf("%w: auto-merge actor is not available", ErrInvalidState)
	}
	actor, err := s.GetUser(ctx, login)
	if err != nil {
		return db.User{}, err
	}
	if email := strings.TrimSpace(pr.AutoMergeAuthorEmail); email != "" {
		actor.Email = email
	}
	return actor, nil
}
