package action

import (
	"encoding/json"
	"errors"
	"fmt"
	"strconv"

	"github.com/tidbcloud/fastflow/pkg/entity/run"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"google.golang.org/protobuf/encoding/protojson"

	globalcommonv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/common/v1"
	globalserverlessv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/serverless/v1"
	regionalserverlessv1 "github.com/tidbcloud/tidb-management-service/api/spec/regional/serverless/v1"
	"github.com/tidbcloud/tidb-management-service/serverless-global-service/domain/adapter"
	"github.com/tidbcloud/tidb-management-service/serverless-global-service/domain/constant"
	"github.com/tidbcloud/tidb-management-service/serverless-global-service/domain/entity"
	"github.com/tidbcloud/tidb-management-service/serverless-global-service/domain/repository"
	"github.com/tidbcloud/tidb-management-service/utilities/clusterevent"
	"github.com/tidbcloud/tidb-management-service/utilities/workflow"
)

const (
	UpdateCluster = "update_cluster"
)

type UpdateClusterParameter struct {
	ClusterId   string `json:"cluster_id"`
	RequestBody string `json:"request_body"`
}

type UpdateClusterAction struct {
	clusterRepo      repository.ClusterRepo
	regionalService  adapter.RegionalService
	serverlessFacade adapter.ServerlessService
	brFacade         adapter.BRService
	eventCommitter   adapter.WALCommitter
}

func NewUpdateClusterAction(
	clusterRepo repository.ClusterRepo,
	regionalService adapter.RegionalService,
	serverlessFacade adapter.ServerlessService,
	brFacade adapter.BRService,
	eventCommitter adapter.WALCommitter,
) *UpdateClusterAction {
	return &UpdateClusterAction{
		clusterRepo:      clusterRepo,
		regionalService:  regionalService,
		serverlessFacade: serverlessFacade,
		brFacade:         brFacade,
		eventCommitter:   eventCommitter,
	}
}

func (a *UpdateClusterAction) Name() string {
	return UpdateCluster
}

func (a *UpdateClusterAction) ParameterNew() any {
	return &UpdateClusterParameter{}
}

func (a *UpdateClusterAction) Run(ctx run.ExecuteContext, params any) error {
	cctx, span := workflow.GenerateContext(ctx)

	updateClusterParam, ok := params.(*UpdateClusterParameter)
	if !ok {
		tilog.Error(cctx, "expect UpdateClusterParameter parameter")
		return errors.New("expect UpdateClusterParameter parameter")
	}

	var err error
	tilog.Info(cctx, "start to update cluster", zap.String("cluster_id", updateClusterParam.ClusterId))
	defer func() {
		if err != nil {
			tilog.Error(
				cctx,
				"failed to update cluster",
				zap.String("cluster_id", updateClusterParam.ClusterId),
				zap.Error(err),
			)
		} else {
			tilog.Info(cctx, "end to update cluster", zap.String("cluster_id", updateClusterParam.ClusterId))
		}
		span.End()
	}()

	var req globalserverlessv1.ServerlessServiceUpdateRequest
	if err := protojson.Unmarshal([]byte(updateClusterParam.RequestBody), &req); err != nil {
		return err
	}

	var clusterEn *entity.Cluster
	clusterEn, err = a.clusterRepo.Get(cctx, updateClusterParam.ClusterId, true)
	if err != nil {
		return err
	}

	if !clusterEn.DeletedAt.IsZero() {
		tilog.Warn(
			cctx,
			"the cluster is deleted, interrupt initialize task",
			zap.String("cluster", updateClusterParam.ClusterId),
		)
		return nil
	}

	servicePlan := ""
	// when update cluster spend limit, serverless service will ignore EnhancedEncryptionEnabled flag, so set it to false
	if req.GetCluster().GetSpendLimit().GetMonthly() != 0 {
		if err = a.serverlessFacade.SetSpendLimit(
			cctx,
			clusterEn.ClusterId,
			clusterEn.TenantId,
			req.GetCluster().GetSpendLimit().GetMonthly(),
			clusterEn.Region.Id.String(),
			false,
			false,
		); err != nil {
			return fmt.Errorf("set spendLimit failed: %w", err)
		}
		servicePlan = constant.ServicePlanStarterScalable
	}
	if req.GetCluster().GetCluster().GetAutoScaling().GetMaxRcu() > 0 {
		servicePlan = globalcommonv1.ServicePlan_Essential.String()
	}
	if servicePlan != "" {
		if req.GetCluster().GetCluster().GetAnnotations() == nil {
			if req.GetCluster().Cluster == nil {
				req.Cluster.Cluster = &regionalserverlessv1.ServerlessRegionalServiceClusterInfo{}
			}
			req.Cluster.Cluster.Annotations = make(map[string]string)
		}
		req.Cluster.Cluster.Annotations[constant.AnnoServicePlan] = servicePlan
	}

	// pass ru_version to regional via annotations
	if req.GetCluster().GetRuVersion() > 0 {
		if req.GetCluster().GetCluster().GetAnnotations() == nil {
			if req.GetCluster().Cluster == nil {
				req.Cluster.Cluster = &regionalserverlessv1.ServerlessRegionalServiceClusterInfo{}
			}
			req.Cluster.Cluster.Annotations = make(map[string]string)
		}
		req.Cluster.Cluster.Annotations[constant.AnnoRuVersion] = strconv.Itoa(int(req.GetCluster().GetRuVersion()))
	}

	// update backup policy
	if req.GetCluster().GetBackupPolicy() != nil {
		if err = a.brFacade.SetBackupPolicy(
			cctx,
			clusterEn.ClusterId,
			clusterEn.TenantId,
			clusterEn.ProjectId,
			req.GetCluster().GetBackupPolicy(),
		); err != nil {
			return fmt.Errorf("set backup policy failed: %w", err)
		}
	}

	if !judgeClusterUpdate(req.GetCluster()) {
		tilog.Info(
			cctx,
			"need not to update cluster in regional service",
			zap.String("cluster_id", updateClusterParam.ClusterId),
		)
		return nil
	}

	// update cluster
	regionalCluster := req.GetCluster().GetCluster()
	regionalCluster.ClusterId = clusterEn.ClusterId
	regionalCluster.ProjectId = clusterEn.ProjectId
	_, err = a.regionalService.UpdateCluster(cctx, clusterEn.Region, regionalCluster)
	if err != nil {
		return err
	}

	if req.GetCluster().GetCluster().GetAutoScaling().GetMaxRcu() > 0 {
		// spending limit only be set for starter plan cluster, and starter plan can upgrade to essential plan
		// so we need try to delete spending limit for essential plan cluster
		if err = a.serverlessFacade.DeleteSpendLimit(cctx, clusterEn.ClusterId, clusterEn.TenantId); err != nil {
			return fmt.Errorf("delete spend limit for essential plan failed: %w", err)
		}
	}

	// commit event
	var updateEvents []clusterevent.Event
	if req.GetCluster().GetCluster().GetEndpoints().GetPublic() != nil {
		eventType := clusterevent.EventTypeUpdatedPublicEndpoint
		ev, err := entity.NewClusterEvent(cctx, clusterEn, eventType, clusterEn.GetUserStatus().String())
		if err != nil {
			tilog.Error(
				cctx,
				"create cluster event failed",
				zap.String("id", clusterEn.ClusterId),
				zap.Any("eventtype", eventType),
				zap.Error(err),
			)
			return err
		}
		updateEvents = append(updateEvents, ev)
	}
	if req.GetCluster().GetCluster().GetAutoScaling().GetMaxRcu() > 0 {
		eventType := clusterevent.EventTypeUpdatedCapacity
		ev, err := entity.NewClusterEvent(cctx, clusterEn, eventType, clusterEn.GetUserStatus().String())
		if err != nil {
			tilog.Error(
				cctx,
				"create cluster event failed",
				zap.String("id", clusterEn.ClusterId),
				zap.Any("eventtype", eventType),
				zap.Error(err),
			)
			return err
		}
		updateEvents = append(updateEvents, ev)
	}
	if req.GetCluster().GetCluster().GetAuditLogConfig() != nil {
		eventType := clusterevent.EventTypeUpdatedAuditLogConfig
		ev, err := entity.NewClusterEvent(cctx, clusterEn, eventType, clusterEn.GetUserStatus().String())
		if err != nil {
			tilog.Error(
				cctx,
				"create cluster event failed",
				zap.String("id", clusterEn.ClusterId),
				zap.Any("eventtype", eventType),
				zap.Error(err),
			)
			return err
		}
		alc, _ := json.Marshal(req.GetCluster().GetCluster().GetAuditLogConfig())
		ev.ExtraInfo.AuditLogConfig = string(alc)
		updateEvents = append(updateEvents, ev)
	}
	if len(updateEvents) > 0 {
		if err = a.eventCommitter.BatchWrite(cctx, clusterevent.TopicClusterEvent, updateEvents); err != nil {
			tilog.Error(cctx, "publish update cluster event failed", zap.Any("events", updateEvents), zap.Error(err))
			return err
		}
	}

	return err
}

func judgeClusterUpdate(req *globalserverlessv1.ServerlessServiceCluster) bool {
	if req.GetCluster().GetEndpoints().GetPublic() != nil {
		return true
	}
	if req.GetCluster().GetEndpoints().GetPrivate() != nil {
		return true
	}
	if len(req.GetCluster().GetAnnotations()) > 0 {
		return true
	}
	if req.GetCluster().GetAuditLogConfig() != nil {
		return true
	}
	if req.GetCluster().GetAutoScaling() != nil {
		return true
	}
	return false
}
