package action

import (
	"fmt"
	"time"

	userv1 "github.com/tidbcloud/aisupport/packages/sdk-go/contracts/api/core-server/user/v1"
	coreuser "github.com/tidbcloud/aisupport/packages/sdk-go/contracts/entity/core-server/user/v1"
	apprent "github.com/tidbcloud/devops/entity/approval"
	"github.com/tidbcloud/devops/entity/event"
	relent "github.com/tidbcloud/devops/entity/release"
	larknotify "github.com/tidbcloud/devops/pkg/infra/clients/larkcli/notify"
	"github.com/tidbcloud/devops/pkg/infra/consts"
	"github.com/tidbcloud/devops/pkg/infra/errs"
	"github.com/tidbcloud/devops/pkg/infra/utils/container"
	"github.com/tidbcloud/devops/pkg/infra/utils/pointer"
	relsvcapi "github.com/tidbcloud/devops/pkg/service/release/api"
)

type actionWhenApproving struct {
	*actionBase
}

func (a *actionWhenApproving) ContinueIfApproved() error {
	ctx := a.Context
	change := a.Change

	if change.ApprovalID == nil {
		return fmt.Errorf("status is approving but approval id is nil")
	}

	approval, err := a.ApprovalService.GetApproval(a.Context, *change.ApprovalID)
	if err != nil {
		return fmt.Errorf("get approval %d failed: %s", *change.ApprovalID, err)
	}
	a.EventDetail.FinshApproval = &event.ChangeEventFinshApproval{
		Result: approval.Status,
	}

	if approval.Status == apprent.ApprovalStatusRejected {
		err := a.PatchChange(ctx,
			PatchChangeStatus(relent.ChangeStatusWithdrawn, "The approval is rejected."),
		)
		return err
	}

	if approval.Status == apprent.ApprovalStatusApproved {
		applicantResp, err := a.UserService.GetUser(ctx, &userv1.GetUserRequest{Email: change.ApplicantEmail})
		if err != nil {
			return fmt.Errorf("get applicant %s info failed: %w", change.ApplicantEmail, err)
		}
		applicant := applicantResp.User

		var tickets []*relent.ReleaseTicket
		if len(change.ChangeItemNames) > 0 {
			var err error
			tickets, _, err = a.ReleaseService.ListTickets(ctx, relsvcapi.ListTicketsParam{
				Names: change.ChangeItemNames,
			})
			if err != nil {
				return fmt.Errorf("get release tickets failed: %w", err)
			}
		}

		// mark PR in FAD
		if consts.GetPartition() != consts.PartitionCN { // not support FAD in CN yet
			a.Logger.Info("begin marking PR in FAD")
			for _, deployment := range a.Change.Deployments {
				if deployment.Info.ArgoCD == nil {
					continue
				}

				for _, pr := range deployment.Info.ArgoCD.DeployPRs {
					// notify FAD to mark the PR as can be merged
					if err := a.FADClient.MarkPR(&pr); err != nil {
						return err
					}
				}
			}
		}

		// send notification if not sent
		var msgID *string
		if change.Notification.LarkTopicID == nil {
			param := larknotify.ReleaseTopicParam{
				ChangeName:      change.Name,
				ChangeLink:      ChangeDetailLink(change.ReleaseID, change.ID),
				ApplicationName: change.ApplicationName,
				WindowStartAt:   change.WindowStartTime(),
				WindowEndAt:     change.WindowEndTime(),
				RiskLevel:       change.RiskLevel,
				ChangeType:      change.Type,

				ChangeItems: make([]larknotify.ReleaseTopicParamChangeItem, len(tickets)),
				Applicant: larknotify.LarkUser{
					Email:  applicant.Email,
					LarkID: nil,
				},
			}

			// notify applicant
			if applicant.LarkType != coreuser.LarkType_LARK_TYPE_GLOBAL {
				a.Logger.Info("applicant is not in global so skip notifing him", "applicant", applicant.Email)
			} else {
				param.Applicant.LarkID = pointer.Pointer(applicant.LarkUserId)
			}

			for i, item := range tickets {
				usersToNotify := []*coreuser.User{}
				// add assignee of this ticket to notify
				userResp, err := a.UserService.GetUser(ctx, &userv1.GetUserRequest{Email: item.AssigneeEmail})
				if err != nil {
					a.Logger.V(4).Info("get assignee info failed", "email", item.AssigneeEmail, "err", err.Error())
				} else {
					usersToNotify = append(usersToNotify, userResp.User)
				}
				// add developers of this ticket to notify
				existedPRAuthor := container.NewSet[string]()
				for _, prLoc := range item.GithubPRLocations {
					pr, err := a.GithubService.GetPullRequestByLoc(ctx, prLoc)
					if err != nil {
						a.Logger.V(4).Info("get pull request failed", "pr", prLoc, "err", err.Error())
						continue
					}
					if existedPRAuthor.Has(pr.Author) {
						continue
					}

					userResp, err := a.UserService.ListUsers(ctx, &userv1.ListUsersRequest{
						GithubId: pr.Author,
						Page:     1,
						PerPage:  1,
					})
					if err != nil {
						a.Logger.V(4).Info("get user with github_id failed", "github_id", pr.Author, "err", err.Error())
						continue
					}
					if len(userResp.Users) == 0 {
						a.Logger.V(4).Info("get user with github_id failed", "github_id", pr.Author, "err", "not found")
						continue
					}
					existedPRAuthor.Insert(pr.Author)
					usersToNotify = append(usersToNotify, userResp.Users[0])
				}

				usersLarkIDs := container.NewSet[string]()
				for _, u := range usersToNotify {
					usersLarkIDs.Insert(u.LarkUserId)
				}

				param.ChangeItems[i] = larknotify.ReleaseTopicParamChangeItem{
					ChangeItem:  item,
					NotifyUsers: usersLarkIDs,
				}
			}

			msgID, err = a.LarkClient.Notify.ReleaseTopic(ctx, a.Conf.NotifyChatID, param)
			if err != nil {
				return fmt.Errorf("create release topic failed: %w", err)
			}
		}

		// record info and update status
		return a.PatchChange(ctx,
			PatchChangeStatus(relent.ChangeStatusWaitForDeploy, ""),
			func(current *relent.ChangeInRepo) error {
				if msgID != nil {
					current.Notification.LarkTopicCreatedAt = pointer.Pointer(time.Now())
					current.Notification.LarkTopicID = msgID
				}
				return nil
			})
	}

	// record pending reason
	err = a.PatchChange(ctx,
		PatchChangeStatus(relent.ChangeStatusApproving, "Waiting for approval."),
	)
	if err != nil {
		return err
	}

	return errs.RetriableErrorf("approval %d is %s", change.ApprovalID, approval.Status)
}

func (a *actionWhenApproving) Cancel() error {
	err := a.actionBase.cancel()
	if err != nil {
		return err
	}

	// cancel previous approval
	if a.Change.ApprovalID != nil {
		appr, err := a.ApprovalService.GetApproval(a.Context, *a.Change.ApprovalID)
		if err != nil {
			return fmt.Errorf("get approval %d failed: %s", *a.Change.ApprovalID, err)
		}

		a.cancelApproval(appr)
	}

	return nil
}

func (a *actionWhenApproving) UpdateChangeBeforeDeploy(update relsvcapi.UpdateChangeParam) error {
	return a.updateBeforeApproved(update)
}
