package ack

import (
	"context"
	"fmt"
	"slices"
	"strings"

	slb "github.com/alibabacloud-go/slb-20140515/v4/client"
	ackv1alpha1 "github.com/crossplane-contrib/provider-upjet-alibabacloud/apis/ack/v1alpha1"
	slbv1alpha1 "github.com/crossplane-contrib/provider-upjet-alibabacloud/apis/slb/v1alpha1"
	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/utils/ptr"
	"sigs.k8s.io/controller-runtime/pkg/client"

	"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/appbox/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"
)

const (
	// aclRefLabelKey is a label used for grouping ACLEntryAttachment belong to a specific acl
	aclRefLabelKey    = "acl-reference"
	apiserverPort     = 6443 // ACK apiserver uses port 6443
	cidrAnnotationKey = "tidbcloud.com/ack-last-applied-acl-cidrs"
)

type acl struct {
	*task.BaseTask
	aliyunCli aliyun.Client
}

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

func (t *acl) Name() string {
	return "ACL"
}

func (t *acl) Create(ctx context.Context) deploy.Result {
	name := t.getACLName()
	providerConfigName := t.ProviderConfigName()
	tags := controllers.ConvertToCloudTags(t.APPBox.Labels, consts.ProviderTypeACK)
	labels := util.MergeMaps(t.APPBox.Labels, map[string]string{aclRefLabelKey: name})
	acl := &slbv1alpha1.Acl{
		ObjectMeta: metav1.ObjectMeta{
			Name:   name,
			Labels: labels,
		},
		Spec: slbv1alpha1.AclSpec{
			ForProvider: slbv1alpha1.AclParameters{
				Name:   ptr.To(name),
				Region: ptr.To(t.APPBox.Spec.Region),
				Tags:   util.ToMapPointer(tags),
			},
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{Name: providerConfigName},
				ManagementPolicies:      controllers.GetResourceManagementPolicies(),
			},
		},
	}
	if err := k8s.Apply(ctx, t.Local, acl); err != nil {
		return deploy.Panic(err)
	}
	cidrs := t.getCidrs()
	// Create new entries
	for _, cidr := range cidrs {
		if cidr == "0.0.0.0/0" {
			continue // Aliyun ACL entry can't be 0.0.0.0/0
		}
		attachmentName := getAttachmentName(t.APPBox.ObjectMeta, t.Config.ResourceNamePrefix, cidr)
		attachment := &slbv1alpha1.AclEntryAttachment{
			ObjectMeta: metav1.ObjectMeta{
				Name:   attachmentName,
				Labels: labels,
			},
			Spec: slbv1alpha1.AclEntryAttachmentSpec{
				ForProvider: slbv1alpha1.AclEntryAttachmentParameters{
					Region:   ptr.To(t.APPBox.Spec.Region),
					ACLIDRef: &xpv1.Reference{Name: name},
					Entry:    ptr.To(cidr),
				},
				ResourceSpec: xpv1.ResourceSpec{
					ProviderConfigReference: &xpv1.Reference{Name: providerConfigName},
					ManagementPolicies:      controllers.GetResourceManagementPolicies(),
				},
			},
		}
		if err := k8s.Apply(ctx, t.Local, attachment); err != nil {
			return deploy.Panic(err)
		}
	}
	// Remove extra entries
	attachList := &slbv1alpha1.AclEntryAttachmentList{}
	matchLabels := client.MatchingLabels{aclRefLabelKey: name}
	if err := t.Local.List(ctx, attachList, matchLabels); err != nil {
		return deploy.Panic(err)
	}
	existingEntries := map[string]slbv1alpha1.AclEntryAttachment{}
	for _, attachment := range attachList.Items {
		if attachment.Spec.ForProvider.Entry != nil {
			existingEntries[*attachment.Spec.ForProvider.Entry] = attachment
		}
	}
	for cidr, attachment := range existingEntries {
		if !slices.Contains(cidrs, cidr) {
			// delete extra entry
			if err := controllers.DeleteUpboundResource(ctx, t.Local, &attachment); err != nil {
				return deploy.Panic(err)
			}
			t.Logger.Info("Deleted extra ACL entry", "cidr", cidr)
		}
	}
	return deploy.Done()
}

func (t *acl) PostCreate(ctx context.Context) deploy.Result {
	lastCidrs := t.APPBox.Annotations[cidrAnnotationKey]
	// compare last applied cidrs with expected cidrs, if they are the same, no need to update the slb listener
	expectedCidrs := strings.Join(t.getCidrs(), ",")
	if lastCidrs == expectedCidrs {
		t.Logger.Info("No change in apiserver public access cidrs, skip updating slb listener ACL")
		return deploy.Done()
	}
	// get apiserver SLB id from managedkubernetes
	ackName := t.K8sName()
	cluster := &ackv1alpha1.ManagedKubernetes{
		ObjectMeta: metav1.ObjectMeta{
			Name: ackName,
		},
	}
	if err := t.Local.Get(ctx, client.ObjectKeyFromObject(cluster), cluster); err != nil {
		return deploy.Panic(err)
	}
	slbId := ptr.Deref(cluster.Status.AtProvider.SlbID, "")
	if slbId == "" {
		return deploy.Panic(fmt.Errorf("failed to get slb ID"))
	}

	// get ACL id
	name := t.getACLName()
	acl := &slbv1alpha1.Acl{
		ObjectMeta: metav1.ObjectMeta{
			Name: name,
		},
	}
	if err := t.Local.Get(ctx, client.ObjectKeyFromObject(acl), acl); err != nil {
		return deploy.Error(err)
	}
	aclId := ptr.Deref(acl.Status.AtProvider.ID, "")
	if aclId == "" {
		return deploy.Panic(fmt.Errorf("ACL is not ready, failed to get ACL ID"))
	}

	// Configure ACL for apiserver SLB
	ar := &aliyun.AssumedRole{RoleARN: util.GetRAMRoleARN(t.VPC.Spec.AccountRef.AccountID, t.Config.DataPlaneRoleName)}
	slbCli, err := t.aliyunCli.SLB(ar, aliyun.WithRegion(t.APPBox.Spec.Region))
	if err != nil {
		return deploy.Panic(err)
	}
	req := &slb.SetLoadBalancerTCPListenerAttributeRequest{
		LoadBalancerId: ptr.To(slbId),
		ListenerPort:   ptr.To(int32(apiserverPort)),
		RegionId:       ptr.To(t.APPBox.Spec.Region),
	}
	// If 0.0.0.0/0 is configured, no need to attach the ACL to the slb listener
	if slices.Contains(t.Config.APPBoxConfig.ApiserverPublicAccessCidrs, "0.0.0.0/0") {
		t.Logger.Info("ApiserverPublicAccessCidrs contains 0.0.0.0/0, disable loadbalancer ACL to allow all")
		req.AclStatus = ptr.To("off")
	} else {
		req.AclStatus = ptr.To("on")
		req.AclType = ptr.To("white")
		req.AclId = ptr.To(aclId)
	}
	if _, err := slbCli.SetLoadBalancerTCPListenerAttribute(req); err != nil {
		return deploy.Panic(err)
	}
	// Record the applied cidrs to appbox annotation to avoid unnecessary sdk calls
	t.APPBox.Annotations[cidrAnnotationKey] = strings.Join(t.getCidrs(), ",")
	return deploy.Done()
}

func (t *acl) Delete(ctx context.Context) deploy.Result {
	aclName := t.getACLName()
	attachList := &slbv1alpha1.AclEntryAttachmentList{}
	matchLabels := client.MatchingLabels{aclRefLabelKey: aclName}
	if err := t.Local.List(ctx, attachList, matchLabels); err != nil {
		return deploy.Error(err)
	}
	for _, attachment := range attachList.Items {
		if err := controllers.DeleteUpboundResource(ctx, t.Local, &attachment); err != nil {
			return deploy.Error(err)
		}
	}
	acl := &slbv1alpha1.Acl{
		ObjectMeta: metav1.ObjectMeta{Name: aclName},
	}
	return deploy.PanicOrDone(controllers.DeleteUpboundResource(ctx, t.Local, acl))
}

func getAttachmentName(meta metav1.ObjectMeta, prefix, ip string) string {
	s := strings.Split(ip, "/")[0] // remove the /xx suffix if exists, it's always /32 or /0
	s = strings.ReplaceAll(s, ".", "-")
	return controllers.UniqueIdentifier(meta, namer.WithPrefix(prefix+"-"+s))
}

func (t *acl) getACLName() string {
	return controllers.UniqueIdentifier(t.APPBox.ObjectMeta, namer.WithPrefix(t.Config.ResourceNamePrefix))
}

func (t *acl) getCidrs() []string {
	// copy the slice to avoid modifying the global configuration
	cidrs := make([]string, len(t.Config.APPBoxConfig.ApiserverPublicAccessCidrs))
	copy(cidrs, t.Config.APPBoxConfig.ApiserverPublicAccessCidrs)
	// Add necessary CIDR blocks according to the document
	// https://www.alibabacloud.com/help/en/ack/ack-managed-and-ack-dedicated/user-guide/configure-network-acls-for-the-api-server-of-an-ack-cluster
	// Adding CIDR block required by ACK control plane
	cidrs = append(cidrs, "100.104.0.0/16") // nolint:makezero
	cidrs = append(cidrs, t.VPC.Spec.CIDR)  // nolint:makezero
	// sort and remove duplicates
	slices.Sort(cidrs)
	cidrs = slices.Compact(cidrs)
	return cidrs
}
