package cluster

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

	"github.com/samber/lo"
	"gorm.io/gorm"

	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/apimiddleware/bizerror"
	"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/grpc/metadata"
	"github.com/tidbcloud/tidb-management-service/utilities/spu/storagetype"
)

func (c *serverImpl) GetNodeInstance(
	ctx context.Context,
	req *openapidedicatedv1beta1.GetNodeInstanceRequest,
) (*openapidedicatedv1beta1.NodeInstance, error) {
	// Extract request meta.
	_, err := metadata.ExtractRequestMetaWithCheckPermission(ctx)
	if err != nil {
		return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "failed to extract request metadata")
	}

	clusterId, err := strconv.ParseUint(req.GetClusterId(), 10, 64)
	if err != nil {
		return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "invalid cluster id")
	}

	compType, groupName := getComponentType(req.GetInstanceId())
	if compType == openapidedicatedv1beta1.ComponentType_COMPONENT_TYPE_UNSPECIFIED {
		return nil, bizerr.New(bizerr.ErrCodeParams, "invalid instance id")
	}

	// get node info from domain service
	var nodeInfo *globaldedicatedv1.ComponentNodeInfo
	nodeInfo, err = c.clusterSvc.GetComponentNodeInfo(ctx, clusterId)
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, bizerror.NewNotFound(err, "cluster not found")
		}
		return nil, bizerror.NewInternal(err, "failed to get node info")
	}

	var nodeInstance *openapidedicatedv1beta1.NodeInstance

	switch compType {
	case openapidedicatedv1beta1.ComponentType_TIDB, openapidedicatedv1beta1.ComponentType_TIPROXY:
		instances := make([]*globaldedicatedv1.ComponentNodeInfo_Component, 0)
		if nodeInfo.GetTidb() != nil {
			instances = append(instances, nodeInfo.GetTidb()...)
		}
		if nodeInfo.GetTiproxy() != nil {
			instances = append(instances, nodeInfo.GetTiproxy()...)
		}
		for _, group := range instances {
			// default group
			if groupName == "" && group.GetIsDefaultTidbGroup() {
				for _, node := range group.GetNodes() {
					if node.GetInstanceName() == req.GetInstanceId() {
						nodeInstance = cvtTidbNodeInstance(node, group, req.GetClusterId(), compType)
					}
				}
			}
			// tidb group
			if groupName != "" && group.GetTidbGroupDisplayName() == groupName {
				for _, node := range group.GetNodes() {
					if fmt.Sprintf(
						"%s-%s",
						group.GetTidbGroupDisplayName(),
						node.GetInstanceName(),
					) == req.GetInstanceId() {
						nodeInstance = cvtTidbNodeInstance(node, group, req.GetClusterId(), compType)
					}
				}
			}
		}
	case openapidedicatedv1beta1.ComponentType_TIKV:
		for _, node := range nodeInfo.GetTikv().GetNodes() {
			if node.GetInstanceName() == req.GetInstanceId() {
				nodeInstance = cvtStorageNodeInstance(
					node,
					req.GetClusterId(),
					openapidedicatedv1beta1.ComponentType_TIKV,
				)
			}
		}

	case openapidedicatedv1beta1.ComponentType_TIFLASH:
		for _, node := range nodeInfo.GetTiflash().GetNodes() {
			if node.GetInstanceName() == req.GetInstanceId() {
				nodeInstance = cvtStorageNodeInstance(
					node,
					req.GetClusterId(),
					openapidedicatedv1beta1.ComponentType_TIFLASH,
				)
			}
		}
	}

	if nodeInstance == nil {
		err = fmt.Errorf("nodeInstance %s not found", req.GetInstanceId())
		return nil, bizerror.NewNotFound(err, err.Error())
	}

	return nodeInstance, nil
}

func (c *serverImpl) ListNodeInstances(
	ctx context.Context,
	req *openapidedicatedv1beta1.ListNodeInstancesRequest,
) (*openapidedicatedv1beta1.ListNodeInstancesResponse, error) {
	// Extract request meta.
	_, err := metadata.ExtractRequestMetaWithCheckPermission(ctx)
	if err != nil {
		return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "failed to extract request metadata")
	}

	clusterId, err := strconv.ParseUint(req.GetClusterId(), 10, 64)
	if err != nil {
		return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "invalid cluster id")
	}

	// get node info from domain service
	var nodeInfo *globaldedicatedv1.ComponentNodeInfo
	nodeInfo, err = c.clusterSvc.GetComponentNodeInfo(ctx, clusterId)
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, bizerror.NewNotFound(err, "cluster not found")
		}
		return nil, bizerror.NewInternal(err, "failed to get node info")
	}

	resp := &openapidedicatedv1beta1.ListNodeInstancesResponse{
		NodeInstances: make([]*openapidedicatedv1beta1.NodeInstance, 0),
	}

	tidbInstances := make([]*openapidedicatedv1beta1.NodeInstance, 0)
	tiproxyInstances := make([]*openapidedicatedv1beta1.NodeInstance, 0)
	tikvInstances := make([]*openapidedicatedv1beta1.NodeInstance, 0)
	tiflashInstances := make([]*openapidedicatedv1beta1.NodeInstance, 0)

	for _, group := range nodeInfo.GetTidb() {
		for _, node := range group.GetNodes() {
			tidbInstances = append(
				tidbInstances,
				cvtTidbNodeInstance(node, group, req.GetClusterId(), openapidedicatedv1beta1.ComponentType_TIDB),
			)
		}
	}

	for _, tikvNode := range nodeInfo.GetTikv().GetNodes() {
		tikvInstances = append(
			tikvInstances,
			cvtStorageNodeInstance(tikvNode, req.GetClusterId(), openapidedicatedv1beta1.ComponentType_TIKV),
		)
	}

	for _, tiflashNode := range nodeInfo.GetTiflash().GetNodes() {
		tiflashInstances = append(
			tiflashInstances,
			cvtStorageNodeInstance(tiflashNode, req.GetClusterId(), openapidedicatedv1beta1.ComponentType_TIFLASH),
		)
	}

	for _, group := range nodeInfo.GetTiproxy() {
		for _, node := range group.GetNodes() {
			tiproxyInstances = append(
				tiproxyInstances,
				cvtTidbNodeInstance(node, group, req.GetClusterId(), openapidedicatedv1beta1.ComponentType_TIPROXY),
			)
		}
	}

	// sort by instance id
	sortByInstanceId := func(a, b *openapidedicatedv1beta1.NodeInstance) int {
		return cmp.Compare(getInstanceIdIndex(a.InstanceId), getInstanceIdIndex(b.InstanceId))
	}
	slices.SortFunc(tidbInstances, sortByInstanceId)
	slices.SortFunc(tikvInstances, sortByInstanceId)
	slices.SortFunc(tiflashInstances, sortByInstanceId)
	slices.SortFunc(tiproxyInstances, sortByInstanceId)

	switch req.GetComponentType() {
	case openapidedicatedv1beta1.ComponentType_TIDB:
		resp.NodeInstances = tidbInstances
	case openapidedicatedv1beta1.ComponentType_TIKV:
		resp.NodeInstances = tikvInstances
	case openapidedicatedv1beta1.ComponentType_TIFLASH:
		resp.NodeInstances = tiflashInstances
	case openapidedicatedv1beta1.ComponentType_TIPROXY:
		resp.NodeInstances = tiproxyInstances
	default:
		resp.NodeInstances = append(resp.NodeInstances, tidbInstances...)
		resp.NodeInstances = append(resp.NodeInstances, tiproxyInstances...)
		resp.NodeInstances = append(resp.NodeInstances, tikvInstances...)
		resp.NodeInstances = append(resp.NodeInstances, tiflashInstances...)
	}

	resp.TotalSize = int32(len(resp.NodeInstances))

	pagedRes, nextPageToken, err := common.LocalPagination(
		resp.NodeInstances,
		req,
		common.ListDefaultPageSize,
		common.ListMaxPageSize,
	)
	if err != nil {
		return nil, bizerror.NewInternal(err, "failed to paginate")
	}

	resp.NodeInstances = pagedRes
	resp.NextPageToken = nextPageToken

	return resp, nil
}

func cvtInstanceState(
	state globaldedicatedv1.ComponentNodeInfo_Component_Node_State,
) openapidedicatedv1beta1.NodeInstance_State {
	switch state {
	case globaldedicatedv1.ComponentNodeInfo_Component_Node_AVAILABLE:
		return openapidedicatedv1beta1.NodeInstance_AVAILABLE
	case globaldedicatedv1.ComponentNodeInfo_Component_Node_UNAVAILABLE:
		return openapidedicatedv1beta1.NodeInstance_UNAVAILABLE
	case globaldedicatedv1.ComponentNodeInfo_Component_Node_CREATING:
		return openapidedicatedv1beta1.NodeInstance_CREATING
	case globaldedicatedv1.ComponentNodeInfo_Component_Node_DELETING:
		return openapidedicatedv1beta1.NodeInstance_DELETING
	default:
		return openapidedicatedv1beta1.NodeInstance_STATE_UNSPECIFIED
	}
}

func cvtTidbNodeInstance(
	node *globaldedicatedv1.ComponentNodeInfo_Component_Node,
	group *globaldedicatedv1.ComponentNodeInfo_Component,
	clusterId string,
	compType openapidedicatedv1beta1.ComponentType,
) *openapidedicatedv1beta1.NodeInstance {
	var instanceId string
	if group.GetIsDefaultTidbGroup() {
		instanceId = node.GetInstanceName()
	} else {
		instanceId = fmt.Sprintf("%s-%s", node.GetInstanceName(), group.GetTidbGroupDisplayName())
	}
	return &openapidedicatedv1beta1.NodeInstance{
		Name:                     fmt.Sprintf("clusters/%s/nodeInstances/%s", clusterId, instanceId),
		ClusterId:                clusterId,
		InstanceId:               instanceId,
		ComponentType:            compType,
		State:                    cvtInstanceState(node.GetState()),
		VCpu:                     int32(node.GetCpu()),
		MemorySizeGi:             int32(node.GetMemorySizeGi()),
		AvailabilityZone:         node.GetAvailabilityZone(),
		TidbNodeGroupId:          lo.ToPtr(strconv.FormatUint(group.GetTidbGroupId(), 10)),
		TidbNodeGroupDisplayName: lo.ToPtr(group.GetTidbGroupDisplayName()),
		IsDefaultTidbNodeGroup:   lo.ToPtr(group.GetIsDefaultTidbGroup()),
		NodeSpecDisplayName:      node.GetNodeSpecDisplayName(),
		NodeSpecKey:              node.GetNodeSpecName(),
		NodeSpecVersion:          node.GetNodeSpecVersion(),
	}
}

func cvtStorageNodeInstance(
	node *globaldedicatedv1.ComponentNodeInfo_Component_Node,
	clusterId string,
	compType openapidedicatedv1beta1.ComponentType,
) *openapidedicatedv1beta1.NodeInstance {
	dataVt := node.GetStorages()[globaldedicatedv1.Sku_DATA.String()].GetVolumeType()
	raftVt := node.GetStorages()[globaldedicatedv1.Sku_RAFT_LOG.String()].GetVolumeType()
	storageType := storagetype.BuildCurrentTikvStorageType(dataVt, raftVt)
	var raftIops *int32
	// User can only get raft iops when storage type is performance.
	if storageType == openapidedicatedv1beta1.Cluster_StorageNodeSetting_Performance {
		raftIops = lo.EmptyableToPtr(
			int32(node.GetStorages()[globaldedicatedv1.Sku_RAFT_LOG.String()].GetStorageIops()),
		)
	}

	var dataStoreIops *int32
	// User can only get data store iops of TiKV for AWS clusters with Plus storage type.
	if storageType == openapidedicatedv1beta1.Cluster_StorageNodeSetting_Plus &&
		dataVt == globaldedicatedv1.StorageMu_IO2 &&
		compType == openapidedicatedv1beta1.ComponentType_TIKV {
		dataStoreIops = lo.EmptyableToPtr(
			int32(node.GetStorages()[globaldedicatedv1.Sku_DATA.String()].GetStorageIops()),
		)
	}

	return &openapidedicatedv1beta1.NodeInstance{
		Name:                fmt.Sprintf("clusters/%s/nodeInstances/%s", clusterId, node.GetInstanceName()),
		ClusterId:           clusterId,
		InstanceId:          node.GetInstanceName(),
		ComponentType:       compType,
		State:               cvtInstanceState(node.GetState()),
		VCpu:                int32(node.GetCpu()),
		MemorySizeGi:        int32(node.GetMemorySizeGi()),
		AvailabilityZone:    node.GetAvailabilityZone(),
		StorageSizeGi:       int32(node.GetStorageSizeGi()),
		StorageType:         lo.EmptyableToPtr(storageType),
		RaftStoreIops:       raftIops,
		DataStoreIops:       dataStoreIops,
		NodeSpecDisplayName: node.GetNodeSpecDisplayName(),
		NodeSpecKey:         node.GetNodeSpecName(),
		NodeSpecVersion:     node.GetNodeSpecVersion(),
	}
}

func getInstanceIdIndex(instanceId string) int {
	m := strings.Split(instanceId, "-")
	indexStr := m[len(m)-1]
	index, _ := strconv.Atoi(indexStr)
	return index
}

func getComponentType(instanceId string) (openapidedicatedv1beta1.ComponentType, string) {
	m := strings.Split(instanceId, "-")

	var (
		compType  string
		groupName string
	)
	switch {
	case len(m) == 2:
		// e.g.
		// tidb-0
		// tikv-1
		// tiflash-2
		compType = m[0]

	case len(m) > 2:
		// e.g.
		// groupName-tidb-0
		compType = m[len(m)-2]
		groupName = strings.Join(m[:len(m)-2], "-")
	}

	switch strings.ToUpper(compType) {
	case openapidedicatedv1beta1.ComponentType_TIDB.String():
		return openapidedicatedv1beta1.ComponentType_TIDB, groupName
	case openapidedicatedv1beta1.ComponentType_TIKV.String():
		return openapidedicatedv1beta1.ComponentType_TIKV, groupName
	case openapidedicatedv1beta1.ComponentType_TIPROXY.String():
		return openapidedicatedv1beta1.ComponentType_TIPROXY, groupName
	case openapidedicatedv1beta1.ComponentType_TIFLASH.String():
		return openapidedicatedv1beta1.ComponentType_TIFLASH, groupName
	default:
		return openapidedicatedv1beta1.ComponentType_COMPONENT_TYPE_UNSPECIFIED, groupName
	}
}
