package utils

import (
	"fmt"
	"strconv"
	"strings"
	"time"

	"github.com/pingcap/log"
	"github.com/tidbcloud/account/idl/pbgen/proto/account"
	"github.com/tidbcloud/core-svc/idl/pbgen/proto/core"
	"github.com/tidbcloud/serverless-service/idl/pbgen/proto/rpc"
	commonv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/common/v1"
	dedicatedv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/dedicated/v1"
	nextgenv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/nextgen/v1"
	serverlessv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/serverless/v1"
	openapiv1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_cloud_open_api/common/v1beta1"
	mgmtv1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_mgmt_service/v1"
	"go.uber.org/zap"
	"gorm.io/gorm"
	"k8s.io/utils/ptr"

	"github.com/pingcap-inc/clinic/pkg/datasource/view"
	"github.com/pingcap-inc/clinic/pkg/db"
	"github.com/pingcap-inc/clinic/pkg/db/model"
	"github.com/pingcap-inc/clinic/pkg/nextgen_client"
	"github.com/pingcap-inc/clinic/pkg/o11y"
)

const (
	clusterStatusPrefix = "CLUSTER_STATUS_"
	userStatusPrefix    = "USER_CLUSTER_STATUS_"
	GardenerArch        = "gardener"
	CloudNativeArch     = "cloudnative"
	ThreeLayerArch      = "3layer"
)

// simple diff for cluster sync, may not precise, return true when diff, return false when may be the same
func ClusterDiff(a, b db.Cluster) bool {
	if a.UpdatedAt != b.UpdatedAt ||
		a.DeletedAt != b.DeletedAt ||
		a.Status != b.Status {
		return true
	}

	return false
}

// ClusterSyncDiff reports whether a normal cluster synchronization has a
// persisted field that is safe to refresh. A nil desired topology means the
// source did not provide topology and must not clear the cached value.
func ClusterSyncDiff(existing, desired db.Cluster) bool {
	if ClusterDiff(existing, desired) {
		return true
	}

	if desired.Topology == nil {
		return false
	}

	return existing.Topology == nil || *existing.Topology != *desired.Topology
}

func ClusterToModel(orgID uint, c *core.Cluster, deployTypeV2 string) db.Cluster {
	var (
		TiDBReplicas       int
		TiKVReplicas       int
		PDReplicas         int
		TiFlashReplicas    int
		CreationFinishedAt *time.Time
		controlPlaneArch   string
	)

	// ComponentType_COMPONENT_TYPE_UNSPECIFIED ComponentType = 0
	// ComponentType_COMPONENT_TYPE_TIDB        ComponentType = 1
	// ComponentType_COMPONENT_TYPE_TIKV        ComponentType = 2
	// ComponentType_COMPONENT_TYPE_TIFLASH     ComponentType = 3
	// ComponentType_COMPONENT_TYPE_PD          ComponentType = 4
	// ComponentType_COMPONENT_TYPE_TICDC       ComponentType = 5

	for _, component := range c.Components {
		switch component.ComponentType {
		case core.ComponentType_COMPONENT_TYPE_TIDB:
			TiDBReplicas = int(component.Replicas)
		case core.ComponentType_COMPONENT_TYPE_TIKV:
			TiKVReplicas = int(component.Replicas)
		case core.ComponentType_COMPONENT_TYPE_PD:
			PDReplicas = int(component.Replicas)
		case core.ComponentType_COMPONENT_TYPE_TIFLASH:
			TiFlashReplicas = int(component.Replicas)
		}
	}

	endTime := int64(0)
	if c.DeletedAt == nil {
		var deletedAt int64 = 0
		c.DeletedAt = &deletedAt
	} else {
		endTime = *c.DeletedAt
	}
	dbComps := make(map[string]db.ClusterComponent, 0)
	for _, c := range c.Components {
		dbComps[c.ComponentType.String()] = db.ClusterComponent{
			Replicas:            c.Replicas,
			CpuNum:              c.CpuNum,
			MemoryGi:            c.MemoryGi,
			StorageSizeGi:       c.StorageSizeGi,
			StorageInstanceType: c.StorageInstanceType,
		}
	}

	if c.CreationFinishedAt != nil {
		t := time.Unix(*c.CreationFinishedAt, 0)
		CreationFinishedAt = &t
	}

	if c.GetAttributes().GetUseNewDedicatedArch() {
		controlPlaneArch = ThreeLayerArch
	} else if c.GetDataPlaneInfo().ShootName == "" {
		controlPlaneArch = CloudNativeArch
	} else {
		controlPlaneArch = GardenerArch
	}

	return db.Cluster{
		Model: gorm.Model{
			CreatedAt: time.Unix(c.CreatedAt, 0),
			UpdatedAt: time.Unix(endTime, 0),
			DeletedAt: gorm.DeletedAt{
				Time:  time.Unix(*c.DeletedAt, 0),
				Valid: *c.DeletedAt > 0,
			},
		},
		Topology: &db.Topology{
			TiDB:    TiDBReplicas,
			TiKV:    TiKVReplicas,
			PD:      PDReplicas,
			TiFlash: TiFlashReplicas,
		},
		CreationFinishedAt: CreationFinishedAt,
		StartTime:          uint64(c.CreatedAt),
		EndTime:            uint64(endTime),
		Version:            c.Version,
		ClusterID:          fmt.Sprintf("%d", c.Id),
		ClusterType:        "cloud",
		Name:               c.Name,
		OrgID:              orgID,
		TenantID:           c.TenantId,
		Status:             ConvertClusterStatus(c.UserStatus.String(), c.Status.String()),
		Region:             c.GetRegionName(),
		Provider:           c.GetCloudProviderName(),
		ShootName:          c.DataPlaneInfo.ShootName,
		K8sNamespace:       c.DataPlaneInfo.K8SNamespace,
		NetworkName:        c.DataPlaneInfo.NetworkName,
		DataPlaneName:      c.DataPlaneInfo.DataplaneName,
		DeployType:         strings.ToLower(strings.TrimPrefix(c.DeployType.String(), "DEPLOY_TYPE_")),
		DeployTypeV2:       deployTypeV2,
		ProjectID:          fmt.Sprintf("%d", c.GetProjectId()),
		ClusterComponent:   dbComps,
		ControlPlaneArch:   controlPlaneArch,
	}
}

func GetMgmtDeployTypeV2(c *mgmtv1.Cluster) string {
	switch c.DeployType {
	case mgmtv1.DeployType_DEPLOY_TYPE_DEDICATED:
		return o11y.DedicatedType
	case mgmtv1.DeployType_DEPLOY_TYPE_SERVERLESS:
		return model.ServicePlanV1BetaToDeployType(c.GetServicePlan())
	case mgmtv1.DeployType_DEPLOY_TYPE_NEXTGEN:
		ngCluster := c.GetNextGenCluster()
		return model.ServicePlanToDeployType(ngCluster.GetServicePlan())
	default:
		return ""
	}
}

// MgmtClusterToModel converts a management cluster to a db.Cluster model. only basic fields are filled.
func MgmtClusterToModel(orgID uint, c *mgmtv1.Cluster) *db.Cluster {
	clinicCluster := &db.Cluster{
		Model: gorm.Model{
			CreatedAt: c.CreateTime.AsTime(),
			// UpdatedAt: time.Unix(endTime, 0),
			DeletedAt: gorm.DeletedAt{
				Time:  c.GetDeleteTime().AsTime(),
				Valid: c.GetDeleteTime() != nil,
			},
		},
		StartTime:    uint64(c.GetCreateTime().GetSeconds()),
		EndTime:      uint64(c.GetDeleteTime().GetSeconds()),
		ClusterID:    strconv.FormatUint(c.ClusterId, 10),
		ClusterType:  "cloud",
		Name:         c.ClusterName,
		OrgID:        orgID,
		TenantID:     c.OrganizationId,
		Region:       c.GetRegion(),
		Provider:     c.GetCloudProvider(),
		DeployType:   strings.ToLower(strings.TrimPrefix(c.DeployType.String(), "DEPLOY_TYPE_")),
		DeployTypeV2: GetMgmtDeployTypeV2(c),
		ProjectID:    fmt.Sprintf("%d", c.GetProjectId()),
		Status:       State2String(GetClusterStatus(c)),
		// Topology:           &db.Topology{},
		// CreationFinishedAt: "filled by ClusterInfo",
		// Version:            "filled by ClusterInfo",
		// Status:             "filled by ClusterInfo",
		ShootName: c.GetNextGenCluster().GetRegionLogicalName(),
		// K8sNamespace:       c.DataPlaneInfo.K8SNamespace,
		// NetworkName:        c.DataPlaneInfo.NetworkName,
		// DataPlaneName:      c.DataPlaneInfo.DataplaneName,
		// ClusterComponent: dbComps,
		// ControlPlaneArch: controlPlaneArch,
		RuVersion: c.GetRuVersion(),
	}
	if c.ProjectId == 0 {
		// for nextgen: set project id to empty string
		clinicCluster.ProjectID = ""
	}

	switch c.DeployType {
	case mgmtv1.DeployType_DEPLOY_TYPE_DEDICATED:
		dedicatedCluster := c.GetDedicatedCluster()

		if dedicatedCluster.GetCreateFinishedTime() != nil {
			clinicCluster.CreationFinishedAt = ptr.To(dedicatedCluster.GetCreateFinishedTime().AsTime())
		}
		clinicCluster.Version = dedicatedCluster.Version
		clinicCluster.ControlPlaneArch = ThreeLayerArch

		// set cluster components
		tikvStorageType := c.TikvStorageType.String()
		tiflashStorageType := ""
		if c.TiflashStorageType != nil {
			tiflashStorageType = c.TiflashStorageType.String()
		}
		components := dedicatedCluster.GetClusterComponents()
		*clinicCluster = MgmtClusterComponentToModel(*clinicCluster, components, tikvStorageType, tiflashStorageType)
		clinicCluster.Topology = topologyFromDedicatedComponents(components)
	case mgmtv1.DeployType_DEPLOY_TYPE_SERVERLESS:
		serverlessCluster := c.GetServerlessCluster()

		if serverlessCluster.CreationFinishedAt != nil {
			clinicCluster.CreationFinishedAt = ptr.To(time.Unix(serverlessCluster.GetCreationFinishedAt(), 0))
		}
		clinicCluster.DeployType = o11y.SharedType
		clinicCluster.ControlPlaneArch = CloudNativeArch
		clinicCluster.Version = serverlessCluster.GetCluster().GetVersion()
		if info := serverlessCluster.GetCluster(); info != nil {
			clinicCluster.KeyspaceID = info.GetKeyspaceId()
			clinicCluster.KeyspaceName = info.GetUserPrefix()
			if as := info.GetAutoScaling(); as != nil {
				clinicCluster.MinRCU = int64(as.GetMinRcu())
				clinicCluster.MaxRCU = int64(as.GetMaxRcu())
			}
		}
	case mgmtv1.DeployType_DEPLOY_TYPE_NEXTGEN:
		ngCluster := c.GetNextGenCluster()

		if ngCluster.GetCreateFinishedTime() != nil {
			clinicCluster.CreationFinishedAt = ptr.To(ngCluster.GetCreateFinishedTime().AsTime())
		}

		// use "premium" as deploy type in clinic instead of "nextgen"; premium-class plans share Premium behavior
		clinicCluster.DeployType = model.NextGenClusterDeployType(ngCluster.GetServicePlan())
		clinicCluster.Version = ngCluster.Version
		clinicCluster.MinRCU = ngCluster.GetMinRcu()
		clinicCluster.MaxRCU = ngCluster.GetMaxRcu()
		clinicCluster.ParentID = ngCluster.GetSharedPoolId()
		clinicCluster.DataPlaneName = ngCluster.GetDataplaneId()
	}

	return clinicCluster
}

// devSharedClusterAWSRegion is the AWS region used by dev shared-tier observability data;
// mgmt/serverless may return us-east-2 but Clinic expects us-east-1 for this environment.
const devSharedClusterAWSRegion = "us-east-1"

// ApplyDevSharedClusterRegion overrides Region for shared-tier (serverless) clusters in dev
// before persisting to the database, so sync from mgmt/serverless matches the actual o11y bucket region.
func ApplyDevSharedClusterRegion(env string, c *db.Cluster) {
	if c == nil || env != "dev" {
		return
	}
	if c.DeployType != o11y.SharedType || c.Region != "us-east-2" {
		return
	}
	c.Region = devSharedClusterAWSRegion
}

func GetResourePoolDeployType(resourcePool *nextgenv1.ResourcePool) string {
	switch resourcePool.ServicePlan {
	case commonv1.ServicePlan_BYOC:
		return o11y.ByocHostType
	case commonv1.ServicePlan_Premium, commonv1.ServicePlan_Premium_Reserved, commonv1.ServicePlan_Essential_V2:
		return o11y.NgHostType
	default:
		return ""
	}
}

// ServicePlanToResourcePoolDeployTypeV2 maps resource pool ServicePlan to cluster.deploy_type_v2 for pool rows.
func ServicePlanToResourcePoolDeployTypeV2(sp commonv1.ServicePlan) string {
	switch sp {
	case commonv1.ServicePlan_BYOC:
		return o11y.DeployTypeV2ByocResourcePool
	case commonv1.ServicePlan_Premium:
		return o11y.DeployTypeV2PremiumResourcePool
	case commonv1.ServicePlan_Premium_Reserved:
		return o11y.DeployTypeV2PremiumReservedResourcePool
	case commonv1.ServicePlan_Essential_V2:
		return o11y.DeployTypeV2EssentialV2ResourcePool
	default:
		return ""
	}
}

func GetResourePoolDeployTypeV2(resourcePool *nextgenv1.ResourcePool) string {
	return ServicePlanToResourcePoolDeployTypeV2(resourcePool.ServicePlan)
}

func ResourcePoolToModel(orgID uint, tenantID uint64, profile *nextgenv1.ResourcePoolProfile, sharedPool *nextgen_client.SharedPool) *db.Cluster {
	resourcePool := profile.ResourcePool

	shootName := ""
	if profile.Region != nil {
		shootName = profile.Region.LogicalName
	}
	return &db.Cluster{
		Model: gorm.Model{
			CreatedAt: resourcePool.CreateTime.AsTime(),
			// UpdatedAt: time.Unix(endTime, 0),
			DeletedAt: gorm.DeletedAt{
				Time:  resourcePool.GetDeleteTime().AsTime(),
				Valid: resourcePool.GetDeleteTime() != nil,
			},
		},
		StartTime:     uint64(resourcePool.GetCreateTime().GetSeconds()),
		EndTime:       uint64(resourcePool.GetDeleteTime().GetSeconds()),
		ClusterID:     strconv.FormatUint(resourcePool.Id, 10),
		ClusterType:   "cloud",
		Name:          resourcePool.Name,
		OrgID:         orgID,
		TenantID:      tenantID,
		Region:        resourcePool.RegionId.String(),
		Provider:      resourcePool.ProviderId.String(),
		DeployType:    GetResourePoolDeployType(resourcePool),
		DeployTypeV2:  GetResourePoolDeployTypeV2(resourcePool),
		ProjectID:     "",
		Version:       profile.Version,
		Status:        State2String(GetResourcePoolStatus(resourcePool, sharedPool)),
		ShootName:     shootName,
		DataPlaneName: resourcePool.DataplaneId,
	}
}

func MgmtClusterComponentToModel(
	cluster db.Cluster,
	comps []*dedicatedv1.Cluster_ClusterComponent,
	tikvStorageType string,
	tiflashStorageType string,
) db.Cluster {
	if len(comps) == 0 {
		return cluster
	}
	// convert mgmt component array to map, using core.ComponentType as the key
	cluster.ClusterComponent = make(db.ClusterComponents, len(comps))
	dedicatedComps := buidlDedicatedComponents(comps)
	for t, compoents := range dedicatedComps {
		if len(compoents) == 0 {
			continue
		}

		dbComponent := cluster.ClusterComponent[t]
		dbComponent.Replicas = 0 // reset replicas to 0
		for _, c := range compoents {
			if t == core.ComponentType_COMPONENT_TYPE_TIDB.String() {
				if cluster.DeletedAt.Valid || c.DeleteTime == nil {
					dbComponent.Replicas += int32(c.GetCount())
				}
				if c.IsDefaultTidbGroup || dbComponent.TidbGroupId == 0 {
					dbComponent.TidbGroupId = c.TidbGroupId
					dbComponent.TidbGroupDisplayName = c.TidbGroupDisplayName
					dbComponent.CpuNum = int32(c.GetCpuNum())
					dbComponent.MemoryGi = int64(c.GetMemorySizeGi())
					dbComponent.TierName = c.SpuName
				}
				var deleteAt *time.Time
				if c.DeleteTime != nil {
					deleteAt = ptr.To(c.DeleteTime.AsTime())
				}
				dbComponent.TidbGroups = append(dbComponent.TidbGroups, db.TidbGroup{
					Id:          c.TidbGroupId,
					DisplayName: c.TidbGroupDisplayName,
					IsDefault:   c.IsDefaultTidbGroup,
					TierName:    c.SpuName,
					CreatedAt:   ptr.To(c.CreateTime.AsTime()),
					DeletedAt:   deleteAt,
					Replicas:    int32(c.GetCount()),
					CpuNum:      int32(c.GetCpuNum()),
					MemoryGi:    int64(c.GetMemorySizeGi()),
				})
			} else {
				// build ComponentStorage, key is `data` or `raftlog`
				storage := make(map[string]*db.ComponentStorage, 0)
				for k, v := range c.Storages {
					if v == nil {
						continue
					}
					st := &db.ComponentStorage{}
					st.DiskType = v.DiskType.String()
					st.SizeGi = v.SizeGi
					st.Iops = v.Iops
					st.ThroughputMibps = v.ThroughputMibps
					st.VolumeType = v.VolumeType.String()
					st.AdditionalIops = v.AdditionalIops
					storage[k] = st
				}

				// only one component is allowed except from tidb，so override all the fields
				dbComponent.Replicas = int32(c.GetCount())
				dbComponent.CpuNum = int32(c.GetCpuNum())
				dbComponent.MemoryGi = int64(c.GetMemorySizeGi())
				dbComponent.Storages = storage
				dbComponent.TierName = c.SpuName
				if t == core.ComponentType_COMPONENT_TYPE_TIKV.String() {
					dbComponent.StorageType = tikvStorageType
				}
				if t == core.ComponentType_COMPONENT_TYPE_TIFLASH.String() {
					dbComponent.StorageType = tiflashStorageType
				}
			}
		}
		cluster.ClusterComponent[t] = dbComponent
	}

	return cluster
}

// topologyFromDedicatedComponents builds the legacy list-view summary from the
// same active dedicated components persisted in ClusterComponent.
func topologyFromDedicatedComponents(comps []*dedicatedv1.Cluster_ClusterComponent) *db.Topology {
	topology := &db.Topology{}
	for _, component := range comps {
		if component == nil || component.GetDeleteTime() != nil {
			continue
		}

		count := int(component.GetCount())
		switch component.GetComponentType() {
		case dedicatedv1.ComponentType_TIDB:
			topology.TiDB += count
		case dedicatedv1.ComponentType_TIKV:
			topology.TiKV += count
		case dedicatedv1.ComponentType_PD:
			topology.PD += count
		case dedicatedv1.ComponentType_TIFLASH:
			topology.TiFlash += count
		}
	}
	return topology
}

func BranchToModel(orgID uint, parentCluster *mgmtv1.Cluster, b *rpc.BasicBranch) db.Cluster {
	var cft *time.Time
	if b.GetCreationFinishedTime() != nil {
		cft = ptr.To(b.GetCreationFinishedTime().AsTime())
	}
	return db.Cluster{
		Model: gorm.Model{
			CreatedAt: b.GetCreateTime().AsTime(),
			UpdatedAt: b.GetUpdateTime().AsTime(),
		},
		CreationFinishedAt: cft,
		StartTime:          uint64(b.GetCreateTime().GetSeconds()),
		// EndTime:            uint64(endTime),
		ClusterID:    b.Id,
		Version:      parentCluster.GetServerlessCluster().GetCluster().Version,
		ClusterType:  "cloud",
		Name:         b.DisplayName,
		OrgID:        orgID,
		TenantID:     parentCluster.OrganizationId,
		Status:       State2String(cvtBranchState(b.State)),
		Region:       parentCluster.GetRegion(),
		Provider:     parentCluster.GetCloudProvider(),
		DeployType:   o11y.SharedType,
		DeployTypeV2: GetMgmtDeployTypeV2(parentCluster),
		ProjectID:    strconv.FormatUint(parentCluster.GetProjectId(), 10),
		ParentID:     strconv.FormatUint(parentCluster.ClusterId, 10),
	}
}

func ToBasicBranch(b *rpc.FullBranch) *rpc.BasicBranch {
	return &rpc.BasicBranch{
		Id:                   b.Id,
		Name:                 b.Name,
		DisplayName:          b.DisplayName,
		ClusterId:            b.ClusterId,
		ParentId:             b.ParentId,
		State:                b.State,
		CreateTime:           b.CreateTime,
		UpdateTime:           b.UpdateTime,
		UserPrefix:           b.UserPrefix,
		Annotations:          b.Annotations,
		CreationFinishedTime: b.CreationFinishedTime,
	}
}

func ConvertClusterStatus(userStatus, clusterStatus string) string {
	clusterStatus = strings.ToLower(strings.TrimPrefix(clusterStatus, clusterStatusPrefix))
	if clusterStatus == "deleted" ||
		clusterStatus == "deleting" ||
		clusterStatus == "cleared" {
		return clusterStatus
	}

	if userStatus == "USER_CLUSTER_STATUS_UPGRADING" ||
		userStatus == "USER_CLUSTER_STATUS_IMPORTING" ||
		userStatus == "USER_CLUSTER_STATUS_UNAVAILABLE" {
		return strings.ToLower(strings.TrimPrefix(userStatus, userStatusPrefix))
	}

	return clusterStatus
}

func CloudOrgToModel(org *account.Org) *db.Organization {
	splitedEmail := strings.Split(org.Email, "@")
	return &db.Organization{
		Model: gorm.Model{
			CreatedAt: time.Unix(org.CreatedAt.Seconds, int64(org.CreatedAt.Nanos)),
			UpdatedAt: time.Unix(org.UpdatedAt.Seconds, int64(org.UpdatedAt.Nanos)),
		},
		Name:        org.Name,
		CreatedBy:   1,
		DbaasOrgID:  uint(org.Id),
		Plan:        org.Plan,
		Kind:        org.Kind,
		Country:     org.Country,
		OrgType:     "cloud",
		EmailDomain: splitedEmail[len(splitedEmail)-1],
	}
}

func buidlDedicatedComponents(comps []*dedicatedv1.Cluster_ClusterComponent) map[string][]*dedicatedv1.Cluster_ClusterComponent {
	components := make(map[string][]*dedicatedv1.Cluster_ClusterComponent, 0)
	for _, c := range comps {
		switch c.ComponentType {
		case dedicatedv1.ComponentType_TIDB:
			components[core.ComponentType_COMPONENT_TYPE_TIDB.String()] = append(components[core.ComponentType_COMPONENT_TYPE_TIDB.String()], c)
		case dedicatedv1.ComponentType_TIKV:
			components[core.ComponentType_COMPONENT_TYPE_TIKV.String()] = append(components[core.ComponentType_COMPONENT_TYPE_TIKV.String()], c)
		case dedicatedv1.ComponentType_TIFLASH:
			components[core.ComponentType_COMPONENT_TYPE_TIFLASH.String()] = append(components[core.ComponentType_COMPONENT_TYPE_TIFLASH.String()], c)
		case dedicatedv1.ComponentType_PD:
			components[core.ComponentType_COMPONENT_TYPE_PD.String()] = append(components[core.ComponentType_COMPONENT_TYPE_PD.String()], c)
		case dedicatedv1.ComponentType_TIPROXY:
			components["COMPONENT_TYPE_TIPROXY"] = append(components["COMPONENT_TYPE_TIPROXY"], c)
		default:
			continue
		}
	}
	return components
}

func ResourcePoolComponentToView(compoents []*nextgen_client.Component, filter func(*nextgen_client.Component) bool) []*view.ResourcePoolComponent {
	result := make([]*view.ResourcePoolComponent, 0, len(compoents))
	for _, group := range compoents {
		if filter != nil && !filter(group) {
			continue
		}

		viewGroup := &view.ResourcePoolComponent{
			Id:            group.Id,
			ComponentType: view.ComponentType(group.ComponentType),
			Version:       group.Version,
			Phase:         group.Phase,
		}
		if group.Configuration != nil {
			viewGroup.Image = &view.ResourcePoolImage{
				Repo: group.Configuration.ImageRepo,
				Tag:  group.Configuration.ImageTag,
			}
		}
		if group.NodeQuota != nil {
			viewGroup.NodeCount = group.NodeQuota.NodeCount
			viewGroup.MaxNodeCount = group.NodeQuota.MaxNodeCount
			viewGroup.MinNodeCount = group.NodeQuota.MinNodeCount
			if group.NodeQuota.NodeSpec != nil {
				viewGroup.CpuNum = group.NodeQuota.NodeSpec.Cpu
				viewGroup.MemoryGi = group.NodeQuota.NodeSpec.MemoryGi
			}
		}
		for _, storage := range group.Storages {
			viewGroup.Storages = append(viewGroup.Storages, &view.ResourcePoolStorage{
				Name:          storage.Name,
				Type:          storage.Type,
				SizeGi:        storage.SizeGi,
				Iops:          storage.Iops,
				ThroughputMbs: storage.ThroughputMbs,
			})
		}
		result = append(result, viewGroup)
	}
	return result
}

func DedicatedComponentToView(components []*dedicatedv1.Cluster_ClusterComponent, configuration *dedicatedv1.ClusterConfiguration, effectiveImageTags map[string]string) []*view.ResourcePoolComponent {
	result := make([]*view.ResourcePoolComponent, 0, len(components))
	imageTags := dedicatedComponentImageTags(configuration, effectiveImageTags)
	for _, component := range components {
		if component == nil || component.GetDeleteTime() != nil {
			continue
		}

		nodeCount := component.GetCount()
		viewGroup := &view.ResourcePoolComponent{
			Id:            strconv.FormatUint(component.GetId(), 10),
			ComponentType: view.ComponentType(component.GetComponentType().String()),
			CpuNum:        component.GetCpuNum(),
			MemoryGi:      component.GetMemorySizeGi(),
			NodeCount:     nodeCount,
			MaxNodeCount:  nodeCount,
			MinNodeCount:  nodeCount,
		}
		if imageTag := imageTags[strings.ToLower(component.GetComponentType().String())]; imageTag != "" {
			viewGroup.Image = &view.ResourcePoolImage{Tag: imageTag}
		}
		for name, storage := range component.GetStorages() {
			if storage == nil {
				continue
			}
			viewGroup.Storages = append(viewGroup.Storages, &view.ResourcePoolStorage{
				Name:          name,
				Type:          dedicatedStorageType(storage),
				SizeGi:        storage.GetSizeGi(),
				Iops:          storage.GetIops(),
				ThroughputMbs: storage.GetThroughputMibps(),
			})
		}
		result = append(result, viewGroup)
	}
	return result
}

func dedicatedComponentImageTags(configuration *dedicatedv1.ClusterConfiguration, effectiveImageTags map[string]string) map[string]string {
	imageTags := make(map[string]string)
	for component, imageTag := range effectiveImageTags {
		if imageTag == "" {
			continue
		}
		imageTags[strings.ToLower(component)] = imageTag
	}
	for _, component := range configuration.GetComponentConfigurations() {
		if component.GetOverwriteImageTag() == "" {
			continue
		}
		componentType := strings.ToLower(component.GetComponentType().String())
		if imageTags[componentType] == "" {
			imageTags[componentType] = component.GetOverwriteImageTag()
		}
	}
	return imageTags
}

func dedicatedStorageType(storage *dedicatedv1.Cluster_ClusterComponent_Storage) string {
	if storage == nil {
		return ""
	}
	if volumeType := storage.GetVolumeType(); volumeType != dedicatedv1.StorageMu_VOLUME_TYPE_UNSPECIFIED {
		return volumeType.String()
	}
	if diskType := storage.GetDiskType(); diskType != dedicatedv1.Sku_DISK_TYPE_UNSPECIFIED {
		return diskType.String()
	}
	return ""
}

func GetClusterStatus(c *mgmtv1.Cluster) openapiv1.ClusterState {
	state := getClusterStatus(c)
	overriddenState := overrideStatus(state, c.CustomStatuses)
	log.Info("cluster state", zap.Uint64("cluster", c.ClusterId), zap.Any("state", state), zap.Any("overridden state", overriddenState))
	return overriddenState
}

// GetClusterStatusWithoutOverride returns the base mgmt cluster state without applying custom status overrides.
func GetClusterStatusWithoutOverride(c *mgmtv1.Cluster) openapiv1.ClusterState {
	state := getClusterStatus(c)
	log.Info("cluster state without override", zap.Uint64("cluster", c.ClusterId), zap.Any("state", state))
	return state
}

func getClusterStatus(c *mgmtv1.Cluster) openapiv1.ClusterState {
	state := openapiv1.ClusterState_CLUSTER_STATE_UNSPECIFIED
	switch c.DeployType {
	case mgmtv1.DeployType_DEPLOY_TYPE_DEDICATED:
		dedicatedCluster := c.GetDedicatedCluster()
		log.Info("dedicated cluster state", zap.Uint64("cluster", c.ClusterId), zap.Any("state", dedicatedCluster.State), zap.Any("health state", dedicatedCluster.HealthState))
		state = cvtDedicatedState(dedicatedCluster.State, dedicatedCluster.HealthState)
	case mgmtv1.DeployType_DEPLOY_TYPE_SERVERLESS:
		serverlessCluster := c.GetServerlessCluster()
		log.Info("dedicated cluster state", zap.Uint64("cluster", c.ClusterId), zap.Any("state", serverlessCluster.UserStatus))
		state = cvtServerlessState(serverlessCluster.UserStatus)
	case mgmtv1.DeployType_DEPLOY_TYPE_NEXTGEN:
		nextgenCluster := c.GetNextGenCluster()
		log.Info("nextgen cluster state", zap.Uint64("cluster", c.ClusterId), zap.Any("state", nextgenCluster.State), zap.Any("health state", nextgenCluster.HealthState))
		state = cvtNextGenState(nextgenCluster.State, nextgenCluster.HealthState)
	}
	return state
}

func cvtDedicatedState(
	in dedicatedv1.Cluster_State,
	health dedicatedv1.Cluster_HealthState,
) openapiv1.ClusterState {
	if health == dedicatedv1.Cluster_HEALTH_STATE_UNSPECIFIED &&
		in == dedicatedv1.Cluster_STATE_UNSPECIFIED {
		return openapiv1.ClusterState_INACTIVE
	}

	switch in {
	case dedicatedv1.Cluster_STATE_UNSPECIFIED:
		return openapiv1.ClusterState_CLUSTER_STATE_UNSPECIFIED
	case dedicatedv1.Cluster_AVAILABLE:
		return openapiv1.ClusterState_ACTIVE
	case dedicatedv1.Cluster_CREATING:
		return openapiv1.ClusterState_CREATING
	case dedicatedv1.Cluster_MODIFYING,
		dedicatedv1.Cluster_SCALING:
		return openapiv1.ClusterState_MODIFYING
	case dedicatedv1.Cluster_PAUSED:
		return openapiv1.ClusterState_PAUSED
	case dedicatedv1.Cluster_PAUSING:
		return openapiv1.ClusterState_PAUSING
	case dedicatedv1.Cluster_RESUMING:
		return openapiv1.ClusterState_RESUMING
	case dedicatedv1.Cluster_DELETED:
		return openapiv1.ClusterState_DELETED
	}
	return openapiv1.ClusterState_CLUSTER_STATE_UNSPECIFIED
}

func cvtNextGenState(
	in nextgenv1.Cluster_State,
	health nextgenv1.Cluster_HealthState,
) openapiv1.ClusterState {
	if health == nextgenv1.Cluster_HEALTH_STATE_UNSPECIFIED &&
		in == nextgenv1.Cluster_STATE_UNSPECIFIED {
		return openapiv1.ClusterState_INACTIVE
	}

	switch in {
	case nextgenv1.Cluster_STATE_UNSPECIFIED:
		return openapiv1.ClusterState_CLUSTER_STATE_UNSPECIFIED
	case nextgenv1.Cluster_AVAILABLE:
		return openapiv1.ClusterState_ACTIVE
	case nextgenv1.Cluster_CREATING:
		return openapiv1.ClusterState_CREATING
	case nextgenv1.Cluster_MODIFYING:
		return openapiv1.ClusterState_MODIFYING
	case nextgenv1.Cluster_DELETED:
		return openapiv1.ClusterState_DELETED
	}
	return openapiv1.ClusterState_CLUSTER_STATE_UNSPECIFIED
}

func cvtServerlessState(in serverlessv1.UserClusterStatus) openapiv1.ClusterState {
	switch in {
	case serverlessv1.UserClusterStatus_USER_CLUSTER_STATUS_UNSPECIFIED,
		serverlessv1.UserClusterStatus_USER_CLUSTER_STATUS_UNAVAILABLE:
		return openapiv1.ClusterState_CLUSTER_STATE_UNSPECIFIED
	case serverlessv1.UserClusterStatus_USER_CLUSTER_STATUS_NORMAL,
		serverlessv1.UserClusterStatus_USER_CLUSTER_STATUS_IMPORTING:
		return openapiv1.ClusterState_ACTIVE
	case serverlessv1.UserClusterStatus_USER_CLUSTER_STATUS_CREATING:
		return openapiv1.ClusterState_CREATING
	case serverlessv1.UserClusterStatus_USER_CLUSTER_STATUS_DELETED:
		return openapiv1.ClusterState_DELETED
	case serverlessv1.UserClusterStatus_USER_CLUSTER_STATUS_RECOVERING:
		return openapiv1.ClusterState_RESTORING
	}
	return openapiv1.ClusterState_CLUSTER_STATE_UNSPECIFIED
}

func cvtBranchState(in rpc.BranchStatus) openapiv1.ClusterState {
	switch in {
	case rpc.BranchStatus_BRANCH_STATE_UNSPECIFIED:
		return openapiv1.ClusterState_CLUSTER_STATE_UNSPECIFIED
	case rpc.BranchStatus_ACTIVE:
		return openapiv1.ClusterState_ACTIVE
	case rpc.BranchStatus_CREATING:
		return openapiv1.ClusterState_CREATING
	case rpc.BranchStatus_DELETED:
		return openapiv1.ClusterState_DELETED
	case rpc.BranchStatus_MAINTENANCE:
		return openapiv1.ClusterState_MAINTENANCE
	}
	return openapiv1.ClusterState_CLUSTER_STATE_UNSPECIFIED
}

func State2String(in openapiv1.ClusterState) string {
	if in == openapiv1.ClusterState_CLUSTER_STATE_UNSPECIFIED {
		return ""
	}
	return strings.ToLower(in.String())
}

func overrideStatus(
	state openapiv1.ClusterState,
	customStatuses []*mgmtv1.ClusterCustomStatus,
) openapiv1.ClusterState {
	// if cluster is creating, not override custom status
	if state == openapiv1.ClusterState_CREATING {
		return state
	}

	// use custom status to override state
	// if there are multiple custom status, use the lastest one
	for _, status := range customStatuses {
		if status.IsExpired {
			continue
		}
		if status.OverrideStatus != nil {
			return *status.OverrideStatus
		}
	}
	return state
}

// Convert resource pool status to cluster state
// from https://github.com/tidbcloud/cluster-service-ng/blob/main/api/cluster-service-ng/v1beta/resources.proto#L60-L66
// PHASE_UNSPECIFIED = 0;
// CREATING = 1;
// AVAILABLE = 2;
// MODIFYING = 3;
// UNAVAILABLE = 4;
func GetResourcePoolStatus(resourcePool *nextgenv1.ResourcePool, sharedPool *nextgen_client.SharedPool) openapiv1.ClusterState {
	if resourcePool.GetDeleteTime() != nil {
		return openapiv1.ClusterState_DELETED
	}

	if sharedPool == nil {
		return openapiv1.ClusterState_CLUSTER_STATE_UNSPECIFIED
	}

	switch sharedPool.Phase {
	case "PHASE_UNSPECIFIED":
		return openapiv1.ClusterState_CLUSTER_STATE_UNSPECIFIED
	case "CREATING":
		return openapiv1.ClusterState_CREATING
	case "AVAILABLE":
		return openapiv1.ClusterState_ACTIVE
	case "MODIFYING":
		return openapiv1.ClusterState_MODIFYING
	case "UNAVAILABLE":
		return openapiv1.ClusterState_INACTIVE
	}
	return openapiv1.ClusterState_CLUSTER_STATE_UNSPECIFIED
}
