package central

import (
	"cmp"
	"fmt"
	"slices"
	"strconv"
	"strings"

	"github.com/gin-gonic/gin"
	centralapi "github.com/tidbcloud/core-svc/idl/pbgen/proto/central"

	openapidedicatedv1beta1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_cloud_open_api/dedicated/v1beta1"
	"github.com/tidbcloud/tidb-management-service/tempo-wrapper/internal/api/types"
	apiutils "github.com/tidbcloud/tidb-management-service/tempo-wrapper/internal/api/utils"
	"github.com/tidbcloud/tidb-management-service/tempo-wrapper/pkg/grpc"
	coregrpc "github.com/tidbcloud/tidb-management-service/tempo-wrapper/pkg/grpc/core_portal"
	mgmtgrpc "github.com/tidbcloud/tidb-management-service/tempo-wrapper/pkg/grpc/mgmt"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
)

func ListNodeProfiles(ctx *gin.Context) {
	req := &centralapi.CalcNodeProfilesReq{}
	err := types.ShouldBindJSON(ctx, req)
	if err != nil {
		types.BadRequestResp(ctx, fmt.Errorf("bind request body failed: %w", err))
		return
	}

	// check org/project id
	orgId, err := strconv.ParseUint(ctx.Param("org_id"), 10, 64)
	if err != nil {
		types.UnauthorizedResp(ctx, err)
		return
	}
	projectId, err := strconv.ParseUint(ctx.Param("project_id"), 10, 64)
	if err != nil {
		types.UnauthorizedResp(ctx, err)
		return
	}

	// set org and project id
	req.OrgId = orgId
	req.ProjectId = projectId

	// serverless not have node profiles
	if req.IsDevTier {
		types.SuccessResp(ctx, &centralapi.CalcNodeProfilesResp{
			NodeProfiles: []*centralapi.ComponentNodeProfiles{
				{
					ComponentType: centralapi.ComponentType_COMPONENT_TYPE_TIDB,
					Profiles: []*centralapi.ClusterNodeProfile{
						{
							Name: "Shared0",
						},
					},
					DefaultProfileName: "Shared0",
				},
				{
					ComponentType: centralapi.ComponentType_COMPONENT_TYPE_TIKV,
					Profiles: []*centralapi.ClusterNodeProfile{
						{
							Name: "Shared0",
						},
					},
					DefaultProfileName: "Shared0",
				},
				{
					ComponentType: centralapi.ComponentType_COMPONENT_TYPE_TIFLASH,
					Profiles: []*centralapi.ClusterNodeProfile{
						{
							Name: "Shared0",
						},
					},
					DefaultProfileName: "Shared0",
				},
			},
			BaseResp: &centralapi.BaseResp{
				ErrCode: int64(bizerr.ErrCodeOK),
				ErrMsg:  "",
			}})
		return
	}

	// dedicated
	if ok, err := apiutils.IsDedicatedAllow(ctx, orgId, projectId); err != nil {
		types.InternalErrorResp(ctx, fmt.Errorf("check cluster failed: %w", err))
		return
	} else if ok {
		var regionId string
		switch {
		case req.Region != "" && req.Provider != "":
			regionId = strings.ToLower(fmt.Sprintf("%s-%s", req.Provider, req.Region))
		default:
			types.BadRequestResp(ctx, fmt.Errorf("region or cloud provider is required"))
			return
		}

		tiers, err := mgmtgrpc.ListNodeSpecs(grpc.NewContext(ctx), &openapidedicatedv1beta1.ListNodeSpecsRequest{
			RegionId:  regionId,
			ProjectId: strconv.FormatUint(req.ProjectId, 10),
		})
		if err != nil {
			types.GrpcErrorResp(ctx, err)
			return
		}

		resp := convertCalcNodeProfilesResp(tiers)
		types.SuccessResp(ctx, resp)
		return
	}

	resp, err := coregrpc.CalcNodeProfiles(grpc.NewContext(ctx), req)
	if err != nil {
		types.GrpcErrorResp(ctx, err)
		return
	}
	types.SuccessResp(ctx, resp)
}

func convertCalcNodeProfilesResp(r *openapidedicatedv1beta1.ListNodeSpecsResponse) *centralapi.CalcNodeProfilesResp {
	resp := &centralapi.CalcNodeProfilesResp{
		BaseResp: &centralapi.BaseResp{
			ErrCode: 0,
			ErrMsg:  "",
		},
	}

	tidbNodeProfiles := &centralapi.ComponentNodeProfiles{
		ComponentType: centralapi.ComponentType_COMPONENT_TYPE_TIDB,
		Profiles:      make([]*centralapi.ClusterNodeProfile, 0),
	}
	tikvNodeProfiles := &centralapi.ComponentNodeProfiles{
		ComponentType: centralapi.ComponentType_COMPONENT_TYPE_TIKV,
		Profiles:      make([]*centralapi.ClusterNodeProfile, 0),
	}
	tiflashNodeProfiles := &centralapi.ComponentNodeProfiles{
		ComponentType: centralapi.ComponentType_COMPONENT_TYPE_TIFLASH,
		Profiles:      make([]*centralapi.ClusterNodeProfile, 0),
	}

	for _, nodeSpec := range r.NodeSpecs {
		switch nodeSpec.GetComponentType() {
		case openapidedicatedv1beta1.ComponentType_TIDB:
			tidbNodeProfiles.Profiles = append(tidbNodeProfiles.Profiles, cvtClusterNodeProfile(nodeSpec))
		case openapidedicatedv1beta1.ComponentType_TIKV:
			tikvNodeProfiles.Profiles = append(tikvNodeProfiles.Profiles, cvtClusterNodeProfile(nodeSpec))
		case openapidedicatedv1beta1.ComponentType_TIFLASH:
			tiflashNodeProfiles.Profiles = append(tiflashNodeProfiles.Profiles, cvtClusterNodeProfile(nodeSpec))
		}
	}

	// sort
	tidbNodeProfiles.Profiles = sortProfils(tidbNodeProfiles.Profiles)
	tikvNodeProfiles.Profiles = sortProfils(tikvNodeProfiles.Profiles)
	tiflashNodeProfiles.Profiles = sortProfils(tiflashNodeProfiles.Profiles)

	if defaultProfile := GetDefaultProfile(centralapi.ComponentType_COMPONENT_TYPE_TIDB, tidbNodeProfiles.Profiles); defaultProfile != nil {
		tidbNodeProfiles.DefaultProfileName = defaultProfile.Name
	}
	if defaultProfile := GetDefaultProfile(centralapi.ComponentType_COMPONENT_TYPE_TIKV, tikvNodeProfiles.Profiles); defaultProfile != nil {
		tikvNodeProfiles.DefaultProfileName = defaultProfile.Name
	}
	if defaultProfile := GetDefaultProfile(centralapi.ComponentType_COMPONENT_TYPE_TIFLASH, tiflashNodeProfiles.Profiles); defaultProfile != nil {
		tiflashNodeProfiles.DefaultProfileName = defaultProfile.Name
	}

	resp.NodeProfiles = []*centralapi.ComponentNodeProfiles{tidbNodeProfiles, tikvNodeProfiles, tiflashNodeProfiles}
	return resp
}

func sortProfils(profiles []*centralapi.ClusterNodeProfile) []*centralapi.ClusterNodeProfile {
	slices.SortFunc(profiles, func(a, b *centralapi.ClusterNodeProfile) int {
		if c := cmp.Compare(a.CpuNum, b.CpuNum); c != 0 {
			return c
		}
		if c := cmp.Compare(a.MemoryGi, b.MemoryGi); c != 0 {
			return c
		}
		return cmp.Compare(len(a.Name), len(b.Name))
	})

	return profiles
}

func GetDefaultProfile(
	ct centralapi.ComponentType,
	profiles []*centralapi.ClusterNodeProfile,
) *centralapi.ClusterNodeProfile {
	var defaultProfile *centralapi.ClusterNodeProfile
	if len(profiles) >= 1 {
		// default policy is the first one
		defaultProfile = profiles[0]

		recommendedNodeCpuNum := int32(8)
		if ct == centralapi.ComponentType_COMPONENT_TYPE_TIFLASH {
			recommendedNodeCpuNum = int32(16)
		}

		for _, profile := range profiles {
			if profile.CpuNum == recommendedNodeCpuNum {
				defaultProfile = profile
				break
			}
		}
	}

	return defaultProfile
}

func cvtClusterNodeProfile(nodeSpec *openapidedicatedv1beta1.NodeSpec) *centralapi.ClusterNodeProfile {
	return &centralapi.ClusterNodeProfile{
		Name:                 nodeSpec.GetNodeSpecKey(),
		DisplayName:          nodeSpec.GetDisplayName(),
		DefaultNodeNum:       nodeSpec.GetDefaultNodeCount(),
		MinStorageSizeGi:     nodeSpec.GetMinStorageSizeGi(),
		MaxStorageSizeGi:     nodeSpec.GetMaxStorageSizeGi(),
		DefaultStorageSizeGi: nodeSpec.GetDefaultStorageSizeGi(),
		CpuNum:               nodeSpec.GetVCpu(),
		MemoryGi:             nodeSpec.GetMemorySizeGi(),
	}
}
