package service

import (
	"context"
	"fmt"

	"github.com/samber/lo"
	apiv1 "github.com/tidbcloud/cluster-service-ng/api/tidbcloud/cluster-agent/v1"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

func (s *AgentService) ListNodes(ctx context.Context, req *apiv1.ListNodesReq) (*apiv1.ListNodesResp, error) {
	logger := tilog.WithContextL(ctx).With(zap.String("cluster_id", req.ClusterId))
	logger.Info("list nodes")
	nodes, err := s.getNodesByClusterId(ctx, req.ClusterId)
	if err != nil {
		return nil, err
	}

	var out []*apiv1.Node
	for compType, nodeList := range nodes {
		out = append(out, lo.Map(nodeList, func(no *corev1.Node, _ int) *apiv1.Node {
			return convertNode(no, compType)
		})...)
	}

	return &apiv1.ListNodesResp{Nodes: out}, nil
}

func (s *AgentService) GetNode(ctx context.Context, req *apiv1.GetNodeReq) (*apiv1.Node, error) {
	logger := tilog.WithContextL(ctx).With(zap.String("cluster_id", req.ClusterId), zap.String("node_name", req.NodeName))
	logger.Info("get node")
	var node corev1.Node
	if err := s.dataplaneCli.Get(ctx, client.ObjectKey{Name: req.NodeName}, &node); err != nil {
		return nil, fmt.Errorf("failed to get node: %w", err)
	}
	return convertNode(&node, ""), nil
}

// getNodesByClusterId returns all nodes referenced by this cluster
func (s *AgentService) getNodesByClusterId(ctx context.Context, clusterId string) (map[string][]*corev1.Node, error) {
	logger := tilog.WithContextL(ctx)
	// So we should get pods owned by this cluster first, then get nodes by pod.spec.nodeName.
	name2pod, err := s.getPodsByClusterId(ctx, clusterId)
	if err != nil {
		return nil, fmt.Errorf("get pods failed: %w", err)
	}
	logger.Info("pod num", zap.Int("num", len(name2pod)))

	var hostnames []string
	nodeName2ComponentType := make(map[string]string, len(name2pod))
	for _, pod := range name2pod {
		if pod.Spec.NodeName == "" {
			continue
		}

		hostnames = append(hostnames, pod.Spec.NodeName)
		componentTypeInLabel := pod.Labels[K8sComponentLabelKey]
		nodeName2ComponentType[pod.Spec.NodeName] = componentTypeInLabel
	}
	logger.Info("hostname num", zap.Int("num", len(hostnames)))
	logger.Debug("hostnames", zap.Strings("hostnames", hostnames))
	if len(hostnames) == 0 {
		return nil, nil
	}

	labelSelector, err := metav1.LabelSelectorAsSelector(&metav1.LabelSelector{
		MatchExpressions: []metav1.LabelSelectorRequirement{
			{
				Key:      corev1.LabelHostname,
				Operator: metav1.LabelSelectorOpIn,
				Values:   hostnames,
			},
		},
	})
	if err != nil {
		return nil, fmt.Errorf("build label selector: %w", err)
	}
	var nodeList corev1.NodeList
	if err := s.dataplaneCli.List(ctx, &nodeList, client.MatchingLabelsSelector{Selector: labelSelector}); err != nil {
		return nil, fmt.Errorf("failed to list nodes by pod's hostname: %w", err)
	}

	nodes := make(map[string][]*corev1.Node)

	for i := range nodeList.Items {
		componentType := nodeName2ComponentType[nodeList.Items[i].Labels[corev1.LabelHostname]]
		if componentType == "" {
			tilog.Warn(ctx, "node's component type not found", zap.String("node_name", nodeList.Items[i].Name))
		}
		if nodes[componentType] == nil {
			nodes[componentType] = []*corev1.Node{}
		}
		nodes[componentType] = append(nodes[componentType], &nodeList.Items[i])
	}

	return nodes, nil
}

func parseProviderInstanceNameFromNode(no *corev1.Node) string {
	provider, err := getCloudProviderFromNode(no)
	if err != nil {
		return "parse failed"
	}
	switch provider {
	case "aws":
		_, instanceId, err := parseEKSNodeProviderID(no.Spec.ProviderID)
		if err != nil {
			return "parse AWS provider id failed"
		}
		return instanceId
	case "gcp":
		_, _, instanceId, err := parseGKENodeProviderID(no.Spec.ProviderID)
		if err != nil {
			return "parse GCP provider id failed"
		}
		return instanceId
	case "azure":
		// do nothing
		// TODO: add azure control logic
		return ""
	case "alicloud":
		instanceId, err := parseAlicloudInstanceIDFromNode(no)
		if err != nil {
			return "parse Alicloud provider id failed"
		}
		return instanceId
	default:
		return fmt.Sprintf("unknown provider: %s", provider)
	}
}

func convertNode(no *corev1.Node, compType string) *apiv1.Node {
	out := &apiv1.Node{
		K8SMetadata:          buildK8sMetadata(no),
		ComponentType:        compType,
		Region:               no.Labels[corev1.LabelTopologyRegion],
		Zone:                 no.Labels[corev1.LabelTopologyZone],
		Hostname:             no.Labels[corev1.LabelHostname],
		ProviderInstanceName: parseProviderInstanceNameFromNode(no),
		ProviderInstanceType: no.Labels[corev1.LabelInstanceTypeStable],
		CpuArch:              no.Labels[corev1.LabelArchStable],
		Ready:                false, // fill later
		ReadyReason:          "",    // fill later
	}

	for _, condition := range no.Status.Conditions {
		if condition.Type == corev1.NodeReady {
			out.Ready = condition.Status == corev1.ConditionTrue
			out.ReadyReason = condition.Reason
		}
	}
	return out
}
