package cluster

import (
	"context"
	"errors"
	"fmt"
	"maps"

	"braces.dev/errtrace"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"gorm.io/gorm"

	globaldedicatedv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/dedicated/v1"
	tidbmgmtservicev1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_mgmt_service/v1"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/domain/constant"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/domain/entity"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/pkg/apimiddleware/bizerror"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
	"github.com/tidbcloud/tidb-management-service/utilities/grpc/metadata"
)

func (svc *service) UpdateCluster(
	ctx context.Context,
	newCluster *entity.Cluster,
	clusterComponents []*globaldedicatedv1.Cluster_ClusterComponent,
	forceScale, validateOnly bool,
	sourceOperation string,
	requestedFields []string,
) (
	*entity.Cluster,
	*entity.ClusterCustomStatus,
	[]*globaldedicatedv1.Cluster_ClusterComponent,
	error,
) {
	spanName := "ClusterService.UpdateCluster"
	ctx, span := tracer.Start(ctx, spanName)
	defer span.End()

	var (
		cluster              *entity.Cluster
		status               *entity.ClusterCustomStatus
		oldClusterComponents []*globaldedicatedv1.Cluster_ClusterComponent
	)

	if newCluster.ClusterId == 0 {
		return nil, nil, nil, errors.New("cluster id is invalid")
	}

	if err := svc.transaction.Transaction(ctx, func(ctx context.Context) error {
		var (
			err     error
			isMutex bool
		)
		// First make sure cluster exists in database
		cluster, err = svc.clusterRepo.GetCluster(ctx, newCluster.ClusterId, false)
		if err != nil {
			return fmt.Errorf("failed to find cluste in database: %w", err)
		}

		if cluster.DeployType != entity.DeployTypeDedicated {
			return errtrace.Wrap(errors.New("cluster is not a dedicated cluster"))
		}

		if cluster.DisplayName != newCluster.DisplayName && newCluster.DisplayName != "" {
			// Check cluster name is unique under the project
			count, err := svc.clusterRepo.CountDisplayNameUnderProject(ctx, cluster.ProjectId, newCluster.DisplayName)
			if err != nil {
				return fmt.Errorf("failed to update cluster display name in database: %w", err)
			}
			if count > 0 {
				return bizerror.NewDisplayNameAlreadyExists(constant.ErrDuplicatedClusterName)
			}

			// if validateOnly is true, then no need to update cluster display name in database.
			if !validateOnly {
				err = svc.clusterRepo.UpdateDisplayName(ctx, cluster.ClusterId, newCluster.DisplayName)
				if err != nil {
					return fmt.Errorf("failed to update cluster display name in database: %w", err)
				}
				cluster.DisplayName = newCluster.DisplayName
			}
		}

		if newCluster.Labels != nil {
			// if validateOnly is true, then no need to update cluster labels in database.
			if !validateOnly {
				err := svc.clusterRepo.UpdateLabels(ctx, cluster.ClusterId, cluster.Labels)
				if err != nil {
					return fmt.Errorf("failed to update cluster labels in database: %w", err)
				}
			}

			maps.Copy(cluster.Labels, newCluster.Labels)
		}

		// no need to update cluster components
		if len(clusterComponents) == 0 {
			return nil
		}

		// check ClusterAction_CLUSTER_ACTION_SCALE_CLUSTER mutex
		isMutex, status, err = svc.customStatusRepo.IsMutexClusterAction(
			ctx,
			newCluster.ClusterId,
			tidbmgmtservicev1.ClusterAction_CLUSTER_ACTION_SCALE_CLUSTER,
		)
		if err != nil {
			if !errors.Is(err, gorm.ErrRecordNotFound) {
				return fmt.Errorf("failed to check mutex cluster action: %w", err)
			}
		}

		if isMutex {
			tilog.Error(ctx, "mutex custom status is exist", zap.Uint64("cluster_id", newCluster.ClusterId), zap.Any("custom status", status))
			return errtrace.Wrap(constant.ErrClusterCustomStatusForbidden)
		}

		var dedicatedCluster *globaldedicatedv1.Cluster
		dedicatedCluster, err = svc.dedicatedCli.GetCluster(ctx, &globaldedicatedv1.GetClusterRequest{ClusterId: newCluster.ClusterId})
		if err != nil {
			return errtrace.Wrap(err)
		}
		oldClusterComponents = dedicatedCluster.ClusterComponents

		// check components
		clusterComponents, err = checkComponents(clusterComponents, oldClusterComponents)
		if err != nil {
			return err
		}

		reqMeta, err := metadata.ExtractRequestMeta(ctx)
		if err != nil {
			return fmt.Errorf("failed to extract request metadata: %w", err)
		}
		request := &globaldedicatedv1.ScaleClusterWithMultiComponentRequest{
			ClusterId:         newCluster.ClusterId,
			ClusterComponents: clusterComponents,
			ValidateOnly:      validateOnly,
			ForceScale:        forceScale,
			SourceOperation:   sourceOperation,
			RequestedFields:   requestedFields,
		}
		dedicatedCluster, err = svc.dedicatedCli.ScaleClusterWithMultiComponent(metadata.AppendToOutgoingMeta(ctx, reqMeta), request)
		if err != nil {
			return errtrace.Wrap(err)
		}

		if dedicatedCluster == nil {
			return errtrace.Wrap(errors.New("global cluster object should not be nil"))
		}

		// touch cluster record in schema.
		// if validateOnly is true, then no need to touch cluster record in schema.
		if !validateOnly {
			err = svc.clusterRepo.TouchCluster(ctx, newCluster.ClusterId)
			if err != nil {
				return fmt.Errorf("failed to touch cluster record in database: %w", err)
			}
		}

		cluster = cvtDedicatedCluster(cluster, dedicatedCluster)

		return nil
	}); err != nil {
		return cluster, status, oldClusterComponents, err
	}

	return cluster, nil, oldClusterComponents, nil
}

func checkComponents(
	newClusterComponents, oldClusterComponents []*globaldedicatedv1.Cluster_ClusterComponent,
) ([]*globaldedicatedv1.Cluster_ClusterComponent, error) {
	var defaultTidbGroupId uint64

	currTiproxyNodeGroup := make(map[uint64]*globaldedicatedv1.Cluster_ClusterComponent)
	currTidbNodeGroup := make(map[uint64]*globaldedicatedv1.Cluster_ClusterComponent)
	var (
		currTiProxySpuName string
		currTiProxyCount   uint32
	)

	// get multi tidb group status
	for _, c := range oldClusterComponents {
		if c.ComponentType == globaldedicatedv1.ComponentType_TIDB {
			if c.IsDefaultTidbGroup {
				defaultTidbGroupId = c.TidbGroupId
			}
			currTidbNodeGroup[c.TidbGroupId] = c
		}

		if c.ComponentType == globaldedicatedv1.ComponentType_TIPROXY {
			currTiproxyNodeGroup[c.TidbGroupId] = c
			currTiProxySpuName = c.GetSpuName()
			currTiProxyCount += c.GetCount()
		}
	}

	// when multi tidb group, must have default work pool id
	if defaultTidbGroupId == 0 {
		return newClusterComponents, bizerr.New(
			bizerr.ErrCodeParams,
			"multi tidb node group is not allowed modify without default tidb node group id",
		)
	}

	newTidbSpuName := ""
	newTiProxySpuName := ""
	for i, c := range newClusterComponents {
		switch c.ComponentType {
		case globaldedicatedv1.ComponentType_TIDB:
			// set default work pool id
			if c.GetTidbGroupId() == 0 {
				newClusterComponents[i].TidbGroupId = defaultTidbGroupId
			}

			// modify tier name
			if c.GetSpuName() != "" {
				if newTidbSpuName != "" && c.GetSpuName() != newTidbSpuName {
					return nil, bizerr.New(
						bizerr.ErrCodeParams,
						"multi tidb node group is not allowed set different node spec",
					)
				}
				newTidbSpuName = c.GetSpuName()
			}

			// remove the tidb node group from the map
			delete(currTidbNodeGroup, c.GetTidbGroupId())

		case globaldedicatedv1.ComponentType_TIPROXY:
			if c.GetTidbGroupId() == 0 {
				newClusterComponents[i].TidbGroupId = defaultTidbGroupId // set default tidb group id
			}
			// if current tiproxy is not empty, tiproxy spu name cannot be updated
			if currTiProxyCount > 0 && c.GetSpuName() != "" && c.GetSpuName() != currTiProxySpuName {
				return nil, bizerr.New(bizerr.ErrCodeParams, "tiproxy node type cannot be updated")
			}
			// if current tiproxy is empty, tiproxy spu name cannot be empty
			if currTiProxyCount == 0 && c.GetSpuName() == "" {
				return nil, bizerr.New(bizerr.ErrCodeParams, "tiproxy node type cannot be empty")
			}
			// if new spuname is not empty, all tiproxy spuname must be the same
			if c.GetSpuName() != "" {
				if newTiProxySpuName != "" && c.GetSpuName() != newTiProxySpuName {
					return nil, bizerr.New(
						bizerr.ErrCodeParams,
						"multi tiproxy group is not allowed set different node spec",
					)
				}
				newTiProxySpuName = c.GetSpuName()
			} else {
				// if new spuname is empty, set the current spuname.
				newTiProxySpuName = currTiProxySpuName
			}

			// remove the tidb node group from the map
			delete(currTiproxyNodeGroup, c.GetTidbGroupId())
		}
	}

	// full tidb node group and tidb proxy node group
	for _, c := range currTidbNodeGroup {
		if newTidbSpuName != "" {
			c.SpuName = newTidbSpuName
		}
		newClusterComponents = append(newClusterComponents, c)
	}

	for _, c := range currTiproxyNodeGroup {
		if newTiProxySpuName != "" {
			c.SpuName = newTiProxySpuName
		}
		newClusterComponents = append(newClusterComponents, c)
	}

	return newClusterComponents, nil
}
