package server

import (
	"context"
	"fmt"
	"sort"
	"strings"

	"github.com/crossplane-contrib/provider-aws/apis/autoscaling/v1beta1"
	"github.com/crossplane-contrib/provider-aws/apis/eks/manualv1alpha1"
	"github.com/samber/lo"
	"k8s.io/apimachinery/pkg/labels"
	"sigs.k8s.io/controller-runtime/pkg/client"

	"github.com/tidbcloud/cluster-controller/server/server/cvt"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	apiv1 "github.com/tidbcloud/infra-provider/api/tidbcloud/cluster-server/v1"
	"github.com/tidbcloud/infra-provider/pkg/nodegroup/selfmanaged"
)

const maxSkewVersion = 2

func (s *ClusterServer) GetInfraInformation(ctx context.Context, req *apiv1.GetInfraInformationRequest) (*apiv1.InfraInformation, error) {
	cluster, err := s.getClusterCR(ctx, req.ClusterId)
	if err != nil {
		return nil, fmt.Errorf("failed to get the cluster CR: %w", err)
	}

	info := &apiv1.InfraInformation{
		ClusterId: req.ClusterId,
		Phase:     cvt.ClusterCRPhase2PBPhase(cluster.Status.Phase),
		Topology:  make(map[string]uint32, 5),
	}
	if cluster.Status.DB != nil {
		info.Version = cluster.Status.DB.Version
	}
	for _, comp := range cluster.GetComponents() {
		info.Topology[string(comp.GetComponentType())] = uint32(comp.GetReplicas())
	}

	switch cluster.GetCloudProvider() {
	case cloudv1.ProviderAWS:
		info.ComponentNodes, info.Notes, err = s.GetNodeInfoAndNotes(ctx, cluster)
		if err != nil {
			return nil, fmt.Errorf("failed to get cluster's node info: %w", err)
		}
	case cloudv1.ProviderGCP:
		// Get this information from the region server, because it can get gke CR,
		// which contains the node pool information
		break
	}

	return info, nil
}

func (s *ClusterServer) GetNodeInfoAndNotes(ctx context.Context, cluster *cloudv1.Cluster) (ngs []*apiv1.NodeGroup, notes []string, err error) {
	versionSet := map[string]struct{}{}

	// Do not care about the count of tidb nodes.
	tidbNodes := &apiv1.NodeGroup{
		Kind:           apiv1.NodeGroup_TIDB_NODES,
		MaxSkewVersion: maxSkewVersion,
	}

	// Currently, only tidb clusters use self-managed nodegroups.
	var asgList v1beta1.AutoScalingGroupList
	if err = s.KubeClient.List(ctx, &asgList, client.MatchingLabelsSelector{
		Selector: labels.SelectorFromSet(map[string]string{
			"cluster": cluster.Name,
		}),
	}); err != nil {
		return nil, nil, fmt.Errorf("failed to list AutoScalingGroups: %w", err)
	}

	var version string
	for _, asg := range asgList.Items {
		if ver := asg.Annotations[selfmanaged.K8sVersionAnnotationKey]; ver != "" {
			versionSet[ver] = struct{}{}
			version = ver
		}
	}
	tidbNodes.Version = version
	ngs = append(ngs, tidbNodes)

	// DM and TiCDC are using managed nodegroups.
	var ngList manualv1alpha1.NodeGroupList
	if err = s.KubeClient.List(ctx, &ngList, client.MatchingLabelsSelector{
		Selector: labels.SelectorFromSet(map[string]string{
			"cluster": cluster.Name,
		}),
	}); err != nil {
		return nil, nil, fmt.Errorf("failed to list NodeGroups: %w", err)
	}

	dmNodes := &apiv1.NodeGroup{
		Kind:           apiv1.NodeGroup_DM_NODES,
		Count:          0,
		MaxSkewVersion: maxSkewVersion,
	}
	cdcNodes := &apiv1.NodeGroup{
		Kind:           apiv1.NodeGroup_CDC_NODES,
		Count:          0,
		MaxSkewVersion: maxSkewVersion,
	}
	otherNodes := &apiv1.NodeGroup{
		Kind:           apiv1.NodeGroup_NODE_GROUP_KIND_UNSPECIFIED,
		Count:          0,
		MaxSkewVersion: maxSkewVersion,
	}
	for _, ng := range ngList.Items {
		switch ng.Labels["component"] {
		case "dm":
			if ng.Status.AtProvider.ScalingConfig.DesiredSize != nil {
				dmNodes.Count += uint32(*ng.Status.AtProvider.ScalingConfig.DesiredSize)
			}
			dmNodes.Version = ng.Status.AtProvider.Version
		case "ticdc":
			if ng.Status.AtProvider.ScalingConfig.DesiredSize != nil {
				cdcNodes.Count += uint32(*ng.Status.AtProvider.ScalingConfig.DesiredSize)
			}
			cdcNodes.Version = ng.Status.AtProvider.Version
		default:
			desiredSize := uint32(0)
			if ng.Status.AtProvider.ScalingConfig.DesiredSize != nil {
				desiredSize = uint32(*ng.Status.AtProvider.ScalingConfig.DesiredSize)
				otherNodes.Count += desiredSize
			}
			otherNodes.Version = ng.Status.AtProvider.Version
			component := ng.Labels["component"]
			if component == "" {
				component = ng.Labels["tidb-component"]
			}
			if component == "" {
				component = "unknown"
			}
			notes = append(notes, fmt.Sprintf("%s node group found: %s(version: %s, desired size: %d)", component, ng.Name, ng.Status.AtProvider.Version, desiredSize))
		}
	}

	if len(versionSet) > 1 {
		versions := lo.Keys(versionSet)
		sort.Strings(versions)
		notes = append(notes, fmt.Sprintf("multiple k8s versions detected in tidb nodes: %s", strings.Join(versions, ",")))
	}

	if dmNodes.Count > 0 {
		ngs = append(ngs, dmNodes)
	}
	if cdcNodes.Count > 0 {
		ngs = append(ngs, cdcNodes)
	}
	if otherNodes.Count > 0 {
		ngs = append(ngs, otherNodes)
		notes = append(notes, fmt.Sprintf("other node group count: %d", otherNodes.Count))
	}

	sort.Slice(ngs, func(i, j int) bool {
		return int(ngs[i].Kind) < int(ngs[j].Kind)
	})
	sort.Strings(notes)
	return ngs, notes, nil
}
