package service

import (
	"context"
	"encoding/json"
	"fmt"
	"strings"
	"time"

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

type directStackMergePlan struct {
	RepositoryFullName  string                       `json:"repository_full_name"`
	BaseRef             string                       `json:"base_ref"`
	BaseSHA             string                       `json:"base_sha"`
	TargetPullRequestID uint                         `json:"target_pull_request_id"`
	ExpectedFinalSHA    string                       `json:"expected_final_sha"`
	Members             []directStackMergePlanMember `json:"members"`
}

type directStackMergePlanMember struct {
	PullRequestID   uint   `json:"pull_request_id"`
	Number          int    `json:"number"`
	BaseBranch      string `json:"base_branch"`
	HeadBranch      string `json:"head_branch"`
	ExpectedHeadSHA string `json:"expected_head_sha"`
	ExpectedTreeSHA string `json:"expected_tree_sha"`
	Message         string `json:"message"`
}

func (p directStackMergePlan) gitMembers() []gitstore.StackMergeMember {
	members := make([]gitstore.StackMergeMember, len(p.Members))
	for i := range p.Members {
		members[i] = gitstore.StackMergeMember{
			Number: p.Members[i].Number, BaseBranch: p.Members[i].BaseBranch,
			HeadBranch: p.Members[i].HeadBranch, ExpectedHeadSHA: p.Members[i].ExpectedHeadSHA,
			Message: p.Members[i].Message,
		}
	}
	return members
}

func (s *Service) prepareDirectStackMergePlan(ctx context.Context, req db.AsyncMergeRequest, pr db.PullRequest, baseRef string, selected []db.PullRequest) (directStackMergePlan, error) {
	plan := directStackMergePlan{
		RepositoryFullName: pr.Repository.FullName, BaseRef: baseRef,
		TargetPullRequestID: pr.ID, Members: make([]directStackMergePlanMember, len(selected)),
	}
	gitStore, err := s.GitForCtx(ctx)
	if err != nil {
		return plan, err
	}
	baseSHA, err := gitStore.HeadSHA(ctx, pr.Repository.FullName, baseRef)
	if err != nil {
		return plan, err
	}
	plan.BaseSHA = baseSHA
	current := baseSHA
	for i := range selected {
		simulated, err := gitStore.SimulateMerge(ctx, pr.Repository.FullName, current, selected[i].HeadSHA)
		if err != nil {
			return plan, fmt.Errorf("simulate Stack member #%d: %w", selected[i].Number, err)
		}
		commit, err := gitStore.GetCommit(ctx, pr.Repository.FullName, simulated)
		if err != nil {
			return plan, err
		}
		plan.Members[i] = directStackMergePlanMember{
			PullRequestID: selected[i].ID, Number: selected[i].Number,
			BaseBranch: selected[i].BaseRef, HeadBranch: selected[i].HeadRef,
			ExpectedHeadSHA: selected[i].HeadSHA, ExpectedTreeSHA: commit.TreeSHA,
			Message: asyncMergeCommitMessage(req, selected[i].Number),
		}
		current = simulated
	}
	plan.ExpectedFinalSHA = current
	return plan, nil
}

func decodeDirectStackMergePlan(req db.AsyncMergeRequest) (directStackMergePlan, error) {
	var plan directStackMergePlan
	if strings.TrimSpace(string(req.MergePlanJSON)) == "" {
		return plan, fmt.Errorf("direct Stack merge request %d has no durable merge plan", req.ID)
	}
	if err := json.Unmarshal([]byte(req.MergePlanJSON), &plan); err != nil {
		return plan, err
	}
	if plan.BaseSHA == "" || plan.BaseRef == "" || len(plan.Members) == 0 {
		return plan, fmt.Errorf("direct Stack merge request %d has an incomplete merge plan", req.ID)
	}
	return plan, nil
}

func (s *Service) loadDirectStackMergePlanPRs(ctx context.Context, plan directStackMergePlan) ([]db.PullRequest, error) {
	prs := make([]db.PullRequest, len(plan.Members))
	for i := range plan.Members {
		pr, err := s.GetPRByID(ctx, plan.Members[i].PullRequestID)
		if err != nil {
			return nil, err
		}
		prs[i] = pr
	}
	return prs, nil
}

func (s *Service) finalizeDirectStackMerge(ctx context.Context, req *db.AsyncMergeRequest, plan directStackMergePlan, selected []db.PullRequest, resultSHA string, memberSHAs []string) error {
	if req == nil || len(selected) != len(plan.Members) || len(memberSHAs) != len(selected) {
		return fmt.Errorf("cannot finalize direct Stack merge without exact member SHAs")
	}
	for i := range memberSHAs {
		if strings.TrimSpace(memberSHAs[i]) == "" {
			return fmt.Errorf("cannot finalize direct Stack merge with an empty member SHA")
		}
	}
	if err := s.markStackPrefixMerged(ctx, selected, req.Actor.Login, memberSHAs); err != nil {
		return err
	}
	now := time.Now().UTC()
	if err := s.DBForCtx(ctx).Model(&db.AsyncMergeRequest{}).Where("id = ? AND status IN ?", req.ID,
		[]string{AsyncMergeMerging, AsyncMergeCascading}).Updates(map[string]any{
		"status": AsyncMergeCascading, "result_sha": resultSHA, "updated_at": now,
		"expires_at": now.Add(asyncMergeRetention),
	}).Error; err != nil {
		return err
	}
	req.Status = AsyncMergeCascading
	req.ResultSHA = resultSHA
	if err := s.cascadeStackAfterMerge(ctx, plan.TargetPullRequestID, resultSHA, req.Actor); err != nil {
		return err
	}
	if err := s.DBForCtx(ctx).Model(&db.AsyncMergeRequest{}).Where("id = ? AND status = ?", req.ID, AsyncMergeCascading).
		Updates(map[string]any{"status": AsyncMergeMerged, "failure_message": "", "updated_at": now,
			"expires_at": now.Add(asyncMergeRetention)}).Error; err != nil {
		return err
	}
	ids := make([]uint, len(selected))
	for i := range selected {
		ids[i] = selected[i].ID
	}
	s.notifyPullRequestsMerged(ctx, ids)
	return nil
}

func (s *Service) recoverDirectStackMerge(ctx context.Context, req db.AsyncMergeRequest) error {
	gitStore, err := s.GitForCtx(ctx)
	if err != nil {
		return err
	}
	plan, err := decodeDirectStackMergePlan(req)
	if err != nil {
		return err
	}
	selected, err := s.loadDirectStackMergePlanPRs(ctx, plan)
	if err != nil {
		return err
	}
	if req.Status == AsyncMergeCascading {
		memberSHAs := make([]string, len(selected))
		for i := range selected {
			memberSHAs[i] = selected[i].MergeCommitSHA
		}
		return s.finalizeDirectStackMerge(ctx, &req, plan, selected, req.ResultSHA, memberSHAs)
	}
	baseSHA, err := gitStore.HeadSHA(ctx, plan.RepositoryFullName, plan.BaseRef)
	if err != nil {
		return err
	}
	if baseSHA == plan.BaseSHA {
		now := time.Now().UTC()
		result := s.DBForCtx(ctx).Model(&db.AsyncMergeRequest{}).Where("id = ? AND status = ?", req.ID, AsyncMergeMerging).
			Updates(map[string]any{"status": AsyncMergePending, "updated_at": now,
				"expires_at": now.Add(asyncMergeRetention)})
		if result.Error != nil || result.RowsAffected != 1 {
			return result.Error
		}
		return s.executeAsyncMerge(ctx, req.ID)
	}
	actual, err := gitStore.GetCommit(ctx, plan.RepositoryFullName, baseSHA)
	if err != nil {
		return err
	}
	expectedTree := plan.Members[len(plan.Members)-1].ExpectedTreeSHA
	if expectedTree == "" || actual.TreeSHA != expectedTree {
		return fmt.Errorf("base advanced to an unrecognized tree while direct Stack merge was in progress")
	}
	expectedTrees := make([]string, len(plan.Members))
	for i := range plan.Members {
		expectedTrees[i] = plan.Members[i].ExpectedTreeSHA
	}
	memberSHAs, err := s.recoverMemberSHAsFromExpectedTrees(ctx, plan.RepositoryFullName, plan.BaseSHA, baseSHA, expectedTrees)
	if err != nil {
		return err
	}
	return s.finalizeDirectStackMerge(ctx, &req, plan, selected, baseSHA, memberSHAs)
}
