package cvt

import (
	"context"
	"fmt"
	"strings"

	"go.uber.org/zap"
	"google.golang.org/protobuf/types/known/timestamppb"
	"k8s.io/apimachinery/pkg/api/resource"

	"github.com/tidbcloud/tiinf/monitor/tilog"

	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	apiv1 "github.com/tidbcloud/infra-provider/api/tidbcloud/cluster-server/v1"
	apicommon "github.com/tidbcloud/infra-provider/api/tidbcloud/common"
	"github.com/tidbcloud/infra-provider/pkg/consts"
	pkgutil "github.com/tidbcloud/infra-provider/pkg/util"
)

var componentsManagedByCluster = map[cloudv1.ComponentType]bool{
	cloudv1.ComponentTiKV:           true,
	cloudv1.ComponentTiDB:           true,
	cloudv1.ComponentTiFlash:        true,
	cloudv1.ComponentTiFlashLearner: true,
	cloudv1.ComponentPD:             true,
	cloudv1.ComponentTiProxy:        true,
}

func Cluster2PB(cluster *cloudv1.Cluster) *apiv1.Cluster {
	if cluster == nil {
		return nil
	}

	proxyUrl := ""

	if cluster.Status.Proxy != nil {
		proxyUrl = cluster.Status.Proxy.Endpoint
	}

	gatewayUrl := ""

	if cluster.Status.Gateway != nil {
		gatewayUrl = cluster.Status.Gateway.Endpoint
	}
	out := &apiv1.Cluster{
		Id:                 cluster.Name,
		TenantId:           cluster.GetTenantID(),
		ProjectId:          cluster.GetProjectID(),
		ClusterPhase:       CalculateClusterPhase(cluster),
		ProxyBaseUrl:       proxyUrl,
		GatewayBaseUrl:     gatewayUrl,
		DefaultTidbGroupId: cluster.Annotations[consts.DefaultLoadBalancerIDAnnotationKey],
		Kubernetes: &apiv1.Kubernetes{
			Name: cluster.Spec.Kubernetes.Name,
		},
		Features: convertFeatures(cluster.Annotations),
	}
	if dbStatus := cluster.Status.DB; dbStatus != nil {
		if dbStatus.TiDB != nil {
			out.TokenBasedAuthEnabled = dbStatus.TiDB.TokenBasedAuthEnabled && !cluster.HasBug(consts.AnnoValueBugNameTokenBasedAuthNotEnabled)
		}
	}
	if cluster.Spec.SharedBucket != nil {
		out.SharedBucket = &apiv1.Cluster_SharedBucket{
			BucketUrl: cluster.Spec.SharedBucket.BucketURL,
		}
	}
	if cluster.Status.Conditions != nil {
		out.Conditions = convertConditions(cluster.Status.Conditions)
	}
	return out
}

func convertFeatures(anno map[string]string) []string {
	var fs []string
	for k, v := range anno {
		feat, found := strings.CutPrefix(k, cloudv1.AnnoKeyFeatureNamePrefix)
		if found && v == consts.AnnoValTrue {
			fs = append(fs, feat)
		}
	}

	return fs
}

// convertConditions converts a slice of cloudv1.ClusterCondition to a slice of apiv1.ClusterCondition
func convertConditions(conditions []cloudv1.ClusterCondition) []*apiv1.ClusterCondition {
	pbConditions := make([]*apiv1.ClusterCondition, 0, len(conditions))
	for _, condition := range conditions {
		// Safely get the enum value, defaulting to 0 if not found to prevent panic
		pbClusterConditionType := apiv1.ClusterCondition_ClusterConditionType_value[string(condition.Type)]
		pbConditionStatus := apiv1.ClusterCondition_ConditionStatus_value[string(condition.Status)]

		pbConditions = append(pbConditions, &apiv1.ClusterCondition{
			ClusterConditionType: apiv1.ClusterCondition_ClusterConditionType(pbClusterConditionType),
			ConditionStatus:      apiv1.ClusterCondition_ConditionStatus(pbConditionStatus),
			LastTransitionTime:   timestamppb.New(condition.LastTransitionTime.Time),
			Reason:               condition.Reason,
			Message:              condition.Message,
		})
	}
	return pbConditions
}

func isPaused(cluster *cloudv1.Cluster) bool {
	return cluster.Status.Phase == cloudv1.ClusterPhaseSuspend && cluster.Suspended() && cluster.Spec.Cluster.SuspendAction != nil && cluster.Spec.Cluster.SuspendAction.SuspendCompute
}

func CalculateClusterPhase(cluster *cloudv1.Cluster) apiv1.Cluster_ClusterPhase {
	// todo(yiwen): push phase==cloudv1.ClusterPhaseSuspend down to `cluster.Synced()`?
	if isPaused(cluster) {
		return apiv1.Cluster_SYNCED
	}

	// Mark cluster as Syncing if TiProxy toggle workflow is running.
	if _, ok := cluster.Annotations[consts.AnnoKeyTiProxyToggleWorkflow]; ok {
		return apiv1.Cluster_SYNCING
	}
	// Mark cluster as Syncing if scale-cluster workflow is running.
	if _, ok := cluster.Annotations[consts.AnnoKeyScaleClusterWorkflow]; ok {
		return apiv1.Cluster_SYNCING
	}

	if err := cluster.FullSyncedWithDetail(); err != nil {
		tilog.Info(context.TODO(), "cluster is syncing", zap.String("cluster", cluster.Name), zap.Error(err))
		// other cases
		return apiv1.Cluster_SYNCING
	}
	//  cluster is resumed/running
	return apiv1.Cluster_SYNCED
}

func ComponentType2PB(componentType cloudv1.ComponentType) apicommon.ComponentType {
	switch componentType {
	case cloudv1.ComponentTiKV:
		return apicommon.ComponentType_TIKV
	case cloudv1.ComponentTiDB:
		return apicommon.ComponentType_TIDB
	case cloudv1.ComponentTiFlash:
		return apicommon.ComponentType_TIFLASH
	case cloudv1.ComponentTiFlashLearner:
		return apicommon.ComponentType_TIFLASH_LEARNER
	case cloudv1.ComponentPD:
		return apicommon.ComponentType_PD
	case cloudv1.ComponentTiProxy:
		return apicommon.ComponentType_TIPROXY

	default:
		panic(fmt.Sprintf("unknown component type: %s", componentType))
	}
}

func ComponentType2CR(componentType apicommon.ComponentType) cloudv1.ComponentType {
	switch componentType {
	case apicommon.ComponentType_TIKV:
		return cloudv1.ComponentTiKV
	case apicommon.ComponentType_TIDB:
		return cloudv1.ComponentTiDB
	case apicommon.ComponentType_TIFLASH:
		return cloudv1.ComponentTiFlash
	case apicommon.ComponentType_TIFLASH_LEARNER:
		return cloudv1.ComponentTiFlashLearner
	case apicommon.ComponentType_PD:
		return cloudv1.ComponentPD
	case apicommon.ComponentType_TIPROXY:
		return cloudv1.ComponentTiProxy

	default:
		panic(fmt.Sprintf("unknown component type: %s", componentType))
	}
}

func Quantity2Gi(quantity *resource.Quantity) uint32 {
	return uint32(quantity.Value() / consts.Gi)
}

func Cluster2ComponentNodeInfosPB(cluster *cloudv1.Cluster) *apiv1.GetComponentNodeInfosResponse {
	if cluster == nil || cluster.Status.DB == nil {
		return &apiv1.GetComponentNodeInfosResponse{}
	}

	var result apiv1.GetComponentNodeInfosResponse
	if cluster.Status.DB.TiProxy != nil {
		compInfo := &apiv1.ComponentNodeInfo{
			ComponentType: apicommon.ComponentType_TIPROXY,
			Nodes:         make([]*apiv1.ComponentNodeInfo_Node, 0),
			TidbGroupId:   cluster.Annotations[consts.DefaultLoadBalancerIDAnnotationKey],
		}

		for _, member := range cluster.Status.DB.TiProxy.Members {
			nodeInfo := &apiv1.ComponentNodeInfo_Node{
				InstanceName:     member.Name,
				State:            MemberStatus2PB(member.Status),
				AvailabilityZone: member.Labels[cloudv1.MemberLabelAvailableZone],
				InstanceType:     member.InstanceType,
			}
			// no storage for TiProxy
			compInfo.Nodes = append(compInfo.Nodes, nodeInfo)
		}
		result.ComponentNodeInfos = append(result.ComponentNodeInfos, compInfo)
	}

	if cluster.Status.DB.PD != nil {
		compInfo := &apiv1.ComponentNodeInfo{
			ComponentType: apicommon.ComponentType_PD,
			Nodes:         make([]*apiv1.ComponentNodeInfo_Node, 0),
		}

		// FIXME: we don't have volumeType/IOPS/Throughput for each node in the cluster status
		var iops, throughput uint32
		var volumeType string
		if cluster.Status.DB.PD.Volume != nil {
			volumeType, iops, throughput = pkgutil.GetTypeIOPSThroughputFromClassName(cluster.Status.DB.PD.Volume.CurrentStorageClass)
		}
		for _, member := range cluster.Status.DB.PD.Members {
			nodeInfo := &apiv1.ComponentNodeInfo_Node{
				InstanceName:     member.Name,
				State:            MemberStatus2PB(member.Status),
				AvailabilityZone: member.Labels[cloudv1.MemberLabelAvailableZone],
				InstanceType:     member.InstanceType,
			}
			if member.Resources.Storage != nil {
				nodeInfo.StorageSizeGi = uint32(member.Resources.Storage.Value() / consts.Gi)
				nodeInfo.StorageIops = iops
				nodeInfo.StorageThroughput = throughput
				nodeInfo.VolumeType = volumeType
			}
			compInfo.Nodes = append(compInfo.Nodes, nodeInfo)
		}
		result.ComponentNodeInfos = append(result.ComponentNodeInfos, compInfo)
	}

	if cluster.Status.DB.TiKV != nil {
		compInfo := &apiv1.ComponentNodeInfo{
			ComponentType: apicommon.ComponentType_TIKV,
			Nodes:         make([]*apiv1.ComponentNodeInfo_Node, 0),
		}
		var iops, throughput uint32
		var volumeType string
		if cluster.Status.DB.TiKV.Volume != nil {
			volumeType, iops, throughput = pkgutil.GetTypeIOPSThroughputFromClassName(cluster.Status.DB.TiKV.Volume.CurrentStorageClass)
		}
		for _, member := range cluster.Status.DB.TiKV.Members {
			nodeInfo := &apiv1.ComponentNodeInfo_Node{
				InstanceName:     member.Name,
				State:            MemberStatus2PB(member.Status),
				AvailabilityZone: member.Labels[cloudv1.MemberLabelAvailableZone],
				InstanceType:     member.InstanceType,
			}
			if member.Resources.Storage != nil {
				nodeInfo.StorageSizeGi = uint32(member.Resources.Storage.Value() / consts.Gi)
				nodeInfo.StorageIops = iops
				nodeInfo.StorageThroughput = throughput
				nodeInfo.VolumeType = volumeType
			}
			compInfo.Nodes = append(compInfo.Nodes, nodeInfo)
		}
		result.ComponentNodeInfos = append(result.ComponentNodeInfos, compInfo)
	}

	if cluster.Status.DB.TiFlash != nil {
		compInfo := &apiv1.ComponentNodeInfo{
			ComponentType: apicommon.ComponentType_TIFLASH,
			Nodes:         make([]*apiv1.ComponentNodeInfo_Node, 0),
		}
		var iops, throughput uint32
		var volumeType string
		if cluster.Status.DB.TiFlash.Volume != nil {
			volumeType, iops, throughput = pkgutil.GetTypeIOPSThroughputFromClassName(cluster.Status.DB.TiFlash.Volume.CurrentStorageClass)
		}
		for _, member := range cluster.Status.DB.TiFlash.Members {
			nodeInfo := &apiv1.ComponentNodeInfo_Node{
				InstanceName:     member.Name,
				State:            MemberStatus2PB(member.Status),
				AvailabilityZone: member.Labels[cloudv1.MemberLabelAvailableZone],
				InstanceType:     member.InstanceType,
			}
			if member.Resources.Storage != nil {
				nodeInfo.StorageSizeGi = uint32(member.Resources.Storage.Value() / consts.Gi)
				nodeInfo.StorageIops = iops
				nodeInfo.StorageThroughput = throughput
				nodeInfo.VolumeType = volumeType
			}
			compInfo.Nodes = append(compInfo.Nodes, nodeInfo)
		}
		result.ComponentNodeInfos = append(result.ComponentNodeInfos, compInfo)
	}

	if cluster.Status.DB.TiDB != nil {
		compInfo := &apiv1.ComponentNodeInfo{
			ComponentType: apicommon.ComponentType_TIDB,
			Nodes:         make([]*apiv1.ComponentNodeInfo_Node, 0),
			TidbGroupId:   cluster.Annotations[consts.DefaultLoadBalancerIDAnnotationKey],
		}
		for _, member := range cluster.Status.DB.TiDB.Members {
			nodeInfo := &apiv1.ComponentNodeInfo_Node{
				InstanceName:     member.Name,
				State:            MemberStatus2PB(member.Status),
				AvailabilityZone: member.Labels[cloudv1.MemberLabelAvailableZone],
				InstanceType:     member.InstanceType,
			}
			// no storage for TiDB
			compInfo.Nodes = append(compInfo.Nodes, nodeInfo)
		}
		result.ComponentNodeInfos = append(result.ComponentNodeInfos, compInfo)
	}

	// also return extra TiDB Groups
	for _, extraGroup := range cluster.Status.ExtraEndpoints {
		if extraGroup.TiDB == nil {
			continue
		}

		compInfo := &apiv1.ComponentNodeInfo{
			ComponentType: apicommon.ComponentType_TIDB,
			Nodes:         make([]*apiv1.ComponentNodeInfo_Node, 0),
			TidbGroupId:   extraGroup.Name,
		}
		for _, member := range extraGroup.TiDB.Members {
			// extract name, e.g `db-1772906909536358400-862b80f8-tidb-0` to `tidb-0`
			name := member.Name
			strs := strings.Split(member.Name, "-")
			if len(strs) > 1 {
				name = fmt.Sprintf("%s-%s", strs[len(strs)-2], strs[len(strs)-1])
			}
			nodeInfo := &apiv1.ComponentNodeInfo_Node{
				InstanceName:     name,
				State:            MemberStatus2PB(member.Status),
				AvailabilityZone: member.Labels[cloudv1.MemberLabelAvailableZone],
				InstanceType:     member.InstanceType,
			}
			// no storage for TiDB
			compInfo.Nodes = append(compInfo.Nodes, nodeInfo)
		}
		result.ComponentNodeInfos = append(result.ComponentNodeInfos, compInfo)

		// append tiproxy
		if extraGroup.TiProxy != nil {
			tiproxy := &apiv1.ComponentNodeInfo{
				ComponentType: apicommon.ComponentType_TIPROXY,
				Nodes:         make([]*apiv1.ComponentNodeInfo_Node, 0),
				TidbGroupId:   extraGroup.Name,
			}
			for _, member := range extraGroup.TiProxy.Members {
				// extract name, e.g `db-1772906909536358400-862b80f8-tiproxy-0` to `tiproxy-0`
				name := member.Name
				strs := strings.Split(member.Name, "-")
				if len(strs) > 1 {
					name = fmt.Sprintf("%s-%s", strs[len(strs)-2], strs[len(strs)-1])
				}
				nodeInfo := &apiv1.ComponentNodeInfo_Node{
					InstanceName:     name,
					State:            MemberStatus2PB(member.Status),
					AvailabilityZone: member.Labels[cloudv1.MemberLabelAvailableZone],
					InstanceType:     member.InstanceType,
				}
				// no storage for TiProxy
				tiproxy.Nodes = append(tiproxy.Nodes, nodeInfo)
			}
			result.ComponentNodeInfos = append(result.ComponentNodeInfos, tiproxy)
		}
	}

	return &result
}

func MemberStatus2PB(ms cloudv1.MemberStatus) apiv1.ComponentNodeInfo_Node_State {
	switch ms {
	case cloudv1.MemberStatusNormal:
		return apiv1.ComponentNodeInfo_Node_NORMAL
	case cloudv1.MemberStatusUnavailable:
		return apiv1.ComponentNodeInfo_Node_UNAVAILABLE
	case cloudv1.MemberStatusCreating:
		return apiv1.ComponentNodeInfo_Node_CREATING
	case cloudv1.MemberStatusTerminating:
		return apiv1.ComponentNodeInfo_Node_TERMINATING
	case cloudv1.MemberStatusLeaving:
		return apiv1.ComponentNodeInfo_Node_LEAVING
	default:
		return apiv1.ComponentNodeInfo_Node_STATE_UNSPECIFIED
	}
}

func ClusterCRPhase2PBPhase(phase cloudv1.ClusterPhase) apiv1.InfraInformation_ClusterPhase {
	switch phase {
	case cloudv1.ClusterPhasePending, "":
		return apiv1.InfraInformation_PENDING
	case cloudv1.ClusterPhaseCreating:
		return apiv1.InfraInformation_CREATING
	case cloudv1.ClusterPhaseSuspend:
		return apiv1.InfraInformation_PAUSED
	case cloudv1.ClusterPhaseRunning:
		return apiv1.InfraInformation_RUNNING
	case cloudv1.ClusterPhaseScaling, cloudv1.ClusterPhaseUpgrading:
		return apiv1.InfraInformation_MODIFYING
	case cloudv1.ClusterPhaseDeleting:
		return apiv1.InfraInformation_DELETING
	case cloudv1.ClusterPhaseUnavailable:
		return apiv1.InfraInformation_UNAVAILABLE
	default:
		return apiv1.InfraInformation_CLUSTER_PHASE_UNSPECIFIED
	}
}

func InfraStorageClassName(componentType apicommon.ComponentType, diskType string, diskIops, diskThroughput int32) string {
	var componentName string
	switch componentType {
	case apicommon.ComponentType_TIPROXY:
		componentName = "tiproxy"
	case apicommon.ComponentType_TIDB:
		componentName = "tidb"
	case apicommon.ComponentType_TIKV:
		componentName = "tikv"
	case apicommon.ComponentType_PD:
		componentName = "pd"
	case apicommon.ComponentType_TIFLASH:
		componentName = "tiflash"
	}

	switch diskType {
	case "gp3", "io2", "premiumv2", "ultrassd", "hyperdisk-balanced":
		return fmt.Sprintf("%s-%s-%d-%d", componentName, diskType, diskIops, diskThroughput)
	default:
		return fmt.Sprintf("%s-%s", componentName, diskType)
	}
}
