package action

import (
	"context"
	"errors"
	"fmt"
	"slices"
	"strconv"
	"strings"
	"time"

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

type ChangeActor interface {
	// GetObjects get objects that are used by action.
	GetContext() *ActionContext
	// Create create a new change.
	Create(param relsvcapi.CreateChangeParam) error
	// Submit transition status from 'Draft' to 'Created'.
	Submit() error
	// SendApprovalIfReady send a approval if all tickets are ready.
	SendApprovalIfReady() error
	// UpdateChangeBeforeDeploy a change before approved and resend a approval if needed.
	UpdateChangeBeforeDeploy(update relsvcapi.UpdateChangeParam) error
	// ContinueIfApproved update change status when approval is finished.
	ContinueIfApproved() error
	// Execute a deployment of a change
	ExecuteDeployment(order relent.ChangeDeployOrder, force bool) error
	// VerifyDeployment verify a deployment of a change
	VerifyDeployment(order relent.ChangeDeployOrder, note string) error
	// Cancel a change
	Cancel() error
	// NotifyToCloseIfNeeded send notification to close the change.
	NotifyToCloseIfNeeded() error
	// Close a change
	Close(result relent.ChangeResult, summary string) error
}

func NewChangeActorByID(ctx context.Context, comp Components, id uint64) ChangeActor {
	change, err := comp.ChangeRepo.GetChange(ctx, id, true)
	if err != nil {
		return &actionBase{actionErr: fmt.Errorf("get change %d failed: %s", id, err)}
	}

	return NewChangeActor(ctx, comp, change)
}

// NewChangeActor create a ChangeActor by change status.
//
// If change is nil, only support to create a new change.
//
// If change is not nil, the change MUST be loaded from repo and change detail is required.
func NewChangeActor(ctx context.Context, comp Components, change *relent.ChangeInRepo) ChangeActor {
	var a ChangeActor
	var err error

	actx := &ActionContext{
		Context:    ctx,
		Components: comp,
		Objects: Objects{
			OriginChange: change,
			EventDetail:  &event.ChangeEventDetail{},
		},
	}

	// if change is nil, only support to create a new change
	if actx.OriginChange == nil {
		return &actionCreateChange{
			actionBase: &actionBase{
				ActionContext: actx,
				actionErr:     errs.APIErrorf(api.ErrCodeFailedPrecond, "only support to create a new change"),
			},
		}
	}

	actx.Change, err = pointer.DeepCopy(actx.OriginChange) // deep copy to avoid modify change directly
	if err != nil {
		return &actionBase{actionErr: fmt.Errorf("copy change failed: %s", err)}
	}
	if actx.Change.Detail == nil {
		return &actionBase{
			actionErr: fmt.Errorf("change detail is required when change action"),
		}
	}

	base := &actionBase{
		ActionContext: actx,
		actionErr: errs.APIErrorf(api.ErrCodeFailedPrecond,
			"unsupported action when status is %s", actx.Change.Status),
	}

	switch actx.Change.Status {
	case relent.ChangeStatusDraft:
		a = &actionWhenDraft{
			actionBase: base,
		}
	case relent.ChangeStatusCreated:
		a = &actionWhenCreated{
			actionBase: base,
		}
	case relent.ChangeStatusApproving:
		a = &actionWhenApproving{
			actionBase: base,
		}
	case relent.ChangeStatusWaitForDeploy:
		a = &actionWhenWaitForDeploy{
			actionBase: base,
		}
	case relent.ChangeStatusInProgress:
		a = &actionWhenInProgress{
			actionBase: base,
		}
	case relent.ChangeStatusObserving:
		a = &actionWhenObserving{
			actionBase: base,
		}
	case relent.ChangeStatusWithdrawn:
		a = &actionWhenWithdrawn{
			actionBase: base,
		}
	case relent.ChangeStatusClosed:
		// finished status, can't do anything
		a = base
	default:
		base.actionErr = fmt.Errorf("unknown change status: %s", actx.Change.Status)
		a = base
	}

	return a
}

type actionBase struct {
	*ActionContext

	actionErr error // default return
}

func (a *actionBase) GetContext() *ActionContext {
	return a.ActionContext
}

func (a *actionBase) Create(param relsvcapi.CreateChangeParam) error {
	return a.actionErr
}

func (a *actionBase) Submit() error {
	return a.actionErr
}

func (a *actionBase) SendApprovalIfReady() error {
	return a.actionErr
}

func (a *actionBase) UpdateChangeBeforeDeploy(update relsvcapi.UpdateChangeParam) error {
	return a.actionErr
}

func (a *actionBase) ContinueIfApproved() error {
	return a.actionErr
}

func (a *actionBase) ExecuteDeployment(order relent.ChangeDeployOrder, force bool) error {
	return a.actionErr
}

func (a *actionBase) VerifyDeployment(order relent.ChangeDeployOrder, note string) error {
	return a.actionErr
}

func (a *actionBase) Cancel() error {
	return a.actionErr
}

func (a *actionBase) NotifyToCloseIfNeeded() error {
	return a.actionErr
}

func (a *actionBase) Close(result relent.ChangeResult, summary string) error {
	return a.actionErr
}

//// some common functions used by action implement ////

// cancel transition status to 'Closed' and result is 'Cancel'.
func (a *actionBase) cancel() error {
	ctx := a.Context
	change := a.Change

	// transition status to 'Cancel'
	return a.PatchChange(ctx,
		PatchChangeToClose(relent.ChangeResultCancel, fmt.Sprintf("cancel when status is %s", change.Status)),
	)
}

func (a *actionBase) close(result relent.ChangeResult, summary string) error {
	ctx := a.Context
	change := a.Change

	if !slices.Contains(resultsToClose, result) {
		return errs.APIErrorf(api.ErrCodeInvalidArgument, "invalid result `%s` to close", result)
	}

	reasons, err := a.canClose(result)
	if err != nil {
		return fmt.Errorf("can close change failed: %w", err)
	}

	if len(reasons) != 0 {
		return errs.APIErrorNew(api.ErrCodeFailedPrecond, errors.New(strings.Join(reasons, ",")))
	}

	// close tickets by the way
	if result == relent.ChangeResultSuccessful {
		err := a.ReleaseService.CloseTickets(ctx, change.ChangeItemNames)
		if err != nil {
			return fmt.Errorf("close tickets failed: %w", err)
		}
	}

	// send a notification
	param := larknotify.ReleaseCloseParam{
		Result:  result,
		Summary: summary,
	}
	_, err = a.LarkClient.Notify.ReleaseClose(ctx, *change.Notification.LarkTopicID, param)
	if err != nil {
		return fmt.Errorf("notify close failed: %w", err)
	}

	return a.PatchChange(ctx, PatchChangeToClose(result, summary))
}

func (a *actionBase) canClose(result relent.ChangeResult) ([]string, error) {
	change := a.Change
	now := time.Now()

	reason := []string{}

	if result == relent.ChangeResultSuccessful {
		// min observe duration when success
		last := change.LastDeployment()
		if last != nil {
			if last.DeployedAt != nil && now.Sub(*last.DeployedAt) < *a.Conf.MinObserveDuration {
				reason = append(reason, fmt.Sprintf("must observe at least %s", a.Conf.MinObserveDuration.String()))
			}
		}

	}

	return reason, nil
}

// updateBeforeApproved update change and transition status to 'Created'.
func (a *actionBase) updateBeforeApproved(update relsvcapi.UpdateChangeParam) error {
	ctx := a.Context
	change := a.Change

	// validate deps
	if update.Deployments != nil {
		if err := CheckDeployments(ctx, a.GithubService.RawAPI(), *update.Deployments); err != nil {
			return err
		}
	}
	if update.ChangeItemNames != nil && len(*update.ChangeItemNames) != 0 {
		rparam := relsvcapi.GetReleaseParam{WithTickets: true}
		release, err := a.ReleaseService.GetRelease(a.Context, change.ReleaseID, rparam)
		if err != nil {
			return err
		}

		_, err = CheckChangeItems(ctx, release, *update.ChangeItemNames)
		if err != nil {
			return err
		}
	}

	// update change and transition status to 'Created'
	a.EventDetail.Update = &event.ChangeEventUpdate{}
	toStatus := relent.ChangeStatusCreated
	if change.Status == relent.ChangeStatusDraft {
		toStatus = relent.ChangeStatusDraft
	}
	return a.PatchChange(ctx,
		PatchChangeToUpdateBeforeApproved(update, a.EventDetail.Update),
		PatchChangeStatus(toStatus, ""),
	)
}

// executeDeployment executes a deployment and transition status
func (a *actionBase) executeDeployment(order relent.ChangeDeployOrder, force bool) error {
	ctx := a.Context
	change := a.Change
	now := time.Now()

	deployment := a.Change.GetDeployment(order)
	if deployment == nil {
		return errs.APIErrorf(api.ErrCodeNotFound, "deployment with order %d is not found", order)
	}

	if deployment.DeployedAt != nil {
		return errs.APIErrorf(api.ErrCodeInvalidArgument, "deployment %d is already deployed", order)
	}

	reasons, err := CheckDeploymentReady(ctx, a.Components, change, deployment)
	if err != nil {
		return err
	}
	if len(reasons) != 0 && !force {
		return errs.APIErrorNew(api.ErrCodeFailedPrecond, errors.New(strings.Join(reasons, ",")))
	}

	a.EventDetail.ExecDeploy = &event.ChangeEventExecDeploy{
		ChangeEventDeploy: event.ChangeEventDeploy{
			Order: order.Int(),
		},
		Force:        force,
		SkippedConds: reasons,
	}

	// execute
	deployErr := ExecDeployment(ctx, a.Components, change, deployment)
	if deployErr != nil {
		return deployErr
	}

	// send notification
	go func() {
		if change.Notification != nil && change.Notification.LarkTopicID != nil {
			param := larknotify.ReleaseDeploymentParam{
				Order:      order,
				Regions:    deployment.Regions,
				DeployedAt: deployment.DeployedAt,
				Method:     deployment.Method,
				Info:       deployment.Info,
			}
			_, err := a.LarkClient.Notify.ReleaseDeployment(context.WithoutCancel(ctx), *change.Notification.LarkTopicID, param)
			if err != nil {
				a.Logger.Error(err, "failed to send deployment notification", "order", order)
			}
		}
	}()

	// update status
	err = a.PatchChange(ctx,
		func(current *relent.ChangeInRepo) error {
			// update deployment status
			for i := range current.Deployments {
				if current.Deployments[i].Order == order {
					current.Deployments[i].Status = relent.ChangeDeployStatusDeployed
					current.Deployments[i].DeployedAt = pointer.Pointer(now)
				}
			}
			// if all deployments are deployed, transition status to 'Observing'
			// otherwise, transition status to 'InProgress'
			var undeployed *int
			for _, d := range current.Deployments {
				if d.DeployedAt == nil {
					undeployed = pointer.Pointer(d.Order.Int())
					break
				}
			}
			if undeployed == nil {
				return PatchChangeStatus(relent.ChangeStatusObserving, "")(current)
			} else {
				return PatchChangeStatus(relent.ChangeStatusInProgress,
					fmt.Sprintf("Some deployments %d are deployed", *undeployed))(current)
			}
		})
	if err != nil {
		return err
	}

	return nil
}

func (a *actionBase) verifyDeployment(order relent.ChangeDeployOrder, note string) error {
	ctx := a.Context

	a.EventDetail = &event.ChangeEventDetail{
		VerifyDeploy: &event.ChangeEventDeploy{
			Order: order.Int(),
		},
	}

	return a.PatchChange(ctx,
		func(current *relent.ChangeInRepo) error {
			for _, deployment := range current.Deployments {
				if deployment.Order == order {
					if deployment.DeployedAt == nil {
						return errs.APIErrorf(api.ErrCodeFailedPrecond, "should deploy before verify")
					}

					deployment.Status = relent.ChangeDeployStatusVerified
					deployment.Note = note
					return nil
				}
			}

			return errs.APIErrorf(api.ErrCodeInvalidArgument, "deployment with order %d is not found", order)
		},
	)
}

func (a *actionBase) sendApproval(changeItems []*relent.ReleaseTicket) (*uint64, error) {
	ctx := a.Context
	change := a.Change

	application, err := a.ApplicationService.GetApplication(a.Context, change.ApplicationName)
	if err != nil {
		return nil, fmt.Errorf("get application %s failed: %w", change.ApplicationName, err)
	}

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

	// create a param
	param := apprsvcapi.CreateApprovalParam{
		CreateApprovalParamBase: apprsvcapi.CreateApprovalParamBase{
			Name:       "Cloud Release",
			Applicant:  applicantResp.User.Email,
			DetailLink: ChangeDetailLink(change.ReleaseID, change.ID),
			Parameters: map[string]string{
				"Change ID":   strconv.FormatUint(change.ID, 10),
				"Name":        change.Name,
				"Type":        string(change.Type),
				"Application": application.Name,
				"Window": fmt.Sprintf("%s - %s",
					change.WindowStartTime().In(timeutil.ChinaTimeZone).Format(time.DateTime),
					change.WindowEndTime().In(timeutil.ChinaTimeZone).Format(time.DateTime),
				),
				"DM Tickets": strings.Join(
					container.TransformSlice(changeItems, func(t *relent.ReleaseTicket) string { return t.Name }), ", ",
				),
				"Risk Level": string(change.RiskLevel),
			},
		},
		Nodes: nil,
	}
	switch change.ApplicationName {
	// if the release is ng image release, use the components corresponding approvers
	case "ng-image-release":
		for _, item := range change.Deployments {
			if item.Info.EEAPI == nil || item.Info.EEAPI.EEAPIBuildNgImage == nil {
				a.Logger.Error(nil, "eeapi build ng image info is nil", "order", item.Order)
				continue
			}
			nodes, err := a.buildNgApprovers(item.Info.EEAPI.EEAPIBuildNgImage)
			if err != nil {
				a.Logger.Error(err, "failed to build ng image build approvers", "order", item.Order)
				continue
			}
			param.Nodes = append(param.Nodes, nodes)
		}

	default:
		// - the application owner is the first approver
		param.Nodes = append(param.Nodes, &apprent.ApproverNode{
			Method: apprent.ApproveMethodAny,
			Approver: &apprent.Approver{
				Emails: application.Owners,
			},
		})
	}

	// - if risk level is high, the applicant's leader is the second approver
	if change.RiskLevel == relent.RiskLevelHigh {
		param.Nodes = append(param.Nodes, &apprent.ApproverNode{
			Method: apprent.ApproveMethodAny,
			Approver: &apprent.Approver{
				ApplicantLeader: true,
			},
		})
	}
	// - add the last approvers
	if len(a.Conf.LastApproverEmails) > 0 {
		param.Nodes = append(param.Nodes, &apprent.ApproverNode{
			Method: apprent.ApproveMethodAny,
			Approver: &apprent.Approver{
				Emails: a.Conf.LastApproverEmails,
			},
		})
	}

	apprID, err := a.ApprovalService.CreateApproval(ctx, param)
	if err != nil {
		return nil, fmt.Errorf("create approval failed: %s", err)
	}

	return apprID, nil
}

func (a *actionBase) cancelApproval(appr *apprent.Approval) error {
	ctx := a.Context

	if appr.Status == apprent.ApprovalStatusPending {
		err := a.ApprovalService.CancelApproval(ctx, appr.ID)
		if err != nil {
			return err
		}
	}

	return nil
}

func (a *actionBase) buildNgApprovers(eeDeployReq *eecli.BumpNgTagRequest) (*apprent.ApproverNode, error) {
	deployRepo := eeDeployReq.Repo
	for repo, approvers := range a.Conf.NgImageBuildApproverEmailsMap {
		if repo == string(deployRepo) {
			return &apprent.ApproverNode{
				Method: apprent.ApproveMethodAny,
				Approver: &apprent.Approver{
					Emails: approvers,
				},
			}, nil
		}
	}
	return nil, fmt.Errorf("no approvers found for ng image build repo %s", deployRepo)
}
