package common

import (
	"strconv"

	"github.com/samber/lo"

	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"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
	"github.com/tidbcloud/tidb-management-service/utilities/spu/storagetype"
)

func BuildPriceClusterComponents(
	tidbNodeSpecKey, tiproxyNodeSpecKey string,
	tidbCount, tikvCount, tiFlashCount, tiproxyCount int32,
	tikvStorageType, tiFlashStorageType openapidedicatedv1beta1.Cluster_StorageNodeSetting_StorageType,
	tikvSize, tiFlashSize int32,
	tikvRaftIops int32,
	tikvDataStoreIops int32,
	cloudProvider openapicommonv1beta1.Region_CloudProvider,
	tikvNodeSpec, tiflashNodeSpec *openapidedicatedv1beta1.NodeSpec,
) ([]*globaldedicatedv1.Cluster_ClusterComponent, bizerr.BizError) {
	if tikvStorageType == openapidedicatedv1beta1.Cluster_StorageNodeSetting_STORAGE_TYPE_UNSPECIFIED {
		return nil, bizerr.New(bizerr.ErrCodeParams, "tikv storage type is required")
	}
	if tiFlashCount > 0 && tiFlashStorageType == openapidedicatedv1beta1.Cluster_StorageNodeSetting_STORAGE_TYPE_UNSPECIFIED {
		return nil, bizerr.New(bizerr.ErrCodeParams, "tiflash storage type is required")
	}

	return BuildCreateClusterComponents(
		tidbNodeSpecKey,
		tiproxyNodeSpecKey,
		"",
		tidbCount,
		tikvCount,
		tiFlashCount,
		tiproxyCount,
		tikvStorageType,
		tiFlashStorageType,
		tikvSize,
		tiFlashSize,
		tikvRaftIops,
		tikvDataStoreIops,
		cloudProvider,
		tikvNodeSpec,
		tiflashNodeSpec,
	)
}

// BuildCreateClusterComponents builds the cluster components for creating/resuming a dedicated cluster.
// TiDB and TiProxy instances across different node groups share the same SPU spec.
func BuildCreateClusterComponents(
	tidbNodeSpecKey, tiproxyNodeSpecKey, version string,
	tidbCount, tikvCount, tiFlashCount, tiproxyCount int32,
	tikvStorageType, tiFlashStorageType openapidedicatedv1beta1.Cluster_StorageNodeSetting_StorageType,
	tikvSize, tiFlashSize int32,
	tikvRaftIops int32,
	tikvDataStoreIops int32,
	cloudProvider openapicommonv1beta1.Region_CloudProvider,
	tikvNodeSpec, tiflashNodeSpec *openapidedicatedv1beta1.NodeSpec,
) ([]*globaldedicatedv1.Cluster_ClusterComponent, bizerr.BizError) {
	if tidbNodeSpecKey == "" {
		return nil, bizerr.New(bizerr.ErrCodeParams, "tidb node spec key is required")
	}
	if tikvNodeSpec.GetNodeSpecKey() == "" {
		return nil, bizerr.New(bizerr.ErrCodeParams, "tikv node spec key is required")
	}

	res := make([]*globaldedicatedv1.Cluster_ClusterComponent, 0, 4)

	// Build tidb component.
	res = append(res, &globaldedicatedv1.Cluster_ClusterComponent{
		ComponentType: globaldedicatedv1.ComponentType_TIDB,
		SpuName:       tidbNodeSpecKey,
		Count:         lo.ToPtr(uint32(tidbCount)),
		TidbGroupId:   0, // default work pool
	})

	// Build tikv component.
	tikvStorages, err := storagetype.BuildRequestTikvStorage(
		tikvSize,
		lo.ToPtr(tikvRaftIops),
		lo.ToPtr(tikvDataStoreIops),
		cloudProvider,
		version,
		tikvStorageType,
		openapidedicatedv1beta1.Cluster_StorageNodeSetting_STORAGE_TYPE_UNSPECIFIED,
		tikvNodeSpec,
	)
	if err != nil {
		return nil, err
	}
	res = append(res, &globaldedicatedv1.Cluster_ClusterComponent{
		ComponentType: globaldedicatedv1.ComponentType_TIKV,
		SpuName:       tikvNodeSpec.GetNodeSpecKey(),
		Count:         lo.ToPtr(uint32(tikvCount)),
		Storages:      tikvStorages,
	})

	// Build tiflash component.
	if tiflashNodeSpec.GetNodeSpecKey() != "" {
		tiflashStorages, err := storagetype.BuildRequestTiflashStorage(
			tiFlashSize,
			cloudProvider,
			version,
			tiFlashStorageType,
			openapidedicatedv1beta1.Cluster_StorageNodeSetting_STORAGE_TYPE_UNSPECIFIED,
			tiflashNodeSpec,
		)
		if err != nil {
			return nil, err
		}
		res = append(res, &globaldedicatedv1.Cluster_ClusterComponent{
			ComponentType: globaldedicatedv1.ComponentType_TIFLASH,
			SpuName:       tiflashNodeSpec.GetNodeSpecKey(),
			Count:         lo.ToPtr(uint32(tiFlashCount)),
			Storages:      tiflashStorages,
		})
	}

	// Build tiproxy component.
	if tiproxyNodeSpecKey != "" {
		if tiproxyCount <= 0 {
			return nil, bizerr.New(bizerr.ErrCodeParams, "tiproxy node count is invalid")
		}
		res = append(res, &globaldedicatedv1.Cluster_ClusterComponent{
			ComponentType: globaldedicatedv1.ComponentType_TIPROXY,
			SpuName:       tiproxyNodeSpecKey,
			Count:         lo.ToPtr(uint32(tiproxyCount)),
			TidbGroupId:   0, // default work pool
		})
	}

	return res, nil
}

// BuildUpdateGlobalComponents builds the cluster components for updating a dedicated cluster.
func BuildUpdateGlobalComponents(
	req *openapidedicatedv1beta1.UpdateClusterRequest_Cluster,
	globalCluster *globaldedicatedv1.Cluster,
	tikvNodeSpec, tiflashNodeSpec *openapidedicatedv1beta1.NodeSpec,
) ([]*globaldedicatedv1.Cluster_ClusterComponent, bizerr.BizError) {
	components := make([]*globaldedicatedv1.Cluster_ClusterComponent, 0)
	version := globalCluster.GetVersion()

	getNodeCount := func(nodeCount *int32) *uint32 {
		if nodeCount == nil {
			return nil
		}
		return lo.ToPtr(uint32(*nodeCount))
	}

	// scale tidb
	if req.GetTidbNodeSetting() != nil {
		newSpuName := req.GetTidbNodeSetting().GetNodeSpecKey()

		switch {
		case len(req.GetTidbNodeSetting().GetTidbNodeGroups()) == 0:
			// append default tidb group
			components = append(components, &globaldedicatedv1.Cluster_ClusterComponent{
				ComponentType: globaldedicatedv1.ComponentType_TIDB,
				SpuName:       newSpuName,
			})

		default:
			// append multiple tidb group
			var modifyGroup = make(map[uint64]struct{}, len(req.GetTidbNodeSetting().GetTidbNodeGroups()))
			for _, group := range req.GetTidbNodeSetting().GetTidbNodeGroups() {
				var (
					groupId uint64
					err     error
				)
				if group.GetTidbNodeGroupId() == "" {
					groupId = 0 // 0 means default tidb group
				} else {
					groupId, err = strconv.ParseUint(group.GetTidbNodeGroupId(), 10, 64)
					if err != nil {
						return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "invalid tidb node group id")
					}
				}

				// check duplicate tidb node group id
				if _, ok := modifyGroup[groupId]; ok {
					return nil, bizerr.New(bizerr.ErrCodeParams, "duplicate tidb node group id")
				}
				modifyGroup[groupId] = struct{}{}

				if group.NodeCount != nil && group.GetNodeCount() == 0 {
					return nil, bizerr.New(
						bizerr.ErrCodeParams,
						"can not set node count to 0 in update operation, please use tidbNodeGroup related method to delete the group",
					)
				}

				component := &globaldedicatedv1.Cluster_ClusterComponent{
					ComponentType: globaldedicatedv1.ComponentType_TIDB,
					Count:         lo.EmptyableToPtr(uint32(group.GetNodeCount())),
					SpuName:       newSpuName,
					TidbGroupId:   groupId,
				}
				components = append(components, component)

				if group.GetTiproxySetting() != nil {
					tiproxyComponent := &globaldedicatedv1.Cluster_ClusterComponent{
						ComponentType: globaldedicatedv1.ComponentType_TIPROXY,
						Count:         getNodeCount(group.GetTiproxySetting().NodeCount),
						TidbGroupId:   groupId,
					}
					if group.GetTiproxySetting().GetNodeSpecKey() != "" {
						tiproxyComponent.SpuName = group.GetTiproxySetting().GetNodeSpecKey()
					}
					components = append(components, tiproxyComponent)
				}
			}
		}
	}

	// scale tikv
	if req.GetTikvNodeSetting() != nil {
		if req.GetTikvNodeSetting().NodeCount != nil && *req.GetTikvNodeSetting().NodeCount == 0 {
			return nil, bizerr.New(bizerr.ErrCodeParams, "tidb node count can not be 0")
		}

		tikvComp, _ := lo.Find(
			globalCluster.GetClusterComponents(),
			func(i *globaldedicatedv1.Cluster_ClusterComponent) bool {
				return i.GetComponentType() == globaldedicatedv1.ComponentType_TIKV
			},
		)
		curTikvSt := storagetype.BuildCurrentTikvStorageType(
			tikvComp.GetStorages()[globaldedicatedv1.Sku_DATA.String()].GetVolumeType(),
			tikvComp.GetStorages()[globaldedicatedv1.Sku_RAFT_LOG.String()].GetVolumeType(),
		)

		storages, err := storagetype.BuildRequestTikvStorage(
			req.GetTikvNodeSetting().GetStorageSizeGi(),
			req.GetTikvNodeSetting().RaftStoreIops,
			req.GetTikvNodeSetting().DataStoreIops,
			openapicommonv1beta1.Region_CloudProvider(globalCluster.GetCloudProviderId()),
			version,
			req.GetTikvNodeSetting().GetStorageType(),
			curTikvSt,
			tikvNodeSpec,
		)
		if err != nil {
			return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "failed to build tikv disk")
		}

		components = append(components, &globaldedicatedv1.Cluster_ClusterComponent{
			ComponentType: globaldedicatedv1.ComponentType_TIKV,
			Count:         getNodeCount(req.GetTikvNodeSetting().NodeCount),
			SpuName:       req.GetTikvNodeSetting().GetNodeSpecKey(),
			Storages:      storages,
		})
	}

	// scale tiflash
	if req.GetTiflashNodeSetting() != nil {
		tiflashComp, _ := lo.Find(
			globalCluster.GetClusterComponents(),
			func(i *globaldedicatedv1.Cluster_ClusterComponent) bool {
				return i.GetComponentType() == globaldedicatedv1.ComponentType_TIFLASH
			},
		)

		// Only build TiFlash component when there is existing TiFlash instances or the request wants to scale TiFlash nodes.
		if tiflashComp.GetCount() > 0 || req.GetTiflashNodeSetting().GetNodeCount() > 0 {
			curTiflashSt := storagetype.BuildCurrentTiflashStorageType(
				tiflashComp.GetStorages()[globaldedicatedv1.Sku_DATA.String()].GetVolumeType(),
				tiflashComp.GetStorages()[globaldedicatedv1.Sku_RAFT_LOG.String()].GetVolumeType(),
			)

			storages, err := storagetype.BuildRequestTiflashStorage(
				req.GetTiflashNodeSetting().GetStorageSizeGi(),
				openapicommonv1beta1.Region_CloudProvider(globalCluster.GetCloudProviderId()),
				version,
				req.GetTiflashNodeSetting().GetStorageType(),
				curTiflashSt,
				tiflashNodeSpec,
			)
			if err != nil {
				return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "failed to build tiflash disk")
			}

			components = append(components, &globaldedicatedv1.Cluster_ClusterComponent{
				ComponentType: globaldedicatedv1.ComponentType_TIFLASH,
				Count:         getNodeCount(req.GetTiflashNodeSetting().NodeCount),
				SpuName:       req.GetTiflashNodeSetting().GetNodeSpecKey(),
				Storages:      storages,
			})
		}
	}

	return components, nil
}
