package ack

import (
	"context"
	"errors"
	"fmt"
	"strconv"
	"strings"
	"time"

	nlb "github.com/alibabacloud-go/nlb-20220430/v4/client"
	"github.com/alibabacloud-go/tea/dara"
	"github.com/alibabacloud-go/tea/tea"
	ecsv1alpha1 "github.com/crossplane-contrib/provider-upjet-alibabacloud/apis/ecs/v1alpha1"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-provider/regional-server/api/v1alpha1"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/cloud/aliyun"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/consts"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/controllers"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/controllers/loadbalancer/task"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/deploy"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/k8s"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/namer"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/util"
	corev1 "k8s.io/api/core/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/utils/ptr"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

const (
	aliCloudPreserveLBOnDeleteAnno = "service.beta.kubernetes.io/alibaba-cloud-loadbalancer-preserve-lb-on-delete"
	aliCloudLoadBalancerIDLabel    = "service.k8s.alibaba/loadbalancer-id"
	proxyProtocolAnno              = "service.beta.kubernetes.io/alibaba-cloud-loadbalancer-proxy-protocol"
	preserveClientIPAnno           = "service.beta.kubernetes.io/alibaba-cloud-loadbalancer-preserve-client-ip"
	connectionDrainEnabledAnno     = "service.beta.kubernetes.io/alibaba-cloud-loadbalancer-connection-drain"
)

// Service create a service for the LB
type Service struct {
	*task.BaseTask
	aliyunCli aliyun.Client
}

func NewService(ctx *task.Context, aliyunCli aliyun.Client) deploy.Task {
	return &Service{
		BaseTask:  &task.BaseTask{Context: ctx},
		aliyunCli: aliyunCli,
	}
}

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

func (t *Service) Create(ctx context.Context) deploy.Result {
	t.BeginAt = util.Pointer(time.Now())
	remoteCli, err := t.GetRemoteClient(ctx)
	if err != nil {
		return deploy.Panic(err)
	}
	svc := t.BuildBasicService()

	var oldSvc *corev1.Service
	found := &corev1.Service{}
	if err := remoteCli.Get(ctx, client.ObjectKeyFromObject(svc), found); err != nil {
		if !apierrors.IsNotFound(err) {
			return deploy.Error(fmt.Errorf("failed to get service %s: %w", svc.Name, err))
		}
	} else {
		oldSvc = found
	}

	if t.LoadBalancer.Spec.Type == v1alpha1.PublicLoadBalancerType {
		sg, err := t.getSecurityGroup(ctx)
		if err != nil {
			return deploy.Panic(err)
		}
		if sg.Status.AtProvider.ID == nil {
			return deploy.Panic(fmt.Errorf("security group not ready"))
		}
		t.overlayPublicSvc(svc, *sg.Status.AtProvider.ID, oldSvc)
	} else {
		t.overlayPrivateSvc(svc, oldSvc)
	}
	return deploy.PanicOrDone(k8s.Apply(ctx, remoteCli, svc))
}

func (t *Service) PostCreate(ctx context.Context) deploy.Result {
	if t.VPC == nil {
		return deploy.Error(fmt.Errorf("vpc is not found"))
	}
	if t.privateIPsAreAllFound(t.LoadBalancer.Status.PrivateIPs) {
		return deploy.Done()
	}
	remoteCli, err := t.GetRemoteClient(ctx)
	if err != nil {
		return deploy.Panic(err)
	}
	svc := t.BuildServiceMeta(t.LoadBalancer)
	if err := remoteCli.Get(ctx, client.ObjectKeyFromObject(svc), svc); err != nil {
		return deploy.Panic(err)
	}
	lbID, ok := svc.Labels[aliCloudLoadBalancerIDLabel]
	if !ok {
		return deploy.Panic(errors.New("load balancer is empty in service"))
	}
	t.LoadBalancer.Status.CloudResourceId = lbID

	assumedRole := &aliyun.AssumedRole{
		RoleARN: fmt.Sprintf("acs:ram::%s:role/%s", t.VPC.Spec.AccountRef.AccountID, t.Config.DataPlaneRoleName),
	}
	cli, err := t.aliyunCli.NLB(assumedRole, aliyun.WithRegion(t.VPC.Spec.Region))
	if err != nil {
		return deploy.Panic(err)
	}

	req := &nlb.GetLoadBalancerAttributeRequest{
		LoadBalancerId: tea.String(lbID),
	}
	resp, err := cli.GetLoadBalancerAttributeWithContext(ctx, req, &dara.RuntimeOptions{})
	if err != nil {
		return deploy.Panic(err)
	}
	ips := make([]string, 0, len(resp.Body.ZoneMappings))
	for _, zoneInfo := range resp.Body.ZoneMappings {
		for _, lbAddr := range zoneInfo.LoadBalancerAddresses {
			for _, ip := range lbAddr.Ipv4LocalAddresses {
				ips = append(ips, tea.StringValue(ip))
			}
		}
	}

	if !t.privateIPsAreAllFound(ips) {
		return deploy.Error(fmt.Errorf("not all private ips are found, current %v", len(ips)))
	}
	// update only when all ips are found
	t.LoadBalancer.Status.PrivateIPs = ips
	return deploy.Done()
}

func (t *Service) privateIPsAreAllFound(ips []string) bool {
	if len(ips) == 0 {
		return false
	}
	total := len(t.VPC.Status.PrivateSubnets)
	if t.LoadBalancer.Spec.Type == v1alpha1.PublicLoadBalancerType {
		total = len(t.VPC.Status.PublicSubnets)
	}

	// aliyun nlb at least has two local ip in one az
	return len(ips) >= total*2
}

func (t *Service) Delete(ctx context.Context) deploy.Result {
	remoteCli, err := t.GetRemoteClient(ctx)
	if err != nil {
		return deploy.Error(err)
	}
	svc := t.BuildServiceMeta(t.LoadBalancer)
	if err := remoteCli.Get(ctx, client.ObjectKeyFromObject(svc), svc); err != nil {
		if client.IgnoreNotFound(err) != nil {
			return deploy.Error(fmt.Errorf("get service %s: %w", svc.Name, err))
		}
		return deploy.Done()
	}
	if _, ok := svc.Annotations[aliCloudPreserveLBOnDeleteAnno]; ok {
		origin := svc.DeepCopy()
		delete(svc.Annotations, aliCloudPreserveLBOnDeleteAnno)
		if err := remoteCli.Patch(ctx, svc, client.MergeFrom(origin)); err != nil {
			return deploy.Error(fmt.Errorf("patch service %s: %w", svc.Name, err))
		}
	}
	return deploy.PanicOrDone(k8s.DeleteObject(ctx, remoteCli, svc))
}

func (t *Service) overlayPrivateSvc(svc *corev1.Service, oldSvc *corev1.Service) {
	tags := t.EffectiveCloudTags(consts.ProviderTypeACK)
	// See https://www.alibabacloud.com/help/en/ack/ack-managed-and-ack-dedicated/user-guide/configure-nlb-instances-by-using-annotations?spm=a2c63.p38356.help-menu-85222.d_2_3_3_4.29fd49f4ITQgXj
	annotations := map[string]string{
		"service.beta.kubernetes.io/alibaba-cloud-loadbalancer-name":                     t.GetNLBName(t.Config.ResourceNamePrefix, svc.ObjectMeta),
		"service.beta.kubernetes.io/alibaba-cloud-loadbalancer-zone-maps":                getAliCloudVSwitchTagValue(t.VPC.Status.PrivateSubnets),
		"service.beta.kubernetes.io/alibaba-cloud-loadbalancer-address-type":             "intranet",
		"service.beta.kubernetes.io/alibaba-cloud-loadbalancer-additional-resource-tags": cloudv1.TagStringForAWS(tags),
		"service.beta.kubernetes.io/alibaba-cloud-loadbalancer-server-group-type":        "Ip",
		preserveClientIPAnno:           strconv.FormatBool(v1alpha1.ResolvePreserveClientIP(t.LoadBalancer.Spec.Type, t.LoadBalancer.Spec.PreserveClientIP)),
		aliCloudPreserveLBOnDeleteAnno: "true",
	}
	if t.LoadBalancer.Spec.GracefulUnhealthyEnabled {
		annotations[connectionDrainEnabledAnno] = "off"
	}

	if v := computeProxyProtocolAnnotation(oldSvc, t.Config.DisableProxyProtocol); v != "" {
		annotations[proxyProtocolAnno] = v
	}

	for k, v := range annotations {
		svc.Annotations[k] = v
	}
	// Caller-provided annotations win: merged last so they override the
	// controller defaults above.
	for k, v := range t.LoadBalancer.Spec.Annotations {
		svc.Annotations[k] = v
	}
	svc.Spec.LoadBalancerClass = ptr.To("alibabacloud.com/nlb")
}

func (t *Service) overlayPublicSvc(svc *corev1.Service, sgID string, oldSvc *corev1.Service) {
	tags := t.EffectiveCloudTags(consts.ProviderTypeACK)
	// See https://www.alibabacloud.com/help/en/ack/ack-managed-and-ack-dedicated/user-guide/configure-nlb-instances-by-using-annotations?spm=a2c63.p38356.help-menu-85222.d_2_3_3_4.29fd49f4ITQgXj
	annotations := map[string]string{
		"service.beta.kubernetes.io/alibaba-cloud-loadbalancer-name":                     t.GetNLBName(t.Config.ResourceNamePrefix, svc.ObjectMeta),
		"service.beta.kubernetes.io/alibaba-cloud-loadbalancer-zone-maps":                getAliCloudVSwitchTagValue(t.VPC.Status.PublicSubnets),
		"service.beta.kubernetes.io/alibaba-cloud-loadbalancer-additional-resource-tags": cloudv1.TagStringForAWS(tags),
		"service.beta.kubernetes.io/alibaba-cloud-loadbalancer-server-group-type":        "Ip",
		preserveClientIPAnno:           strconv.FormatBool(v1alpha1.ResolvePreserveClientIP(t.LoadBalancer.Spec.Type, t.LoadBalancer.Spec.PreserveClientIP)),
		aliCloudPreserveLBOnDeleteAnno: "true",
		"service.beta.kubernetes.io/alibaba-cloud-loadbalancer-security-group-ids": sgID,
	}
	if t.LoadBalancer.Spec.GracefulUnhealthyEnabled {
		annotations[connectionDrainEnabledAnno] = "off"
	}

	if v := computeProxyProtocolAnnotation(oldSvc, t.Config.DisableProxyProtocol); v != "" {
		annotations[proxyProtocolAnno] = v
	}

	for k, v := range annotations {
		svc.Annotations[k] = v
	}
	// Caller-provided annotations win: merged last so they override the
	// controller defaults above.
	for k, v := range t.LoadBalancer.Spec.Annotations {
		svc.Annotations[k] = v
	}
	// BuildBasicService sets spec.loadBalancerSourceRanges from IpWhiteList.
	// On the public path the same IpWhiteList is already enforced by the
	// {prefix}-tidb SecurityGroup (see securitygroup.go: deny-all + one
	// allow rule per CIDR/port). Bind the SG to the NLB above, so drop
	// the listener-level filter to avoid two layers fighting over the
	// same CIDR list. The private path (overlayPrivateSvc) does not
	// attach a SG, so it keeps loadBalancerSourceRanges as-is.
	svc.Spec.LoadBalancerSourceRanges = nil
	svc.Spec.LoadBalancerClass = ptr.To("alibabacloud.com/nlb")
}

func computeProxyProtocolAnnotation(oldSvc *corev1.Service, disableProxyProtocol bool) string {
	if oldSvc == nil {
		if !disableProxyProtocol {
			return "on"
		}
	} else if v, ok := oldSvc.Annotations[proxyProtocolAnno]; ok {
		return v
	}
	return ""
}

func getAliCloudVSwitchTagValue(subnets []v1alpha1.Subnet) string {
	vSwitches := make([]string, 0, len(subnets))
	for _, subnet := range subnets {
		vSwitches = append(vSwitches, fmt.Sprintf("%s:%s", subnet.AvailabilityZoneID, subnet.SubnetID))
	}
	return strings.Join(vSwitches, ",")
}

func (t *Service) getSecurityGroup(ctx context.Context) (*ecsv1alpha1.SecurityGroup, error) {
	sg := &ecsv1alpha1.SecurityGroup{
		ObjectMeta: metav1.ObjectMeta{
			Name: t.getSecurityGroupName(),
		},
	}
	if err := t.Local.Get(ctx, client.ObjectKeyFromObject(sg), sg); err != nil {
		return nil, err
	}
	return sg, nil
}

func (t *Service) getSecurityGroupName() string {
	return controllers.UniqueIdentifier(t.LoadBalancer.ObjectMeta, namer.WithPrefix(t.Config.ResourceNamePrefix+"-tidb"))
}
