package cluster

import (
	"context"
	"errors"
	"strconv"
	"strings"

	"github.com/samber/lo"
	"github.com/tidbcloud/tiinf/sdk/auditlog"
	"google.golang.org/protobuf/proto"

	bffclusterv1beta1 "github.com/tidbcloud/tidb-management-service/api/spec/bff/cluster_management/v1beta1"
	globaldedicatedv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/dedicated/v1"
	openapicommonv1beta1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_cloud_open_api/common/v1beta1"
	openapidedicatedv1beta1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_cloud_open_api/dedicated/v1beta1"
	appcommon "github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/app/common"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/domain/adapter/eventbus"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
	"github.com/tidbcloud/tidb-management-service/utilities/featuregate"
	"github.com/tidbcloud/tidb-management-service/utilities/grpc/metadata"
	"github.com/tidbcloud/tidb-management-service/utilities/spu"
)

func (s *Server) AdvancedModifyDedicatedCluster(
	ctx context.Context,
	request *bffclusterv1beta1.AdvancedModifyDedicatedClusterRequest,
) (*bffclusterv1beta1.Cluster, error) {
	var (
		err            error
		reqMeta        *metadata.RequestMeta
		req            = request.GetCluster()
		currentCluster *openapidedicatedv1beta1.Cluster
		desiredCluster *openapidedicatedv1beta1.Cluster
	)

	detailsFunc := func() auditlog.Details {
		details := eventbus.NewAuditLogDetails(eventbus.TierTypeDedicated)
		details = eventbus.AppendDedicatedClusterDetails(details, currentCluster)

		if req.GetTidbNodeSetting() != nil {
			details[auditlog.KeyNewTidbSize] = desiredCluster.GetTidbNodeSetting().GetNodeSpecKey()
			for _, group := range desiredCluster.GetTidbNodeSetting().GetTidbNodeGroups() {
				if _, ok := details[auditlog.KeyNewTidbQuantity]; !ok {
					details[auditlog.KeyNewTidbQuantity] = int32(0)
				}
				// count all tidb node
				// not record the tidb group id yet
				details[auditlog.KeyNewTidbQuantity] = details[auditlog.KeyNewTidbQuantity].(int32) + group.GetNodeCount()
				if group.GetTiproxySetting() != nil {
					if group.GetTiproxySetting().GetNodeSpecKey() != "" {
						details[auditlog.KeyNewTiProxySize] = group.GetTiproxySetting().GetNodeSpecKey()
					}
					if _, ok := details[auditlog.KeyNewTiProxyQuantity]; !ok {
						details[auditlog.KeyNewTiProxyQuantity] = int32(0)
					}
					details[auditlog.KeyNewTiProxyQuantity] = details[auditlog.KeyNewTiProxyQuantity].(int32) + group.GetTiproxySetting().
						GetNodeCount()
				}
			}
		}

		if req.GetTikvNodeSetting() != nil {
			details[auditlog.KeyNewTikvSize] = desiredCluster.GetTikvNodeSetting().GetNodeSpecKey()
			details[auditlog.KeyNewTikvQuantity] = desiredCluster.GetTikvNodeSetting().GetNodeCount()
			details[auditlog.KeyNewTikvStorageSize] = desiredCluster.GetTikvNodeSetting().GetStorageSizeGi()
			if req.GetTikvNodeSetting().GetStorageType() != openapidedicatedv1beta1.Cluster_StorageNodeSetting_STORAGE_TYPE_UNSPECIFIED {
				details[auditlog.KeyNewTikvStorageType] = req.GetTikvNodeSetting().GetStorageType().String()
			}
			if req.GetTikvNodeSetting().GetDataStoreIops() > 0 {
				details[auditlog.KeyNewTikvDataStoreIops] = req.GetTikvNodeSetting().GetDataStoreIops()
			}
		}

		if req.GetTiflashNodeSetting() != nil {
			details[auditlog.KeyNewTiflashSize] = desiredCluster.GetTiflashNodeSetting().GetNodeSpecKey()
			details[auditlog.KeyNewTiflashQuantity] = desiredCluster.GetTiflashNodeSetting().GetNodeCount()
			details[auditlog.KeyNewTiflashStorageSize] = desiredCluster.GetTiflashNodeSetting().GetStorageSizeGi()
		}

		// fulfill old cluster component
		if currentCluster != nil {
			// tidb
			details[auditlog.KeyOldTidbSize] = currentCluster.GetTidbNodeSetting().GetNodeSpecKey()
			for _, group := range currentCluster.GetTidbNodeSetting().GetTidbNodeGroups() {
				if _, ok := details[auditlog.KeyOldTidbQuantity]; !ok {
					details[auditlog.KeyOldTidbQuantity] = int32(0)
				}
				details[auditlog.KeyOldTidbQuantity] = details[auditlog.KeyOldTidbQuantity].(int32) + group.GetNodeCount()
				if group.GetTiproxySetting() != nil {
					if group.GetTiproxySetting().GetNodeSpecKey() != "" {
						details[auditlog.KeyOldTiProxySize] = group.GetTiproxySetting().GetNodeSpecKey()
					}
					if _, ok := details[auditlog.KeyOldTiProxyQuantity]; !ok {
						details[auditlog.KeyOldTiProxyQuantity] = int32(0)
					}
					details[auditlog.KeyOldTiProxyQuantity] = details[auditlog.KeyOldTiProxyQuantity].(int32) + group.GetTiproxySetting().
						GetNodeCount()
				}
			}

			// tikv
			details[auditlog.KeyOldTikvSize] = currentCluster.GetTikvNodeSetting().GetNodeSpecKey()
			details[auditlog.KeyOldTikvQuantity] = currentCluster.GetTikvNodeSetting().GetNodeCount()
			details[auditlog.KeyOldTikvStorageSize] = currentCluster.GetTikvNodeSetting().GetStorageSizeGi()
			if req.GetTikvNodeSetting().GetStorageType() != openapidedicatedv1beta1.Cluster_StorageNodeSetting_STORAGE_TYPE_UNSPECIFIED {
				details[auditlog.KeyOldTikvStorageType] = currentCluster.GetTikvNodeSetting().GetStorageType().String()
			}
			if currentCluster.GetTikvNodeSetting().GetStorageType() == openapidedicatedv1beta1.Cluster_StorageNodeSetting_Plus &&
				currentCluster.GetCloudProvider() == openapicommonv1beta1.Region_aws &&
				currentCluster.GetTikvNodeSetting().GetDataStoreIops() > 0 {
				details[auditlog.KeyOldTikvDataStoreIops] = currentCluster.GetTikvNodeSetting().GetDataStoreIops()
			}

			// tiflash
			if currentCluster.GetTiflashNodeSetting() != nil {
				details[auditlog.KeyOldTiflashSize] = currentCluster.GetTiflashNodeSetting().GetNodeSpecKey()
				details[auditlog.KeyOldTiflashQuantity] = currentCluster.GetTiflashNodeSetting().GetNodeCount()
				details[auditlog.KeyOldTiflashStorageSize] = currentCluster.GetTiflashNodeSetting().GetStorageSizeGi()
			}
		}

		return details
	}
	defer func() {
		s.auditLogSender.SubmitAuditLog(ctx, auditlog.EventScaleCluster, reqMeta, detailsFunc, err)
	}()

	// Extract request meta.
	reqMeta, err = metadata.ExtractRequestMetaWithCheckPermission(ctx)
	if err != nil {
		return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "failed to extract request metadata")
	}

	if request.Cluster == nil {
		return nil, bizerr.New(bizerr.ErrCodeParams, "cluster object should not be nil")
	}

	// get cluster
	var commonCluster *bffclusterv1beta1.Cluster
	commonCluster, err = s.GetCluster(
		ctx,
		&bffclusterv1beta1.GetClusterRequest{ClusterId: request.GetCluster().GetClusterId()},
	)
	if err != nil {
		return nil, err
	}

	if commonCluster.GetClusterType() != bffclusterv1beta1.Cluster_DEDICATED {
		return nil, bizerr.New(
			bizerr.ErrCodeParams,
			"advanced modify cluster only support dedicated cluster, got %s",
			commonCluster.GetClusterType(),
		)
	}

	// for audit log to record old cluster component
	currentCluster = commonCluster.GetDedicatedCluster()
	var ok bool
	desiredCluster, ok = proto.Clone(currentCluster).(*openapidedicatedv1beta1.Cluster)
	if !ok {
		return nil, bizerr.New(bizerr.ErrCodeUnknown, "failed to clone current cluster")
	}

	// Caution: Count of newly created tidb groups are all added to the count of group[0].
	// Then these groups are all dropped!
	if err := s.validateWholeCluster(ctx, request.GetCluster(), currentCluster); err != nil {
		return nil, bizerr.WrapError(err, "failed to pre check")
	}

	err = s.dbProvider.Transaction(ctx, func(ctx context.Context) error {
		orchestrateRequests := advancedRequestTransform(request)

		// Caution: Each component is validated separately!
		if err := s.orchestrator.ValidateOneByOne(ctx, orchestrateRequests); err != nil {
			return err
		}

		return s.orchestrator.Commit(ctx, request.Cluster.ClusterId, orchestrateRequests, request)
	})
	if err != nil {
		return nil, err
	}

	// merge request cluster
	desiredCluster = s.mergeAdvancedCluster(ctx, desiredCluster, request.GetCluster())

	return &bffclusterv1beta1.Cluster{
		ClusterType: bffclusterv1beta1.Cluster_DEDICATED,
		Cluster:     &bffclusterv1beta1.Cluster_DedicatedCluster{DedicatedCluster: desiredCluster},
	}, nil
}

// Caution: Count of newly created tidb groups are all added to the count of group[0].
// Then these groups are all dropped!
func (s *Server) validateWholeCluster(
	ctx context.Context,
	reqCluster *bffclusterv1beta1.AdvancedModifyDedicatedClusterRequest_Cluster,
	currentCluster *openapidedicatedv1beta1.Cluster,
) error {
	tidbNodeSetting := reqCluster.GetTidbNodeSetting()
	if tidbNodeSetting == nil {
		return nil
	}

	// check deleted tidb group with scale down tidb node spec
	if len(tidbNodeSetting.DeleteTidbNodeGroupIds) > 0 &&
		(strings.HasPrefix(tidbNodeSetting.GetNodeSpecKey(), "4C") || strings.HasPrefix(tidbNodeSetting.GetNodeSpecKey(), "2C")) {
		return errors.New("please delete non-DefaultGroup TiDB Node Groups before scale down tidb node spec")
	}

	// check tidb group name
	tgNames := lo.SliceToMap(
		tidbNodeSetting.CreateTidbNodeGroups,
		func(item *bffclusterv1beta1.AdvancedModifyDedicatedClusterRequest_TidbNodeGroupCreate) (string, any) {
			return item.DisplayName, struct{}{}
		},
	)
	if len(tgNames) != len(tidbNodeSetting.CreateTidbNodeGroups) {
		return errors.New("duplicate tidb group name")
	}

	updateCluster, updated, err := buildValidateOnlyRequest(currentCluster, reqCluster)
	if err != nil {
		return err
	}
	if !updated {
		return nil
	}

	_, err = s.openAPIDedicatedClusterServer.UpdateCluster(ctx, updateCluster)

	return err
}

func buildValidateOnlyRequest(
	currentCluster *openapidedicatedv1beta1.Cluster,
	reqCluster *bffclusterv1beta1.AdvancedModifyDedicatedClusterRequest_Cluster,
) (*openapidedicatedv1beta1.UpdateClusterRequest, bool, error) {
	// Extract TiDB node settings from the req cluster
	tidbNodeSetting := reqCluster.GetTidbNodeSetting()

	// Validate request has at least one operation specified
	if len(tidbNodeSetting.CreateTidbNodeGroups) == 0 && len(tidbNodeSetting.UpdateTidbNodeGroups) == 0 &&
		len(tidbNodeSetting.DeleteTidbNodeGroupIds) == 0 {
		return nil, false, errors.New("empty request body")
	}

	// If request only contains deletions, no quota check is needed
	if len(tidbNodeSetting.CreateTidbNodeGroups) == 0 && len(tidbNodeSetting.UpdateTidbNodeGroups) == 0 {
		return nil, false, nil
	}

	// Map TiDB node group updates by ID for quick lookup
	updateTGId2Count := lo.SliceToMap(
		tidbNodeSetting.UpdateTidbNodeGroups,
		func(item *bffclusterv1beta1.AdvancedModifyDedicatedClusterRequest_TidbNodeGroupUpdate) (string, int32) {
			return item.TidbNodeGroupId, item.NodeCount
		},
	)

	// Create a set of group IDs to be deleted for quick lookup
	deleteTGIdMap := lo.SliceToMap(tidbNodeSetting.DeleteTidbNodeGroupIds, func(item string) (string, any) {
		return item, struct{}{}
	})

	// Map current TiDB node groups by ID for comparison
	currentTGId2Count := lo.SliceToMap(
		currentCluster.TidbNodeSetting.TidbNodeGroups,
		func(item *openapidedicatedv1beta1.TidbNodeGroup) (string, int32) {
			return item.TidbNodeGroupId, item.NodeCount
		},
	)

	// Construct the updated node groups list based on current state
	// and requested modifications (excluding deleted groups)
	newTGs := make([]*openapidedicatedv1beta1.UpdateClusterRequest_TidbNodeSetting_TidbNodeGroup, 0)
	for tgId, cCount := range currentTGId2Count {
		// Skip groups marked for deletion
		if _, ok := deleteTGIdMap[tgId]; ok {
			continue
		}

		// Use updated count if specified, otherwise keep current count
		nCount, ok := updateTGId2Count[tgId]
		if !ok {
			nCount = cCount
		}

		newTGs = append(newTGs, &openapidedicatedv1beta1.UpdateClusterRequest_TidbNodeSetting_TidbNodeGroup{
			TidbNodeGroupId: tgId,
			NodeCount:       lo.ToPtr(nCount),
		})
	}

	// Calculate total node count from newly created groups
	addTidbCount := lo.SumBy(
		tidbNodeSetting.CreateTidbNodeGroups,
		func(item *bffclusterv1beta1.AdvancedModifyDedicatedClusterRequest_TidbNodeGroupCreate) int32 {
			return item.NodeCount
		},
	)

	// Add the new nodes to the first group for validation purposes
	// This simplifies quota validation by treating all new nodes as part of an existing group
	newTGs[0] = &openapidedicatedv1beta1.UpdateClusterRequest_TidbNodeSetting_TidbNodeGroup{
		TidbNodeGroupId: newTGs[0].TidbNodeGroupId,
		NodeCount:       lo.ToPtr(newTGs[0].GetNodeCount() + addTidbCount),
	}

	// Construct and return the validation-only request
	return &openapidedicatedv1beta1.UpdateClusterRequest{
		Cluster: &openapidedicatedv1beta1.UpdateClusterRequest_Cluster{
			ClusterId: currentCluster.ClusterId,
			TidbNodeSetting: &openapidedicatedv1beta1.UpdateClusterRequest_TidbNodeSetting{
				NodeSpecKey:    tidbNodeSetting.GetNodeSpecKey(),
				TidbNodeGroups: newTGs,
			},
			TikvNodeSetting:    reqCluster.GetTikvNodeSetting(),
			TiflashNodeSetting: reqCluster.GetTiflashNodeSetting(),
		},
		ValidateOnly: true,
	}, true, nil
}

func (s *Server) CheckModificationRisks(
	ctx context.Context,
	req *bffclusterv1beta1.CheckModificationRisksRequest,
) (*bffclusterv1beta1.CheckModificationRisksResponse, error) {
	// Extract request meta.
	_, err := metadata.ExtractRequestMetaWithCheckPermission(ctx)
	if err != nil {
		return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "failed to extract request metadata")
	}

	if req.Cluster == nil {
		return nil, bizerr.New(bizerr.ErrCodeParams, "cluster object should not be nil")
	}

	clusterId, err := strconv.ParseUint(req.GetCluster().GetClusterId(), 10, 64)
	if err != nil {
		return nil, bizerr.New(bizerr.ErrCodeParams, "invalid cluster id: %s", req.GetCluster().GetClusterId())
	}

	globalCluster, err := s.dedicatedCli.GetCluster(
		ctx,
		&globaldedicatedv1.GetClusterRequest{ClusterId: clusterId},
	)
	if err != nil {
		return nil, err
	}

	if !s.featGateCli.CheckFeatureEnabled(
		featuregate.EnableModifyRiskChecks,
		globalCluster.GetOrganizationId(),
		globalCluster.GetProjectId(),
		globalCluster.GetCloudProviderId().String(),
		globalCluster.GetRegionId().String(),
	) {
		return &bffclusterv1beta1.CheckModificationRisksResponse{Risked: false}, nil
	}

	// get node spec.
	tikvNodeSpec, tiflashNodeSpec, err := appcommon.GetUpdateNodeSpecs(
		ctx,
		s.openAPIDedicatedRegionServer,
		req.GetCluster().GetTikvNodeSetting().GetNodeSpecKey(),
		req.GetCluster().GetTiflashNodeSetting().GetNodeSpecKey(),
		strconv.FormatUint(globalCluster.GetProjectId(), 10),
		globalCluster,
	)
	if err != nil {
		return nil, err
	}

	components, err := appcommon.BuildUpdateGlobalComponents(&openapidedicatedv1beta1.UpdateClusterRequest_Cluster{
		TikvNodeSetting: req.GetCluster().GetTikvNodeSetting(),
	}, globalCluster, tikvNodeSpec, tiflashNodeSpec)
	if err != nil {
		return nil, err
	}

	resp, err := s.dedicatedCli.BuildDesiredComponents(ctx, &globaldedicatedv1.BuildDesiredComponentsRequest{
		Target: &globaldedicatedv1.BuildDesiredComponentsRequest_ModifyCluster{
			ModifyCluster: &globaldedicatedv1.BuildDesiredComponentsRequest_Modify{
				ClusterId:                  clusterId,
				RequestedClusterComponents: components,
			},
		},
	})
	if err != nil {
		return nil, err
	}
	if len(resp.GetClusterDiffs()) != 1 {
		return nil, bizerr.New(
			bizerr.ErrCodeUnknown,
			"unexpected response from BuildDesiredComponents, expected 1 diff, got %d",
			len(resp.GetClusterDiffs()),
		)
	}

	desiredPd, ok := lo.Find(
		resp.GetClusterDiffs()[0].GetDesiredEffectiveComponents(),
		func(c *globaldedicatedv1.Cluster_ClusterComponent) bool {
			return c.GetComponentType() == globaldedicatedv1.ComponentType_PD
		},
	)
	// Pd could be no change after scale.
	if !ok {
		return &bffclusterv1beta1.CheckModificationRisksResponse{Risked: false}, nil
	}

	currentPd, ok := lo.Find(
		resp.GetClusterDiffs()[0].GetCurrentEffectiveComponents(),
		func(c *globaldedicatedv1.Cluster_ClusterComponent) bool {
			return c.GetComponentType() == globaldedicatedv1.ComponentType_PD
		},
	)
	if !ok {
		return nil, bizerr.New(bizerr.ErrCodeUnknown, "current cluster does not have PD component")
	}

	if spu.CompareComponentSize(
		&spu.ComponentSize{
			CPUNum: currentPd.GetCpuNum(),
			MemGi:  currentPd.GetMemorySizeGi(),
		},
		&spu.ComponentSize{
			CPUNum: desiredPd.GetCpuNum(),
			MemGi:  desiredPd.GetMemorySizeGi(),
		},
	) < 0 {
		return &bffclusterv1beta1.CheckModificationRisksResponse{
			Risked: true,
			Risks: []*bffclusterv1beta1.CheckModificationRisksResponse_Risk{
				{
					ComponentType: openapidedicatedv1beta1.ComponentType_TIKV,
					BizCode:       int64(bizerr.ErrCodePdScale),
					Description:   "To ensure optimal performance after TiKV scaling up, this operation requires scaling of the internal service. As this process may cause temporary SQL connection interruptions, it's recommended to perform it during a low traffic period.",
				},
			},
		}, nil
	}

	return &bffclusterv1beta1.CheckModificationRisksResponse{Risked: false}, nil
}
