package server

import (
	"context"
	"strings"

	"github.com/samber/lo"
	apiv1 "github.com/tidbcloud/cluster-service-ng/api/cluster-service-ng/v1beta"
	"github.com/tidbcloud/cluster-service-ng/domain/entity"
	schemav1 "github.com/tidbcloud/cluster-service-ng/pkg/storage/etcd/schema/v1"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	autoscalingv2 "k8s.io/api/autoscaling/v2"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

var componentTypeLookup map[string]apiv1.ComponentType = map[string]apiv1.ComponentType{
	entity.CoprocessorWorkerType: apiv1.ComponentType_COPROCESSOR_WORKER,
	"tidb":                       apiv1.ComponentType_TIDB,
	"tiproxy":                    apiv1.ComponentType_TIPROXY,
	entity.TiKVWorkerType:        apiv1.ComponentType_TIKV_WORKER,
	entity.TiDBWorkerType:        apiv1.ComponentType_TIDB_WORKER,
}

func (s *serverImpl) GetHPASpec(
	ctx context.Context,
	sharedPoolId string,
) ([]entity.ComponentHPASpec, error) {
	if sharedPoolId == "" {
		return nil, status.Errorf(codes.InvalidArgument, "sharedPoolId is required")
	}
	sharedPool, err := s.sharedPoolRepo.Load(ctx, sharedPoolId, false)
	if err != nil {
		return nil, err
	}
	remoteCli, err := s.dataplaneManager.GetClient(sharedPool.GetDataplaneName())
	if err != nil {
		return nil, status.Errorf(codes.Internal, "failed to get remote client for shared pool: %s", err)
	}
	namespace := "tidb" + sharedPool.GetId()
	hpas := new(autoscalingv2.HorizontalPodAutoscalerList)

	if err := remoteCli.List(ctx, hpas, &client.ListOptions{
		Namespace: namespace,
	}); err != nil {
		return nil, status.Errorf(codes.Internal, "failed to list hpas: %s", err)
	}
	out := make([]entity.ComponentHPASpec, 0)
	for i := range hpas.Items {
		hpa := hpas.Items[i]
		val := hpa.GetObjectMeta().GetLabels()[schemav1.LabelComponentKey]
		switch componentTypeLookup[val] {
		case apiv1.ComponentType_TIDB:
			group := hpa.GetLabels()[schemav1.LabelPingcapGroupKey]
			out = append(out, entity.ComponentHPASpec{
				Id:            strings.Split(group, "-")[1], // db-<Id>
				ComponentType: apiv1.ComponentType_TIDB,
				Spec:          toPBHPASpec(&hpa.Spec),
			})

		case apiv1.ComponentType_TIDB_WORKER:
			out = append(out, entity.ComponentHPASpec{
				ComponentType: apiv1.ComponentType_TIDB_WORKER,
				Spec:          toPBHPASpec(&hpa.Spec),
			})
		case apiv1.ComponentType_TIKV_WORKER:
			out = append(out, entity.ComponentHPASpec{
				ComponentType: apiv1.ComponentType_TIKV_WORKER,
				Spec:          toPBHPASpec(&hpa.Spec),
			})
		case apiv1.ComponentType_COPROCESSOR_WORKER:
			out = append(out, entity.ComponentHPASpec{
				ComponentType: apiv1.ComponentType_COPROCESSOR_WORKER,
				Spec:          toPBHPASpec(&hpa.Spec),
			})
		case apiv1.ComponentType_TIPROXY:
			group := hpa.GetLabels()[schemav1.LabelPingcapGroupKey]
			out = append(out, entity.ComponentHPASpec{
				Id:            strings.TrimPrefix(group, "db-"),
				ComponentType: apiv1.ComponentType_TIPROXY,
				Spec:          toPBHPASpec(&hpa.Spec),
			})
		default:
			// do nothing
		}
	}
	return out, nil
}

func toPBHPASpec(in *autoscalingv2.HorizontalPodAutoscalerSpec) *entity.HorizontalPodAutoscalerSpec {
	out := new(entity.HorizontalPodAutoscalerSpec)
	out.MinReplicas = in.MinReplicas
	out.MaxReplicas = &in.MaxReplicas
	for _, item := range in.Metrics {
		out.Metrics = append(out.Metrics, toPBMetricSpec(item))
	}
	out.Behavior = toPBHPABehavior(in.Behavior)

	return out
}

func toPBMetricSpec(in autoscalingv2.MetricSpec) *entity.MetricSpec {
	out := new(entity.MetricSpec)
	switch in.Type {
	case autoscalingv2.ObjectMetricSourceType:
		if in.Object != nil {
			out.Object = new(entity.ObjectMetricSource)
			out.Object.Target = toPBMetricTarget(in.Object.Target)
			out.Object.Metric = toPBMetricIdentifier(in.Object.Metric)
		}
	case autoscalingv2.PodsMetricSourceType:
		if in.Pods != nil {
			out.Pods = new(entity.PodsMetricSource)
			out.Pods.Target = toPBMetricTarget(in.Pods.Target)
			out.Pods.Metric = toPBMetricIdentifier(in.Pods.Metric)
		}
	case autoscalingv2.ResourceMetricSourceType:
		if in.Resource != nil {
			out.Resource = new(entity.ResourceMetricSource)
			out.Resource.Target = toPBMetricTarget(in.Resource.Target)
			out.Resource.Name = (*string)(&in.Resource.Name)
		}
	case autoscalingv2.ContainerResourceMetricSourceType:
		if in.ContainerResource != nil {
			out.ContainerResource = new(entity.ContainerResourceMetricSource)
			out.ContainerResource.Target = toPBMetricTarget(in.ContainerResource.Target)
			out.ContainerResource.Name = (*string)(&in.ContainerResource.Name)
			out.ContainerResource.Container = &in.ContainerResource.Container
		}
	case autoscalingv2.ExternalMetricSourceType:
		if in.External != nil {
			out.External = new(entity.ExternalMetricSource)
			out.External.Target = toPBMetricTarget(in.External.Target)
			out.External.Metric = toPBMetricIdentifier(in.External.Metric)
		}
	}
	return out
}

func toPBMetricTarget(in autoscalingv2.MetricTarget) *entity.MetricTarget {
	out := new(entity.MetricTarget)
	if in.Value != nil {
		out.Value = lo.ToPtr(in.Value.String())
	}
	out.AverageUtilization = in.AverageUtilization
	if in.AverageValue != nil {
		out.AverageValue = lo.ToPtr(in.AverageValue.String())
	}
	return out
}

func toPBMetricIdentifier(in autoscalingv2.MetricIdentifier) *entity.MetricIdentifier {
	out := new(entity.MetricIdentifier)
	out.Name = &in.Name
	return out
}

func toPBHPABehavior(in *autoscalingv2.HorizontalPodAutoscalerBehavior) *entity.HorizontalPodAutoscalerBehavior {
	if in == nil {
		return nil
	}
	out := new(entity.HorizontalPodAutoscalerBehavior)
	out.ScaleDown = toPBHPAScalingRules(in.ScaleDown)
	out.ScaleUp = toPBHPAScalingRules(in.ScaleUp)
	return out
}

func toPBHPAScalingRules(in *autoscalingv2.HPAScalingRules) *entity.HPAScalingRules {
	out := new(entity.HPAScalingRules)
	out.StabilizationWindowSeconds = in.StabilizationWindowSeconds
	out.Policies = toPBHPAScalingPolicy(in.Policies)
	return out
}

func toPBHPAScalingPolicy(in []autoscalingv2.HPAScalingPolicy) []*entity.HPAScalingPolicy {
	if in == nil {
		return nil
	}
	out := make([]*entity.HPAScalingPolicy, 0, len(in))
	for _, item := range in {
		switch item.Type {
		case autoscalingv2.PodsScalingPolicy:
			out = append(out, &entity.HPAScalingPolicy{
				Type:          string(autoscalingv2.PodsScalingPolicy),
				Value:         item.Value,
				PeriodSeconds: item.PeriodSeconds,
			})
		case autoscalingv2.PercentScalingPolicy:
			out = append(out, &entity.HPAScalingPolicy{
				Type:          string(autoscalingv2.PercentScalingPolicy),
				Value:         item.Value,
				PeriodSeconds: item.PeriodSeconds,
			})
		default:
			continue
		}
	}
	return out
}
