package eks

import (
	"context"
	"fmt"
	"time"

	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"

	eksv1alpha1 "github.com/tidbcloud/eks-provider/api/v1alpha1"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-provider/pkg/controllers/common"

	"github.com/tidbcloud/eks-provider/controllers/cluster/task"
	"github.com/tidbcloud/eks-provider/controllers/cluster/task/utils"
	"github.com/tidbcloud/infra-provider/pkg/consts"
	"github.com/tidbcloud/infra-provider/pkg/deploy"
	"github.com/tidbcloud/infra-provider/pkg/k8s"
)

const (
	defaultServiceRequeue = 10 * time.Second
)

// Service task creates public endpoint LB for EKS based cluster.
type Service struct {
	*task.BaseTask
}

func NewService(ctx *task.Context) *Service {
	return &Service{BaseTask: &task.BaseTask{Context: ctx}}
}

func (t *Service) Name() string {
	return "Service"
}

func (t *Service) deletePublicLBService(ctx context.Context, cli client.Client) error {
	svc := &corev1.Service{
		ObjectMeta: metav1.ObjectMeta{
			Name:      t.Namer.GetPublicTiDBServiceName(),
			Namespace: t.Namer.GetTiDBNamespace(),
		},
	}
	return k8s.DeleteObjectCompleted(ctx, cli, svc)
}

func (t *Service) createPublicLBService(ctx context.Context, cli client.Client) error {
	svc := &corev1.Service{
		ObjectMeta: metav1.ObjectMeta{
			Name:      t.Namer.GetPublicTiDBServiceName(),
			Namespace: t.Namer.GetTiDBNamespace(),
		},
	}
	_, err := controllerutil.CreateOrUpdate(ctx, cli, svc, func() error {
		if err := t.setServiceAnnotations(svc); err != nil {
			return err
		}
		t.setServiceSpec(svc)
		return nil
	})
	return err
}

func (t *Service) setServiceAnnotations(svc *corev1.Service) error {
	if svc.Annotations == nil {
		svc.Annotations = make(map[string]string)
	}
	tags := cloudv1.TagsFromLabels(t.Cluster.Labels)
	tags = common.ConcatenateTags(tags, map[string]string{
		common.Cluster:   t.Cluster.Name,
		common.Component: common.Loadbalance,
		common.UsedBy:    common.CustomerTiDB,
	})
	tags[consts.TagKeyCrossplaneVendor] = consts.TagValCrossplaneVendor
	svc.Annotations["service.beta.kubernetes.io/aws-load-balancer-type"] = "external"
	svc.Annotations["service.beta.kubernetes.io/aws-load-balancer-nlb-target-type"] = "instance"
	svc.Annotations["service.beta.kubernetes.io/aws-load-balancer-scheme"] = "internet-facing"
	svc.Annotations["service.beta.kubernetes.io/aws-load-balancer-additional-resource-tags"] = cloudv1.TagStringForAWS(tags)
	svc.Annotations["service.beta.kubernetes.io/aws-load-balancer-target-group-attributes"] = "preserve_client_ip.enabled=true"
	// enable ip target type if dedicated SG feature is enabled
	if t.Cluster.FeatureEnabled(consts.ClusterFeatureDedicatedSG) {
		svc.Annotations["service.beta.kubernetes.io/aws-load-balancer-nlb-target-type"] = "ip"
	}
	if t.TCGetter == nil {
		return fmt.Errorf("tidbcluster getter is nil")
	}
	utils.SetNLBAnnoForTiproxy(svc.Annotations, t.TCGetter.IsTLSEnabled(), t.Cluster.Spec.SqlEndpoints.UseTiProxyAsLBTarget)

	cluster := t.Cluster
	if v, ok := cluster.Annotations[eksv1alpha1.ClusterPublicServiceLBNameAnnKey]; ok {
		svc.Annotations[awsLBNameAnnKey] = v
	}
	return nil
}

func (t *Service) setServiceSpec(svc *corev1.Service) {
	svc.Spec.Type = corev1.ServiceTypeLoadBalancer
	svc.Spec.SessionAffinity = corev1.ServiceAffinityNone
	svc.Spec.ExternalTrafficPolicy = corev1.ServiceExternalTrafficPolicyTypeLocal
	svc.Spec.LoadBalancerSourceRanges = t.Cluster.Spec.SqlEndpoints.Public.LoadBalancerSourceRanges

	// p.NodePort may be set automatically, so set field one by one to avoid diff
	if len(svc.Spec.Ports) != 1 {
		svc.Spec.Ports = []corev1.ServicePort{{}}
	}
	p := &svc.Spec.Ports[0]
	p.Name = "mysql-client"
	p.Port = t.Cluster.GetPublicSQLEndpointPort()
	p.TargetPort = t.Cluster.Spec.SqlEndpoints.GetTargetPort()
	p.Protocol = corev1.ProtocolTCP
	svc.Spec.Selector = t.Namer.WithComponent(string(t.Cluster.Spec.SqlEndpoints.GetTargetComponent())).GetPodLabels()
}

func (t *Service) Create(ctx context.Context) deploy.Result {
	cli := t.EnvProvider.GetClient()
	// delete the public LB service it is disabled in cluster spec
	if !t.Cluster.Spec.SqlEndpoints.Public.IsEnabled() {
		return deploy.ErrorOrDone(t.deletePublicLBService(ctx, cli))
	}
	return deploy.ErrorOrDone(t.createPublicLBService(ctx, cli))
}

func (t *Service) PostCreate(ctx context.Context) deploy.Result {
	cli := t.EnvProvider.GetClient()

	if !t.Cluster.Spec.SqlEndpoints.Public.IsEnabled() {
		return deploy.ErrorOrDone(k8s.GetDeletedObject(ctx, cli, types.NamespacedName{
			Name:      t.Namer.GetPublicTiDBServiceName(),
			Namespace: t.Namer.GetTiDBNamespace(),
		}, &corev1.Service{}))
	}
	var svc corev1.Service
	key := types.NamespacedName{
		Namespace: t.Namer.GetTiDBNamespace(),
		Name:      t.Namer.GetPublicTiDBServiceName(),
	}
	if err := cli.Get(ctx, key, &svc); err != nil {
		return deploy.Error(err)
	}
	if len(svc.Status.LoadBalancer.Ingress) == 0 {
		return deploy.Error(fmt.Errorf("service load balancer ingress not ready")).WithRequeuAfter(defaultServiceRequeue)
	}
	// the ingress hostname for LB service is generated by AWS automatically
	// we will create a DNS A record for the public endpoint
	// the endpoint status will be updated when the DNS alias is ensured
	return deploy.Done()
}

func (t *Service) Delete(ctx context.Context) deploy.Result {
	cli := t.EnvProvider.GetClient()
	return deploy.ErrorOrDone(t.deletePublicLBService(ctx, cli))
}

func (t *Service) PostDelete(ctx context.Context) deploy.Result {
	cli := t.EnvProvider.GetClient()
	return deploy.ErrorOrDone(k8s.GetDeletedObject(ctx, cli, types.NamespacedName{
		Name:      t.Namer.GetPublicTiDBServiceName(),
		Namespace: t.Namer.GetTiDBNamespace(),
	}, &corev1.Service{}))
}
