package region

import (
	"cmp"
	"context"
	"fmt"
	"slices"
	"strconv"

	"github.com/samber/lo"

	globalcommonv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/common/v1"
	globaldedicatedv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/dedicated/v1"
	openapidedicatedv1beta1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_cloud_open_api/dedicated/v1beta1"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/pkg/common"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
	"github.com/tidbcloud/tidb-management-service/utilities/enum"
	"github.com/tidbcloud/tidb-management-service/utilities/grpc/metadata"
	"github.com/tidbcloud/tidb-management-service/utilities/spu/storagetype"
)

const (
	// Copy from core portal.
	recommendedNodeCpuNum        int32 = 8
	recommendedTiFlashNodeCpuNum int32 = 16
	recommendedTiProxyNodeCpuNum int32 = 2
)

func (t serverImpl) GetNodeSpecForPrice(
	ctx context.Context,
	req *openapidedicatedv1beta1.GetNodeSpecRequest,
) (*openapidedicatedv1beta1.NodeSpec, error) {
	ctx, span := tracer.Start(ctx, "GetNodeSpecForPrice")
	defer span.End()

	params, err := t.parseAndValidateCommonParams(ctx, req.GetProjectId(), req.GetClusterId(), req.GetRegionId())
	if err != nil {
		return nil, err
	}

	spu, err_ := t.dedicatedCli.GetSpu(ctx, &globaldedicatedv1.GetSpuRequest{
		ByAttributes: &globaldedicatedv1.GetSpuRequest_ByAttributes{
			CloudProvider:     params.cloudProvider,
			Region:            params.regionName,
			ComponentType:     common.CvtComponentTypeToGlobal(req.GetComponentType()),
			SpuName:           req.GetNodeSpecKey(),
			TenantId:          params.reqMeta.OrgID,
			ProjectId:         params.projectId,
			ExistingClusterId: params.clusterId,
		},
	})
	if err_ != nil {
		return nil, bizerr.WrapError(err_, "failed to get spu")
	}

	return spu2NodeSpec(spu, req.GetRegionId(), "", nil), nil
}

func (t serverImpl) GetNodeSpec(
	ctx context.Context,
	req *openapidedicatedv1beta1.GetNodeSpecRequest,
) (*openapidedicatedv1beta1.NodeSpec, error) {
	ctx, span := tracer.Start(ctx, "GetNodeSpec")
	defer span.End()

	params, err := t.parseAndValidateCommonParams(ctx, req.GetProjectId(), req.GetClusterId(), req.GetRegionId())
	if err != nil {
		return nil, err
	}

	version, globalCluster, err := t.getVersionAndClusterFromGlobal(
		ctx,
		params.reqMeta.OrgID,
		params.projectId,
		params.clusterId,
		req.GetTidbVersion(),
		params.cloudProvider,
		params.regionName,
	)
	if err != nil {
		return nil, err
	}

	thirdProviders, err_ := t.accountCli.GetTenantThirdPartyAccountProviders(ctx, params.reqMeta.OrgID)
	if err_ != nil {
		return nil, bizerr.WrapError(err_, "failed to get third party account providers")
	}
	if len(thirdProviders) > 0 {
		if !lo.Contains(thirdProviders, common.GlobalProviderToOpenApi(params.cloudProvider)) {
			return nil, bizerr.New(bizerr.ErrCodePermission, bizerr.ErrMsg3rdPartyAccount, params.cloudProvider)
		}
	}

	spu, err_ := t.dedicatedCli.GetSpu(ctx, &globaldedicatedv1.GetSpuRequest{
		ByAttributes: &globaldedicatedv1.GetSpuRequest_ByAttributes{
			CloudProvider:     params.cloudProvider,
			Region:            params.regionName,
			ComponentType:     common.CvtComponentTypeToGlobal(req.GetComponentType()),
			SpuName:           req.GetNodeSpecKey(),
			TenantId:          params.reqMeta.OrgID,
			ProjectId:         params.projectId,
			ExistingClusterId: params.clusterId,
		},
	})
	if err_ != nil {
		return nil, bizerr.WrapError(err_, "failed to get spu")
	}

	return spu2NodeSpec(spu, req.GetRegionId(), version, globalCluster), nil
}

func (t serverImpl) ListNodeSpecs(
	ctx context.Context,
	req *openapidedicatedv1beta1.ListNodeSpecsRequest,
) (*openapidedicatedv1beta1.ListNodeSpecsResponse, error) {
	ctx, span := tracer.Start(ctx, "ListNodeSpecs")
	defer span.End()

	params, err := t.parseAndValidateCommonParams(ctx, req.GetProjectId(), req.GetClusterId(), req.GetRegionId())
	if err != nil {
		return nil, err
	}

	version, globalCluster, err := t.getVersionAndClusterFromGlobal(
		ctx,
		params.reqMeta.OrgID,
		params.projectId,
		params.clusterId,
		req.GetTidbVersion(),
		params.cloudProvider,
		params.regionName,
	)
	if err != nil {
		return nil, err
	}

	thirdProviders, err_ := t.accountCli.GetTenantThirdPartyAccountProviders(ctx, params.reqMeta.OrgID)
	if err_ != nil {
		return nil, bizerr.WrapError(err_, "failed to get third party account providers")
	}
	if len(thirdProviders) > 0 {
		if !lo.Contains(thirdProviders, common.GlobalProviderToOpenApi(params.cloudProvider)) {
			return nil, bizerr.New(bizerr.ErrCodePermission, bizerr.ErrMsg3rdPartyAccount, params.cloudProvider)
		}
	}

	resp, err_ := t.dedicatedCli.ListSpus(ctx, &globaldedicatedv1.ListSpusRequest{
		Request: &globaldedicatedv1.ListSpusRequest_WithSku{
			WithSku: &globaldedicatedv1.ListSpusRequest_FilterWithSku{
				TenantId:          params.reqMeta.OrgID,
				ProjectId:         params.projectId,
				ExistingClusterId: params.clusterId,
				CloudProvider:     params.cloudProvider,
				Region:            params.regionName,
			},
		},
	})
	if err_ != nil {
		return nil, bizerr.WrapError(err_, "failed to list spus")
	}

	spus := resp.GetSpus()

	if len(spus) == 0 {
		return &openapidedicatedv1beta1.ListNodeSpecsResponse{
			NodeSpecs:     []*openapidedicatedv1beta1.NodeSpec{},
			TotalSize:     0,
			NextPageToken: "",
		}, nil
	}

	spus = lo.Filter(spus, func(spu *globaldedicatedv1.Spu, _ int) bool {
		if req.GetComponentType() == openapidedicatedv1beta1.ComponentType_COMPONENT_TYPE_UNSPECIFIED {
			return true
		}
		return spu.ComponentType == common.CvtComponentTypeToGlobal(req.GetComponentType())
	})

	nodeSpecs := lo.Map(spus, func(spu *globaldedicatedv1.Spu, _ int) *openapidedicatedv1beta1.NodeSpec {
		return spu2NodeSpec(spu, req.GetRegionId(), version, globalCluster)
	})

	t.selectDefaultNodeSpecs(nodeSpecs)
	t.sortNodeSpecs(nodeSpecs)

	pagedNodeSpecs, nextToken, paginationErr := common.LocalPagination(
		nodeSpecs,
		req,
		common.ListDefaultPageSize,
		common.ListMaxPageSize,
	)
	if paginationErr != nil {
		return nil, bizerr.WrapError(paginationErr, "failed to paginate node specs")
	}

	return &openapidedicatedv1beta1.ListNodeSpecsResponse{
		NodeSpecs:     pagedNodeSpecs,
		TotalSize:     int32(len(spus)),
		NextPageToken: nextToken,
	}, nil
}

type parsedRequestParams struct {
	reqMeta       *metadata.RequestMeta
	projectId     uint64
	clusterId     uint64
	cloudProvider globalcommonv1.CloudProvider_Name
	regionName    globalcommonv1.Region_Name
}

func (t serverImpl) parseAndValidateCommonParams(
	ctx context.Context,
	projectIdStr, clusterIdStr, regionId string,
) (*parsedRequestParams, bizerr.BizError) {
	reqMeta, err := metadata.ExtractRequestMetaWithCheckPermission(ctx)
	if err != nil {
		return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "failed to extract request metadata")
	}

	var projectId uint64
	if projectIdStr == "" {
		projectId = reqMeta.DefaultProjectID
	} else {
		var parseErr error
		projectId, parseErr = strconv.ParseUint(projectIdStr, 10, 64)
		if parseErr != nil {
			return nil, bizerr.WrapErrorWithCode(parseErr, bizerr.ErrCodeParams, "invalid project id")
		}
	}

	var clusterId uint64
	if clusterIdStr != "" {
		var parseErr error
		clusterId, parseErr = strconv.ParseUint(clusterIdStr, 10, 64)
		if parseErr != nil {
			return nil, bizerr.WrapErrorWithCode(parseErr, bizerr.ErrCodeParams, "invalid cluster id")
		}
	}

	if regionId == "" {
		return nil, bizerr.New(bizerr.ErrCodeParams, "region id is required")
	}

	provider, region, parseErr := common.ParseOpenApiRegionNameOrId(regionId)
	if parseErr != nil {
		return nil, bizerr.WrapErrorWithCode(parseErr, bizerr.ErrCodeParams, "invalid region, please check and try again")
	}

	regionName_, err := enum.FromString[globalcommonv1.Region_Name](region)
	if err != nil {
		return nil, bizerr.New(bizerr.ErrCodeParams, "region %s is not supported", regionId)
	}
	regionName := globalcommonv1.Region_Name(regionName_)

	cloudProvider_, err := enum.FromString[globalcommonv1.CloudProvider_Name](provider)
	if err != nil {
		return nil, bizerr.New(bizerr.ErrCodeParams, "provider %s is not supported", provider)
	}
	cloudProvider := globalcommonv1.CloudProvider_Name(cloudProvider_)

	return &parsedRequestParams{
		reqMeta:       reqMeta,
		projectId:     projectId,
		clusterId:     clusterId,
		cloudProvider: cloudProvider,
		regionName:    regionName,
	}, nil
}

func spu2NodeSpec(spu *globaldedicatedv1.Spu, regionId, version string, globalCluster *globaldedicatedv1.Cluster) *openapidedicatedv1beta1.NodeSpec {
	out := &openapidedicatedv1beta1.NodeSpec{
		Name:                 fmt.Sprintf("componentTypes/%s/nodeSpecs/%s", spu.ComponentType.String(), spu.Name),
		NodeSpecKey:          spu.Name,
		DisplayName:          spu.DisplayName,
		Version:              spu.Version,
		RegionId:             regionId,
		VCpu:                 spu.CpuCore,
		MemorySizeGi:         spu.MemorySizeGi,
		DefaultStorageSizeGi: spu.GetDataStorage().GetDefaultSizeGi(),
		MaxStorageSizeGi:     spu.GetDataStorage().GetMaxSizeGi(),
		MinStorageSizeGi:     spu.GetDataStorage().GetMinSizeGi(),
		MaxRaftStoreIops:     lo.EmptyableToPtr(spu.GetRaftlogStorage().GetMaxIops()),
		MinRaftStoreIops:     lo.EmptyableToPtr(spu.GetRaftlogStorage().GetMinIops()),
		DefaultNodeCount:     spu.DefaultCount,
		ComponentType:        common.CvtComponentTypeToOpenapi(spu.GetComponentType()),
		StorageTypeDetails: spuStorageTypeDetails2NodeSpec(
			spu.GetComponentType(),
			spu.GetCloudProvider(),
			spu.GetStorageTypeDetails(),
		),
	}

	switch spu.ComponentType {
	case globaldedicatedv1.ComponentType_TIKV:
		tikvComp, _ := lo.Find(globalCluster.GetClusterComponents(), func(i *globaldedicatedv1.Cluster_ClusterComponent) bool {
			return i.GetComponentType() == globaldedicatedv1.ComponentType_TIKV
		})
		curSt := storagetype.BuildCurrentTikvStorageType(
			tikvComp.GetStorages()[globaldedicatedv1.Sku_DATA.String()].GetVolumeType(),
			tikvComp.GetStorages()[globaldedicatedv1.Sku_RAFT_LOG.String()].GetVolumeType(),
		)
		out.StorageTypes = storagetype.BuildSupportedTikvStorageTypes(spu, version, curSt)

	case globaldedicatedv1.ComponentType_TIFLASH:
		tiflashComp, _ := lo.Find(globalCluster.GetClusterComponents(), func(i *globaldedicatedv1.Cluster_ClusterComponent) bool {
			return i.GetComponentType() == globaldedicatedv1.ComponentType_TIFLASH
		})
		curSt := storagetype.BuildCurrentTiflashStorageType(
			tiflashComp.GetStorages()[globaldedicatedv1.Sku_DATA.String()].GetVolumeType(),
			tiflashComp.GetStorages()[globaldedicatedv1.Sku_RAFT_LOG.String()].GetVolumeType(),
		)
		out.StorageTypes = storagetype.BuildSupportedTiflashStorageTypes(spu, version, curSt)
	}

	return out
}

func spuStorageTypeDetails2NodeSpec(
	componentType globaldedicatedv1.ComponentType,
	cloudProvider globalcommonv1.CloudProvider_Name,
	details map[string]*globaldedicatedv1.Spu_StorageTypeDetail,
) map[string]*openapidedicatedv1beta1.NodeSpec_StorageTypeDetail {
	// The Open API currently exposes detailed IOPS information only for AWS TiKV Plus storage type.
	if componentType != globaldedicatedv1.ComponentType_TIKV ||
		cloudProvider != globalcommonv1.CloudProvider_AWS || len(details) == 0 {
		return nil
	}

	globalPlusStorageType := globaldedicatedv1.Spu_Plus.String()
	detail, ok := details[globalPlusStorageType]
	if !ok {
		return nil
	}

	openApiPlusStorageType := openapidedicatedv1beta1.Cluster_StorageNodeSetting_Plus.String()
	return map[string]*openapidedicatedv1beta1.NodeSpec_StorageTypeDetail{
		openApiPlusStorageType: {
			DataStorage: spuStorage2NodeSpecStorage(detail.GetDataStorage()),
		},
	}
}

func spuStorage2NodeSpecStorage(storage *globaldedicatedv1.Spu_Storage) *openapidedicatedv1beta1.NodeSpec_Storage {
	if storage == nil {
		return nil
	}

	return &openapidedicatedv1beta1.NodeSpec_Storage{
		DefaultIops: lo.EmptyableToPtr(storage.GetDefaultIops()),
		MinIops:     lo.EmptyableToPtr(storage.GetMinIops()),
		MaxIops:     lo.EmptyableToPtr(storage.GetMaxIops()),
	}
}

func (t serverImpl) getVersionAndClusterFromGlobal(
	ctx context.Context,
	orgId, projectId, clusterId uint64,
	tidbVersion string,
	cloudProvider globalcommonv1.CloudProvider_Name,
	region globalcommonv1.Region_Name,
) (string, *globaldedicatedv1.Cluster, bizerr.BizError) {
	var (
		version       string
		globalCluster *globaldedicatedv1.Cluster
	)

	if clusterId == 0 {
		if tidbVersion != "" {
			version = tidbVersion
		} else {
			versionResp, err := t.dedicatedCli.GetDefaultTidbVersion(ctx, &globaldedicatedv1.GetDefaultTidbVersionRequest{
				CloudProvider: cloudProvider,
				Region:        region,
				TenantId:      orgId,
			})
			if err != nil {
				return "", nil, bizerr.WrapError(err, "failed to get default tidb version")
			}
			version = versionResp.GetVersion()
		}
	} else {
		var err error
		globalCluster, err = t.dedicatedCli.GetCluster(ctx, &globaldedicatedv1.GetClusterRequest{ClusterId: clusterId})
		if err != nil {
			return "", nil, bizerr.WrapError(err, "failed to get cluster info")
		}
		if projectId != globalCluster.GetProjectId() {
			return "", nil, bizerr.New(bizerr.ErrCodePermission, "cluster %d does not belong to project %d", clusterId, projectId)
		}
		version = globalCluster.GetVersion()
	}

	return version, globalCluster, nil
}

func (t serverImpl) selectDefaultNodeSpecs(nodeSpecs []*openapidedicatedv1beta1.NodeSpec) {
	type groupKey struct {
		componentType openapidedicatedv1beta1.ComponentType
		version       string
	}

	group := make(map[groupKey][]*openapidedicatedv1beta1.NodeSpec)
	hasDefault := make(map[groupKey][]*openapidedicatedv1beta1.NodeSpec)

	for _, n := range nodeSpecs {
		key := groupKey{componentType: n.GetComponentType(), version: n.GetVersion()}
		group[key] = append(group[key], n)

		var recommendedCpuNum int32
		switch n.GetComponentType() {
		case openapidedicatedv1beta1.ComponentType_TIFLASH:
			recommendedCpuNum = recommendedTiFlashNodeCpuNum
		case openapidedicatedv1beta1.ComponentType_TIPROXY:
			recommendedCpuNum = recommendedTiProxyNodeCpuNum
		default:
			recommendedCpuNum = recommendedNodeCpuNum
		}

		if n.GetVCpu() == recommendedCpuNum {
			n.Default = true
			hasDefault[key] = append(hasDefault[key], n)
		}
	}

	for key, specs := range group {
		if len(hasDefault[key]) == 0 {
			specs[0].Default = true
		} else if len(hasDefault[key]) > 1 {
			for i, n := range hasDefault[key] {
				if i != 0 {
					n.Default = false
				}
			}
		}
	}
}

func (t serverImpl) sortNodeSpecs(nodeSpecs []*openapidedicatedv1beta1.NodeSpec) {
	slices.SortFunc(nodeSpecs, func(a, b *openapidedicatedv1beta1.NodeSpec) int {
		if a.GetComponentType() != b.GetComponentType() {
			return cmp.Compare(a.GetComponentType(), b.GetComponentType())
		}
		if a.GetDefault() != b.GetDefault() {
			if a.GetDefault() {
				return -1
			}
			return 1
		}
		if a.GetVCpu() != b.GetVCpu() {
			return cmp.Compare(a.GetVCpu(), b.GetVCpu())
		}
		if a.GetMemorySizeGi() != b.GetMemorySizeGi() {
			return cmp.Compare(a.GetMemorySizeGi(), b.GetMemorySizeGi())
		}
		return cmp.Compare(a.GetNodeSpecKey(), b.GetNodeSpecKey())
	})
}
