package cluster

import (
	"context"
	"strconv"
	"strings"

	"github.com/samber/lo"
	regionaldedicatedv1 "github.com/tidbcloud/infra-provider/api/tidbcloud/regional-server/v1"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"

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

func (s *serviceImpl) GetComponentNodeInfo(
	ctx context.Context,
	req *globaldedicatedv1.GetComponentNodeInfoRequest,
) (*globaldedicatedv1.ComponentNodeInfo, bizerr.BizError) {
	ctx, span := tracer.Start(ctx, "Domain.GetComponentNodeInfo")
	defer span.End()

	if req.ClusterId == 0 {
		return nil, bizerr.New(bizerr.ErrCodeParams, "cluster_id is required")
	}

	cluster, err := s.clusterRepo.GetAndLoadComponents(ctx, req.ClusterId)
	if err != nil {
		return nil, err
	}

	region, err := s.regionRepo.SelectRegionById(ctx, cluster.RegionId, cluster.OrganizationId)
	if err != nil {
		return nil, err
	}

	if region.State != globalcommonv1.Region_ACTIVE {
		return nil, bizerr.New(bizerr.ErrCodePermission, "region is not active")
	}

	getReq := &regionaldedicatedv1.GetComponentNodeInfosRequest{
		ClusterId: strconv.FormatUint(req.ClusterId, 10),
	}

	nodeInfos, err := s.regionServerCli.GetComponentNodeInfo(ctx, region, getReq)
	if err != nil {
		if err.ErrCode() == bizerr.ErrCodeRecordNotFound {
			tilog.Warn(ctx, "cluster node has not been created yet", zap.Uint64("cluster_id", req.ClusterId))
			return &globaldedicatedv1.ComponentNodeInfo{}, nil
		}
		return nil, err
	}

	nodeInfo, err := s.cvtToGlobalNodeInfo(ctx, nodeInfos, cluster)
	if err != nil {
		return nil, err
	}
	var defaultTidbGroup *entity.TidbNodeGroup
	for _, tidbgroup := range nodeInfo.Tidb {
		// FIXME(@yifan): should be optimized to single query.
		tidbGroup, err := s.tidbGroupRepo.Load(ctx, cluster.ClusterId, tidbgroup.TidbGroupId)
		if err != nil {
			tilog.Warn(
				ctx,
				"failed to load work pool",
				zap.Uint64("cluster_id", cluster.ClusterId),
				zap.Uint64("tidb_group_id", tidbgroup.TidbGroupId),
				zap.Error(err),
			)
			continue
		}
		tidbgroup.TidbGroupDisplayName = tidbGroup.DisplayName
		tidbgroup.IsDefaultTidbGroup = tidbGroup.Default
		if tidbGroup.Default {
			defaultTidbGroup = tidbGroup
		}
	}

	for _, tidbgroup := range nodeInfo.Tiproxy {
		// FIXME(@cwen0): We don't get tidb group id from clusterserver now, so we set default tidb group to tiproxy group.
		// We should get tidb group id from clusterserver in the future.
		if tidbgroup.TidbGroupId == 0 && defaultTidbGroup != nil {
			tidbgroup.TidbGroupId = defaultTidbGroup.Id
			tidbgroup.TidbGroupDisplayName = defaultTidbGroup.DisplayName
			tidbgroup.IsDefaultTidbGroup = true
			continue
		}
		tidbGroup, err := s.tidbGroupRepo.Load(ctx, cluster.ClusterId, tidbgroup.TidbGroupId)
		if err != nil {
			tilog.Warn(
				ctx,
				"failed to load work pool",
				zap.Uint64("cluster_id", cluster.ClusterId),
				zap.Uint64("tidb_group_id", tidbgroup.TidbGroupId),
				zap.Error(err),
			)
			continue
		}
		tidbgroup.TidbGroupDisplayName = tidbGroup.DisplayName
		tidbgroup.IsDefaultTidbGroup = tidbGroup.Default
	}

	return nodeInfo, nil
}

func (s *serviceImpl) cvtToGlobalNodeInfo(
	ctx context.Context,
	nodeInfos *regionaldedicatedv1.GetComponentNodeInfosResponse,
	cluster *entity.Cluster,
) (*globaldedicatedv1.ComponentNodeInfo, bizerr.BizError) {
	info := &globaldedicatedv1.ComponentNodeInfo{
		Tidb: make([]*globaldedicatedv1.ComponentNodeInfo_Component, 0),
	}
	for _, infoComp := range nodeInfos.ComponentNodeInfos {
		switch infoComp.ComponentType {
		case regionaldedicatedv1.ComponentType_TIDB:
			repoComp, ok := lo.Find(cluster.ClusterComponents, func(c *entity.ClusterComponent) bool {
				return c.GetComponentType() == globaldedicatedv1.ComponentType_TIDB &&
					strconv.FormatUint(c.GetTidbGroupId(), 10) == infoComp.GetTidbGroupId()
			})
			if !ok {
				return nil, bizerr.New(bizerr.ErrCodeUnknown, "tidb group not found")
			}
			info.Tidb = append(info.Tidb, s.cvtNodeInfoDetails(ctx, infoComp, repoComp, cluster))
		case regionaldedicatedv1.ComponentType_TIPROXY:
			// Skip TiProxy components reported with zero nodes.
			// This is currently allowed, but results in an empty entry if not skipped.
			if len(infoComp.GetNodes()) == 0 {
				tilog.Info(ctx, "skip tiproxy group with no nodes",
					zap.Uint64("cluster_id", cluster.GetClusterId()),
					zap.String("tiproxy_group_id", infoComp.GetTidbGroupId()))
				continue
			}
			repoComp, ok := lo.Find(cluster.ClusterComponents, func(c *entity.ClusterComponent) bool {
				return c.GetComponentType() == globaldedicatedv1.ComponentType_TIPROXY
			})
			if !ok {
				return nil, bizerr.New(bizerr.ErrCodeUnknown, "tiproxy group not found")
			}
			info.Tiproxy = append(info.Tiproxy, s.cvtNodeInfoDetails(ctx, infoComp, repoComp, cluster))
		case regionaldedicatedv1.ComponentType_TIKV:
			repoComp, ok := lo.Find(cluster.ClusterComponents, func(c *entity.ClusterComponent) bool {
				return c.GetComponentType() == globaldedicatedv1.ComponentType_TIKV
			})
			if !ok {
				return nil, bizerr.New(bizerr.ErrCodeUnknown, "tikv group not found")
			}
			info.Tikv = s.cvtNodeInfoDetails(ctx, infoComp, repoComp, cluster)
		case regionaldedicatedv1.ComponentType_TIFLASH:
			// Skip TiFlash components reported with zero nodes.
			// This is currently allowed, but results in an empty entry if not skipped.
			if len(infoComp.GetNodes()) == 0 {
				tilog.Info(ctx, "skip tiflash group with no nodes",
					zap.Uint64("cluster_id", cluster.GetClusterId()))
				continue
			}
			repoComp, ok := lo.Find(cluster.ClusterComponents, func(c *entity.ClusterComponent) bool {
				return c.GetComponentType() == globaldedicatedv1.ComponentType_TIFLASH
			})
			if !ok {
				return nil, bizerr.New(bizerr.ErrCodeUnknown, "tiflash group not found")
			}
			info.Tiflash = s.cvtNodeInfoDetails(ctx, infoComp, repoComp, cluster)
		}
	}
	return info, nil
}

func (s *serviceImpl) cvtNodeInfoDetails(
	ctx context.Context,
	nodeInfoDetails *regionaldedicatedv1.ComponentNodeInfo,
	repoComp *entity.ClusterComponent,
	cluster *entity.Cluster,
) *globaldedicatedv1.ComponentNodeInfo_Component {
	if nodeInfoDetails == nil {
		return nil
	}

	var tidbGroupId uint64
	if nodeInfoDetails.TidbGroupId != "" {
		var err error
		tidbGroupId, err = strconv.ParseUint(nodeInfoDetails.TidbGroupId, 10, 64)
		if err != nil {
			tilog.Warn(
				ctx,
				"failed to parse tidb group id",
				zap.String("tidb_group_id", nodeInfoDetails.TidbGroupId),
				zap.Error(err),
			)
		}
	}

	component := &globaldedicatedv1.ComponentNodeInfo_Component{
		Nodes:       make([]*globaldedicatedv1.ComponentNodeInfo_Component_Node, 0, len(nodeInfoDetails.Nodes)),
		TidbGroupId: tidbGroupId,
	}

	for _, regionNode := range nodeInfoDetails.Nodes {
		volumeType, err := enum.FromString[globaldedicatedv1.StorageMu_VolumeType](regionNode.GetVolumeType())
		if err != nil {
			tilog.Warn(
				ctx,
				"failed to convert volume type",
				zap.String("volume_type", regionNode.GetVolumeType()),
				zap.Error(err),
			)
		}

		globalNode := &globaldedicatedv1.ComponentNodeInfo_Component_Node{
			InstanceName:           strings.TrimPrefix(regionNode.GetInstanceName(), "db-"),
			State:                  cvtComponentNodeState(regionNode.GetState()),
			StorageSizeGi:          regionNode.GetStorageSizeGi(),
			StorageIops:            regionNode.GetStorageIops(),
			StorageThroughputMibps: regionNode.GetStorageThroughput(),
			AvailabilityZone:       regionNode.GetAvailabilityZone(),
			InstanceType:           regionNode.GetInstanceType(),
			VolumeType:             globaldedicatedv1.StorageMu_VolumeType(volumeType),
			MeteringSkuKeys: entity.BuildComputeSkus(
				cluster.GetCloudProviderId().String(),
				cluster.GetRegionId().String(),
				regionNode.GetInstanceType(),
				repoComp.GetComponentType(),
				regionNode.GetState(),
			),
			Storages:            make(map[string]*globaldedicatedv1.ComponentNodeInfo_Component_Storage),
			NodeSpecDisplayName: repoComp.GetSpuDisplayName(),
			NodeSpecName:        repoComp.GetSpuName(),
			NodeSpecVersion:     repoComp.GetSpuVersion(),
		}

		if regionNode.GetInstanceType() == repoComp.GetInstanceType() {
			globalNode.Cpu = repoComp.GetCpuNum()
			globalNode.MemorySizeGi = repoComp.GetMemorySizeGi()
		} else {
			// if instance type not found, try to get it from repo
			// when scale cluster, the old instance type not in the cluster instance type list
			instanceType, err := s.instanceMuRepo.Get(ctx, regionNode.GetInstanceType())
			if err != nil {
				tilog.Warn(
					ctx,
					"failed to get instance type for regional node",
					zap.String("instance_type", regionNode.GetInstanceType()),
					zap.Error(err),
				)
			} else {
				globalNode.Cpu = uint32(instanceType.CpuCore)
				globalNode.MemorySizeGi = uint32(instanceType.MemoryGi)
			}
		}

		if _, ok := entity.ComponentsHaveStorage[repoComp.GetComponentType()]; !ok {
			component.Nodes = append(component.Nodes, globalNode)
			continue
		}

		dataVt := repoComp.GetStorages()[globaldedicatedv1.Sku_DATA.String()].GetVolumeType()
		raftVt := repoComp.GetStorages()[globaldedicatedv1.Sku_RAFT_LOG.String()].GetVolumeType()
		var storageType openapidedicatedv1beta1.Cluster_StorageNodeSetting_StorageType
		switch repoComp.GetComponentType() {
		case globaldedicatedv1.ComponentType_TIKV:
			storageType = storagetype.BuildCurrentTikvStorageType(dataVt, raftVt)
		case globaldedicatedv1.ComponentType_TIFLASH:
			storageType = storagetype.BuildCurrentTiflashStorageType(dataVt, raftVt)
		}

		for diskTypeStr, storage := range repoComp.GetStorages() {
			globalNode.Storages[diskTypeStr] = &globaldedicatedv1.ComponentNodeInfo_Component_Storage{
				StorageSizeGi:          storage.GetSizeGi(),
				StorageIops:            storage.GetIops(),
				StorageThroughputMibps: storage.GetThroughputMibps(),
				VolumeType:             storage.GetVolumeType(),
				AdditionalIops:         storage.GetAdditionalIops(),
				MeteringSkuKeys: entity.BuildStorageSkus(
					cluster.GetCloudProviderId().String(),
					cluster.GetRegionId().String(),
					storageType.String(),
					diskTypeStr,
					storage.GetVolumeType().String(),
					storage.GetSizeGi(),
					storage.GetIops(),
					storage.GetThroughputMibps(),
					storage.GetAdditionalIops(),
				),
			}
		}

		component.Nodes = append(component.Nodes, globalNode)
	}

	return component
}

func cvtComponentNodeState(
	state regionaldedicatedv1.ComponentNodeInfo_Node_State,
) globaldedicatedv1.ComponentNodeInfo_Component_Node_State {
	switch state {
	case regionaldedicatedv1.ComponentNodeInfo_Node_STATE_UNSPECIFIED:
		return globaldedicatedv1.ComponentNodeInfo_Component_Node_UNAVAILABLE
	case regionaldedicatedv1.ComponentNodeInfo_Node_NORMAL:
		return globaldedicatedv1.ComponentNodeInfo_Component_Node_AVAILABLE
	case regionaldedicatedv1.ComponentNodeInfo_Node_UNAVAILABLE:
		return globaldedicatedv1.ComponentNodeInfo_Component_Node_UNAVAILABLE
	case regionaldedicatedv1.ComponentNodeInfo_Node_CREATING:
		return globaldedicatedv1.ComponentNodeInfo_Component_Node_CREATING
	case regionaldedicatedv1.ComponentNodeInfo_Node_TERMINATING:
		return globaldedicatedv1.ComponentNodeInfo_Component_Node_DELETING
	case regionaldedicatedv1.ComponentNodeInfo_Node_LEAVING:
		return globaldedicatedv1.ComponentNodeInfo_Component_Node_DELETING
	}
	return globaldedicatedv1.ComponentNodeInfo_Component_Node_UNAVAILABLE
}
