package metrics

import (
	"context"
	"strconv"
	"strings"
	"time"

	autoscalingv1beta1 "github.com/crossplane-contrib/provider-aws/apis/autoscaling/v1beta1"
	ec2v1alpha1 "github.com/crossplane-contrib/provider-aws/apis/ec2/v1alpha1"
	eksv1alpha1 "github.com/crossplane-contrib/provider-aws/apis/eks/manualv1alpha1"
	gkecontainerv1beta1 "github.com/crossplane-contrib/provider-gcp/apis/container/v1beta1"
	ackv1alpha1 "github.com/crossplane-contrib/provider-upjet-alibabacloud/apis/ack/v1alpha1"
	aliplv1alpha1 "github.com/crossplane-contrib/provider-upjet-alibabacloud/apis/privatelink/v1alpha1"
	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	ec2v1beta1 "github.com/upbound/provider-aws/apis/ec2/v1beta1"
	upboundeksv1beta2 "github.com/upbound/provider-aws/apis/eks/v1beta2"
	azureksv1beta1 "github.com/upbound/provider-azure/apis/containerservice/v1beta1"
	"go.uber.org/multierr"
	corev1 "k8s.io/api/core/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/api/meta"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"

	agentv2 "github.com/tidbcloud/observability/agent-control-server/api/v2"
	"github.com/tidbcloud/observability/agent-control-server/pkg/consts"
)

func ignoreNoMatchError(err error) error {
	if meta.IsNoMatchError(err) || apierrors.IsNotFound(err) {
		return nil
	}
	return err
}

type desiredNodeCountMetric struct {
	provider string
	cluster  string
	name     string
	crdType  string
	desired  float64
}

// WrapReconciledFunc wraps the reconciled function to record the metrics
// of the controller reconciliation.
func WrapReconciledFunc(agent string, ctx context.Context, req ctrl.Request, f func(ctx context.Context, req ctrl.Request) (ctrl.Result, error)) (ctrl.Result, error) {
	lbs := []string{agent, req.Namespace, req.Name}
	now := time.Now()
	res, err := f(ctx, req)
	if err != nil {
		controllerReconciledTotal.WithLabelValues(append(lbs, "success")...).Inc()
	} else {
		controllerReconciledTotal.WithLabelValues(append(lbs, "failed")...).Inc()
	}
	controllerReconciledDuration.WithLabelValues(lbs...).Observe(time.Since(now).Seconds())
	return res, err
}

// RecordAgentStatus records the status of all agents.
func RecordAgentStatus(cli client.Client) error {
	var errs error
	errs = multierr.Append(errs, recordSharedPoolAgentStatus(cli))
	errs = multierr.Append(errs, recordTidbGroupAgentStatus(cli))
	errs = multierr.Append(errs, recordKubernetesAgentStatus(cli))
	errs = multierr.Append(errs, recordO11yK8sAgentStatus(cli))
	errs = multierr.Append(errs, recordO11yTidbAgentStatus(cli))
	return errs
}

func recordSharedPoolAgentStatus(cli client.Client) error {
	var agents agentv2.SharedPoolAgentList
	err := cli.List(context.Background(), &agents)
	if err != nil {
		return ignoreNoMatchError(err)
	}

	ts := time.Now().Unix()
	for _, agent := range agents.Items {
		agentStatus.WithLabelValues(
			consts.AgentSharedPool,
			agent.Spec.KubernetesClusterName,
			agent.Spec.O11y.TenantID,
			agent.Spec.O11y.ApplicationID,
			agent.Status.Phase,
		).Set(float64(ts))

		annotations := agent.GetAnnotations()
		agentAnnotations.WithLabelValues(
			consts.AgentSharedPool,
			agent.Spec.KubernetesClusterName,
			agent.Spec.O11y.TenantID,
			agent.Spec.O11y.ApplicationID,
			emptyToDash(annotations[consts.AnnotationKeyPauseReconcile]),
			emptyToDash(annotations[consts.AnnotationKeyO11yVMAgentConfigTemplate]),
			emptyToDash(annotations[consts.AnnotationKeyO11yVectorConfigTemplate]),
		).Set(float64(ts))

		agentCreated.WithLabelValues(
			consts.AgentSharedPool,
			agent.Spec.KubernetesClusterName,
			agent.Spec.O11y.TenantID,
			agent.Spec.O11y.ApplicationID,
		).Set(float64(agent.CreationTimestamp.Unix()))
	}
	return nil
}

func recordTidbGroupAgentStatus(cli client.Client) error {
	var agents agentv2.TiDBGroupAgentList
	err := cli.List(context.Background(), &agents)
	if err != nil {
		return ignoreNoMatchError(err)
	}

	ts := time.Now().Unix()
	for _, agent := range agents.Items {
		agentStatus.WithLabelValues(
			consts.AgentTidbGroup,
			agent.Spec.KubernetesClusterName,
			agent.Spec.O11y.TenantID,
			agent.Spec.O11y.ApplicationID,
			agent.Status.Phase,
		).Set(float64(ts))

		annotations := agent.GetAnnotations()
		agentAnnotations.WithLabelValues(
			consts.AgentTidbGroup,
			agent.Spec.KubernetesClusterName,
			agent.Spec.O11y.TenantID,
			agent.Spec.O11y.ApplicationID,
			emptyToDash(annotations[consts.AnnotationKeyPauseReconcile]),
			emptyToDash(annotations[consts.AnnotationKeyO11yVMAgentConfigTemplate]),
			emptyToDash(annotations[consts.AnnotationKeyO11yVectorConfigTemplate]),
		).Set(float64(ts))

		agentCreated.WithLabelValues(
			consts.AgentTidbGroup,
			agent.Spec.KubernetesClusterName,
			agent.Spec.O11y.TenantID,
			agent.Spec.O11y.ApplicationID,
		).Set(float64(agent.CreationTimestamp.Unix()))
	}
	return nil
}

func recordKubernetesAgentStatus(cli client.Client) error {
	var agents agentv2.KubernetesAgentList
	err := cli.List(context.Background(), &agents)
	if err != nil {
		return ignoreNoMatchError(err)
	}

	ts := time.Now().Unix()
	for _, agent := range agents.Items {
		agentStatus.WithLabelValues(
			consts.AgentKubernetes,
			agent.Spec.KubernetesClusterName,
			agent.Spec.O11y.TenantID,
			agent.Spec.O11y.ApplicationID,
			agent.Status.Phase,
		).Set(float64(ts))

		annotations := agent.GetAnnotations()
		agentAnnotations.WithLabelValues(
			consts.AgentKubernetes,
			agent.Spec.KubernetesClusterName,
			agent.Spec.O11y.TenantID,
			agent.Spec.O11y.ApplicationID,
			emptyToDash(annotations[consts.AnnotationKeyPauseReconcile]),
			emptyToDash(annotations[consts.AnnotationKeyO11yVMAgentConfigTemplate]),
			emptyToDash(annotations[consts.AnnotationKeyO11yVectorConfigTemplate]),
		).Set(float64(ts))

		agentCreated.WithLabelValues(
			consts.AgentKubernetes,
			agent.Spec.KubernetesClusterName,
			agent.Spec.O11y.TenantID,
			agent.Spec.O11y.ApplicationID,
		).Set(float64(agent.CreationTimestamp.Unix()))
	}
	return nil
}

func recordO11yK8sAgentStatus(cli client.Client) error {
	var agents agentv2.O11yK8sAgentList
	err := cli.List(context.Background(), &agents)
	if err != nil {
		return ignoreNoMatchError(err)
	}

	ts := time.Now().Unix()
	for _, agent := range agents.Items {
		agentStatus.WithLabelValues(
			consts.AgentO11yK8s,
			agent.Spec.KubernetesClusterName,
			agent.Spec.O11y.TenantID,
			agent.Spec.O11y.ApplicationID,
			agent.Status.Phase,
		).Set(float64(ts))

		annotations := agent.GetAnnotations()
		agentAnnotations.WithLabelValues(
			consts.AgentO11yK8s,
			agent.Spec.KubernetesClusterName,
			agent.Spec.O11y.TenantID,
			agent.Spec.O11y.ApplicationID,
			emptyToDash(annotations[consts.AnnotationKeyPauseReconcile]),
			emptyToDash(annotations[consts.AnnotationKeyO11yVMAgentConfigTemplate]),
			emptyToDash(annotations[consts.AnnotationKeyO11yVectorConfigTemplate]),
		).Set(float64(ts))

		agentCreated.WithLabelValues(
			consts.AgentO11yK8s,
			agent.Spec.KubernetesClusterName,
			agent.Spec.O11y.TenantID,
			agent.Spec.O11y.ApplicationID,
		).Set(float64(agent.CreationTimestamp.Unix()))
	}
	return nil
}

func recordO11yTidbAgentStatus(cli client.Client) error {
	var agents agentv2.O11yTiDBAgentList
	err := cli.List(context.Background(), &agents)
	if err != nil {
		return ignoreNoMatchError(err)
	}

	ts := time.Now().Unix()
	for _, agent := range agents.Items {
		agentStatus.WithLabelValues(
			consts.AgentO11yTidb,
			agent.Spec.KubernetesClusterName,
			agent.Spec.O11y.TenantID,
			agent.Spec.O11y.ApplicationID,
			agent.Status.Phase,
		).Set(float64(ts))

		annotations := agent.GetAnnotations()
		agentAnnotations.WithLabelValues(
			consts.AgentO11yTidb,
			agent.Spec.KubernetesClusterName,
			agent.Spec.O11y.TenantID,
			agent.Spec.O11y.ApplicationID,
			emptyToDash(annotations[consts.AnnotationKeyPauseReconcile]),
			emptyToDash(annotations[consts.AnnotationKeyO11yVMAgentConfigTemplate]),
			emptyToDash(annotations[consts.AnnotationKeyO11yVectorConfigTemplate]),
		).Set(float64(ts))

		agentCreated.WithLabelValues(
			consts.AgentO11yTidb,
			agent.Spec.KubernetesClusterName,
			agent.Spec.O11y.TenantID,
			agent.Spec.O11y.ApplicationID,
		).Set(float64(agent.CreationTimestamp.Unix()))
	}
	return nil
}

// emptyToDash returns "-" if s is empty, otherwise returns s.
func emptyToDash(s string) string {
	if s == "" {
		return "-"
	}
	return s
}

func readyStatus(cond corev1.ConditionStatus) string {
	if cond == corev1.ConditionUnknown {
		return "NotReady"
	}
	return "Ready"
}

func recordAliCloudVPCEndpointAlpha1Status(cli client.Client) error {
	var vpcEps aliplv1alpha1.VpcEndpointList
	err := cli.List(context.Background(), &vpcEps)
	if err != nil {
		return ignoreNoMatchError(err)
	}

	ts := time.Now().Unix()
	for _, ep := range vpcEps.Items {
		provider := ep.Spec.ForProvider
		if provider.ServiceName == nil || provider.VPCID == nil || provider.EndpointType == nil {
			continue
		}
		cond := ep.Status.GetCondition(xpv1.TypeReady)
		lbs := []string{
			aliplv1alpha1.CRDGroup + "/" + aliplv1alpha1.CRDVersion,
			ep.Namespace,
			ep.Name,
			*provider.ServiceName,
			*provider.VPCID,
			*provider.EndpointType,
			string(cond.Reason),
			readyStatus(cond.Status),
		}
		vpcEndpointStatus.WithLabelValues(lbs...).Set(float64(ts))
	}
	return nil
}

func recordAwsVPCEndpointAlpha1Status(cli client.Client) error {
	var vpcEps ec2v1alpha1.VPCEndpointList
	err := cli.List(context.Background(), &vpcEps)
	if err != nil {
		return ignoreNoMatchError(err)
	}

	ts := time.Now().Unix()
	for _, ep := range vpcEps.Items {
		provider := ep.Spec.ForProvider
		if provider.ServiceName == nil || provider.VPCID == nil || provider.VPCEndpointType == nil {
			continue
		}
		cond := ep.Status.GetCondition(xpv1.TypeReady)
		lbs := []string{
			ec2v1alpha1.CRDGroup + "/" + ec2v1alpha1.CRDVersion,
			ep.Namespace,
			ep.Name,
			*provider.ServiceName,
			*provider.VPCID,
			*provider.VPCEndpointType,
			string(cond.Reason),
			readyStatus(cond.Status),
		}
		vpcEndpointStatus.WithLabelValues(lbs...).Set(float64(ts))
	}
	return nil
}

func recordAwsVPCEndpointBeta1Status(cli client.Client) error {
	var vpcEps ec2v1beta1.VPCEndpointList
	err := cli.List(context.Background(), &vpcEps)
	if err != nil {
		return ignoreNoMatchError(err)
	}

	ts := time.Now().Unix()
	for _, ep := range vpcEps.Items {
		provider := ep.Spec.ForProvider
		if provider.ServiceName == nil || provider.VPCID == nil || provider.VPCEndpointType == nil {
			continue
		}
		cond := ep.Status.GetCondition(xpv1.TypeReady)
		lbs := []string{
			ec2v1beta1.CRDGroup + "/" + ec2v1beta1.CRDVersion,
			ep.Namespace,
			ep.Name,
			*provider.ServiceName,
			*provider.VPCID,
			*provider.VPCEndpointType,
			string(cond.Reason),
			readyStatus(cond.Status),
		}
		vpcEndpointStatus.WithLabelValues(lbs...).Set(float64(ts))
	}
	return nil
}

// RecordVPCEndpointStatus records the status of all vpcendpoint.
func RecordVPCEndpointStatus(cli client.Client) error {
	var errs error
	errs = multierr.Append(errs, recordAliCloudVPCEndpointAlpha1Status(cli))
	errs = multierr.Append(errs, recordAwsVPCEndpointAlpha1Status(cli))
	errs = multierr.Append(errs, recordAwsVPCEndpointBeta1Status(cli))
	return errs
}

// RecordNodeGroupDesiredCount records the desired node count of
// nodegroup/nodepool resources for the configured cloud vendor.
// It only resets metrics after a successful list to avoid alerting gaps
// on transient errors.
func RecordNodeGroupDesiredCount(cli client.Client, vendor string) error {
	switch vendor {
	case consts.VendorTypeAWS:
		return recordEKSNodeGroupDesiredCount(cli)
	case consts.VendorTypeGCP:
		return recordGKENodePoolDesiredCount(cli)
	case consts.VendorTypeAzure:
		return recordAzureNodePoolDesiredCount(cli)
	case consts.VendorTypeAliCloud:
		return recordAliCloudNodePoolDesiredCount(cli)
	default:
		return nil
	}
}

func recordEKSNodeGroupDesiredCount(cli client.Client) error {
	metrics, err := listEKSNodeGroupDesiredCount(cli)
	if err != nil {
		return err
	}

	additionalMetrics, err := listUpboundEKSNodeGroupDesiredCount(cli)
	if err != nil {
		return err
	}
	metrics = append(metrics, additionalMetrics...)

	additionalMetrics, err = listAutoScalingGroupDesiredCount(cli)
	if err != nil {
		return err
	}
	metrics = append(metrics, additionalMetrics...)

	nodegroupDesiredNodeCount.Reset()
	for _, metric := range metrics {
		lbs := []string{metric.provider, metric.cluster, metric.name, metric.crdType}
		nodegroupDesiredNodeCount.WithLabelValues(lbs...).Set(metric.desired)
	}
	return nil
}

func listEKSNodeGroupDesiredCount(cli client.Client) ([]desiredNodeCountMetric, error) {
	var nodegroups eksv1alpha1.NodeGroupList
	if err := cli.List(context.Background(), &nodegroups); err != nil {
		return nil, ignoreNoMatchError(err)
	}

	metrics := make([]desiredNodeCountMetric, 0, len(nodegroups.Items))
	for _, ng := range nodegroups.Items {
		desiredSize := eksNodeGroupDesiredSize(&ng)
		if desiredSize == nil {
			continue
		}
		metrics = append(metrics, desiredNodeCountMetric{
			provider: "eks",
			cluster:  ng.Spec.ForProvider.ClusterName,
			name:     ng.Name,
			crdType:  "eks.aws.crossplane.io/v1alpha1/NodeGroup",
			desired:  float64(*desiredSize),
		})
	}
	return metrics, nil
}

func eksNodeGroupDesiredSize(nodegroup *eksv1alpha1.NodeGroup) *int32 {
	if nodegroup.Spec.ForProvider.ScalingConfig != nil && nodegroup.Spec.ForProvider.ScalingConfig.DesiredSize != nil {
		return nodegroup.Spec.ForProvider.ScalingConfig.DesiredSize
	}

	return nodegroup.Status.AtProvider.ScalingConfig.DesiredSize
}

func listUpboundEKSNodeGroupDesiredCount(cli client.Client) ([]desiredNodeCountMetric, error) {
	var nodegroups upboundeksv1beta2.NodeGroupList
	if err := cli.List(context.Background(), &nodegroups); err != nil {
		return nil, ignoreNoMatchError(err)
	}

	metrics := make([]desiredNodeCountMetric, 0, len(nodegroups.Items))
	for _, ng := range nodegroups.Items {
		if ng.Spec.ForProvider.ScalingConfig == nil || ng.Spec.ForProvider.ScalingConfig.DesiredSize == nil {
			continue
		}
		clusterName := ""
		if ng.Spec.ForProvider.ClusterName != nil {
			clusterName = *ng.Spec.ForProvider.ClusterName
		}
		metrics = append(metrics, desiredNodeCountMetric{
			provider: "eks",
			cluster:  clusterName,
			name:     ng.Name,
			crdType:  "eks.aws.upbound.io/v1beta2/NodeGroup",
			desired:  *ng.Spec.ForProvider.ScalingConfig.DesiredSize,
		})
	}
	return metrics, nil
}

func listAutoScalingGroupDesiredCount(cli client.Client) ([]desiredNodeCountMetric, error) {
	var groups autoscalingv1beta1.AutoScalingGroupList
	if err := cli.List(context.Background(), &groups); err != nil {
		return nil, ignoreNoMatchError(err)
	}

	metrics := make([]desiredNodeCountMetric, 0, len(groups.Items))
	for _, group := range groups.Items {
		if group.Spec.ForProvider.DesiredCapacity == nil {
			continue
		}
		metrics = append(metrics, desiredNodeCountMetric{
			provider: "eks",
			cluster:  extractAutoScalingGroupClusterName(group.Spec.ForProvider.Tags),
			name:     group.Name,
			crdType:  "autoscaling.aws.crossplane.io/v1beta1/AutoScalingGroup",
			desired:  float64(*group.Spec.ForProvider.DesiredCapacity),
		})
	}
	return metrics, nil
}

func extractAutoScalingGroupClusterName(tags []*autoscalingv1beta1.Tag) string {
	for _, tag := range tags {
		if tag == nil || tag.Key == nil {
			continue
		}
		key := *tag.Key
		value := ""
		if tag.Value != nil {
			value = *tag.Value
		}
		switch {
		case key == "eks:cluster-name":
			return value
		case strings.HasPrefix(key, "kubernetes.io/cluster/"):
			return strings.TrimPrefix(key, "kubernetes.io/cluster/")
		}
	}

	return ""
}

func recordGKENodePoolDesiredCount(cli client.Client) error {
	var nodepools gkecontainerv1beta1.NodePoolList
	if err := cli.List(context.Background(), &nodepools); err != nil {
		return ignoreNoMatchError(err)
	}

	nodegroupDesiredNodeCount.Reset()
	for _, np := range nodepools.Items {
		if np.Spec.ForProvider.InitialNodeCount == nil {
			continue
		}
		lbs := []string{
			"gke",
			np.Spec.ForProvider.Cluster,
			np.Name,
			"gcp.NodePool",
		}
		nodegroupDesiredNodeCount.WithLabelValues(lbs...).Set(float64(*np.Spec.ForProvider.InitialNodeCount))
	}
	return nil
}

func recordAzureNodePoolDesiredCount(cli client.Client) error {
	var nodepools azureksv1beta1.KubernetesClusterNodePoolList
	if err := cli.List(context.Background(), &nodepools); err != nil {
		return ignoreNoMatchError(err)
	}

	nodegroupDesiredNodeCount.Reset()
	for _, np := range nodepools.Items {
		if np.Spec.ForProvider.NodeCount == nil {
			continue
		}
		clusterID := ""
		if np.Spec.ForProvider.KubernetesClusterID != nil {
			clusterID = *np.Spec.ForProvider.KubernetesClusterID
		}
		lbs := []string{
			"aks",
			clusterID,
			np.Name,
			"azure.KubernetesClusterNodePool",
		}
		nodegroupDesiredNodeCount.WithLabelValues(lbs...).Set(*np.Spec.ForProvider.NodeCount)
	}
	return nil
}

func recordAliCloudNodePoolDesiredCount(cli client.Client) error {
	var nodepools ackv1alpha1.KubernetesNodePoolList
	if err := cli.List(context.Background(), &nodepools); err != nil {
		return ignoreNoMatchError(err)
	}

	nodegroupDesiredNodeCount.Reset()
	for _, np := range nodepools.Items {
		if np.Spec.ForProvider.DesiredSize == nil {
			continue
		}
		desired, err := strconv.Atoi(*np.Spec.ForProvider.DesiredSize)
		if err != nil {
			ctrl.Log.WithName("metrics").Error(err, "failed to parse desired size",
				"nodepool", np.Name, "value", *np.Spec.ForProvider.DesiredSize)
			continue
		}
		clusterID := ""
		if np.Spec.ForProvider.ClusterID != nil {
			clusterID = *np.Spec.ForProvider.ClusterID
		}
		lbs := []string{
			"ack",
			clusterID,
			np.Name,
			"alicloud.KubernetesNodePool",
		}
		nodegroupDesiredNodeCount.WithLabelValues(lbs...).Set(float64(desired))
	}
	return nil
}
