package eks

import (
	"context"
	"fmt"
	"net"
	"strings"
	"time"

	"github.com/aws/aws-sdk-go-v2/service/ec2"
	"github.com/crossplane-contrib/provider-aws/apis/eks/v1alpha1"
	"github.com/crossplane-contrib/provider-aws/apis/eks/v1beta1"
	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/client-go/util/workqueue"
	"k8s.io/utils/pointer"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/event"
	"sigs.k8s.io/controller-runtime/pkg/handler"
	"sigs.k8s.io/controller-runtime/pkg/log"
	"sigs.k8s.io/controller-runtime/pkg/reconcile"

	"github.com/tidbcloud/eks-provider/controllers/network/task"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-provider/pkg/cloud/aws"
	"github.com/tidbcloud/infra-provider/pkg/consts"
	"github.com/tidbcloud/infra-provider/pkg/deploy"
	"github.com/tidbcloud/infra-provider/pkg/k8s"
	"github.com/tidbcloud/infra-provider/pkg/namer"
	"github.com/tidbcloud/infra-provider/pkg/util"
)

type EKS struct {
	*task.BaseTask
	awsCli aws.Client
}

func NewEKS(ctx *task.Context, awsCli aws.Client) *EKS {
	return &EKS{BaseTask: &task.BaseTask{Context: ctx}, awsCli: awsCli}
}

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

func (t *EKS) Create(ctx context.Context) deploy.Result {
	t.BeginAt = util.Pointer(time.Now())
	name := namer.UniqueIdentifier(t.Network.ObjectMeta)
	eksName := namer.LegacyBuildClusterNameByNetwork(t.Network)
	util.SetAnnotation(t.Network, consts.AnnotationKeyEKSName, eksName)
	util.SetAnnotation(t.Network, consts.AnnotationKeyDataplaneName, eksName)
	tags := cloudv1.TagsFromLabels(t.Network.Labels)
	tags = util.AddResourceName(tags, eksName)
	tags[consts.TagKeyCrossplaneVendor] = consts.TagValCrossplaneVendor

	endpointPublicAccess := true

	// We don't do version update in network controller
	// so use the static global EKS default version specified in configuration
	// version changes will be ignored for existing eks cluster by calling t.ignoreChanges
	version := t.Config.Network.GetK8sVersion(t.GetTenantID(), t.GetProjectID())
	if version == "" {
		return deploy.Panic(fmt.Errorf("no version specified in configuration"))
	}
	cluster := &v1beta1.Cluster{
		ObjectMeta: metav1.ObjectMeta{
			Name:   eksName,
			Labels: t.Namer.Labels(),
		},
		Spec: v1beta1.ClusterSpec{
			ForProvider: v1beta1.ClusterParameters{
				Region:  pointer.String(t.Network.Spec.Region),
				Version: pointer.String(version),
				ResourcesVpcConfig: v1beta1.VpcConfigRequest{
					EndpointPrivateAccess: pointer.Bool(true),
					EndpointPublicAccess:  pointer.Bool(endpointPublicAccess),
				},
				RoleArn: t.EKSNetworkSpec.ServiceRoleArn,
				Tags:    tags,
			},
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{Name: name},
			},
		},
	}

	// TODO: don't use ids directly, use IDRef or IDSelector instead
	cluster.Spec.ForProvider.ResourcesVpcConfig.SecurityGroupIDs = append(cluster.Spec.ForProvider.ResourcesVpcConfig.SecurityGroupIDs, t.EKSNetworkSpec.SecurityGroupId)
	cluster.Spec.ForProvider.ResourcesVpcConfig.SubnetIDs = append(cluster.Spec.ForProvider.ResourcesVpcConfig.SubnetIDs, t.EKSNetworkSpec.SubnetIds...)

	if endpointPublicAccess {
		cluster.Spec.ForProvider.ResourcesVpcConfig.PublicAccessCidrs = t.Config.Network.ApiserverPublicAccessCidrs
	}
	if err := t.ignoreChanges(ctx, cluster); err != nil {
		return deploy.Panic(err)
	}

	return deploy.PanicOrDone(k8s.Apply(ctx, t.Local, cluster))
}

// ignore some fields changes for existing clusters
func (t *EKS) ignoreChanges(ctx context.Context, newCluster *v1beta1.Cluster) error {
	key := types.NamespacedName{
		Name: namer.LegacyBuildClusterNameByNetwork(t.Network),
	}
	var c v1beta1.Cluster
	err := t.Local.Get(ctx, key, &c)
	if err != nil {
		if apierrors.IsNotFound(err) {
			return nil
		}
		return err
	}
	// ignore eks version change
	// otherwise all eks clusters will be updated at the same time once we change the global version
	newCluster.Spec.ForProvider.Version = c.Spec.ForProvider.Version
	return nil
}

func (t *EKS) PostCreate(ctx context.Context) (res deploy.Result) {
	key := types.NamespacedName{
		Name: namer.LegacyBuildClusterNameByNetwork(t.Network),
	}
	var c v1beta1.Cluster
	err := t.Local.Get(ctx, key, &c)
	if err != nil {
		return deploy.PanicOrDone(err)
	}
	t.Cluster = task.NewEKSClusterIdentifier(&c)
	status := c.Status.AtProvider.Status
	if status != v1beta1.ClusterStatusActive {
		return deploy.Error(fmt.Errorf("eks cluster status not ready: %v", status))
	}
	if err = t.authorizeSecurityGroupIngress(ctx, c.Status.AtProvider.ResourcesVpcConfig.ClusterSecurityGroupID); err != nil {
		return deploy.Error(fmt.Errorf("update eks cluster securitygroup rule failed, %s", err.Error()))
	}
	return deploy.Done()
}

func (t *EKS) Delete(ctx context.Context) deploy.Result {
	cluster := &v1beta1.Cluster{
		ObjectMeta: metav1.ObjectMeta{
			Name: namer.LegacyBuildClusterNameByNetwork(t.Network),
		},
	}
	if err := k8s.DeleteObject(ctx, t.Local, cluster); err != nil {
		return deploy.Error(err)
	}

	if err := k8s.PatchObject(ctx, t.Local, cluster, k8s.RemoveCrossplaneExternalCreatePending, k8s.DeletionTrigger); err != nil {
		return deploy.Error(err)
	}

	// Delete all addons of the eks cluster.
	// they are created by the k8s version controller
	if err := t.Local.DeleteAllOf(ctx, &v1alpha1.Addon{}, client.MatchingLabels{
		"eks": cluster.Name,
	}); err != nil && !apierrors.IsNotFound(err) {
		return deploy.Error(fmt.Errorf("delete addons failed: %v", err))
	}

	return deploy.Done()
}

func (t *EKS) PostDelete(ctx context.Context) deploy.Result {
	key := types.NamespacedName{
		Name: namer.LegacyBuildClusterNameByNetwork(t.Network),
	}
	var c v1beta1.Cluster
	return deploy.ErrorOrDone(k8s.GetDeletedObject(ctx, t.Local, key, &c))
}

// TODO(liubo02): Change it to avoid too many api calls.
// authorizeSecurityGroupIngress adds inbound rules to the security group of EKS control plane.
func (t *EKS) authorizeSecurityGroupIngress(ctx context.Context, eskClusterSgId string) error {
	cidrs := []string{"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"}
	ipProtocol := "-1"
	ec2Cli := t.awsCli.EC2(aws.GetRoleFromAccount(t.Account))
	for _, cidr := range cidrs {
		if _, err := ec2Cli.AuthorizeSecurityGroupIngress(ctx, &ec2.AuthorizeSecurityGroupIngressInput{
			CidrIp:  &cidr,
			GroupId: &eskClusterSgId,
			// all protocol
			IpProtocol: &ipProtocol,
		}); err != nil {
			if !strings.Contains(fmt.Sprintf("%s", err), "InvalidPermission.Duplicate") {
				return fmt.Errorf("failed authorize security group ingress: %v", err)
			}
		}
	}

	return nil
}

// HandlerFuncs to enqueue network.
var HandlerFuncs = handler.Funcs{
	UpdateFunc: func(ctx context.Context, e event.UpdateEvent, q workqueue.TypedRateLimitingInterface[ctrl.Request]) {
		logger := log.Log
		old, ok := e.ObjectOld.(*v1beta1.Cluster)
		if !ok {
			logger.Error(fmt.Errorf("unexpected type"), "failed to convert old object to Cluster", "object", e.ObjectOld)
			return
		}
		new, ok := e.ObjectNew.(*v1beta1.Cluster)
		if !ok {
			logger.Error(fmt.Errorf("unexpected type"), "failed to convert old object to Cluster", "object", e.ObjectNew)
			return
		}

		if old.Status.AtProvider.Status != v1beta1.ClusterStatusActive &&
			new.Status.AtProvider.Status == v1beta1.ClusterStatusActive {

			ns := new.GetLabels()[namer.LegacyNetworkNamespaceSelectorKey]
			name := new.GetLabels()[namer.LegacyNetworkSelectorKey]

			if ns == "" || name == "" {
				logger.Error(fmt.Errorf("unexpected labels"), "failed to get ns or name from labels", "eks name", new.GetName())
			}

			logger.Info("enqueue network cause eks cluster become active", "namespace", ns, "name", name)
			q.Add(reconcile.Request{NamespacedName: types.NamespacedName{
				Namespace: ns,
				Name:      name,
			}})
		}
	},
}

func ServiceIPv4CIDR(cidr string) (string, error) {
	_, ipNet, err := net.ParseCIDR(cidr)
	if err != nil {
		return "", err
	}
	ipv4 := ipNet.IP.To4()
	if ipv4 == nil {
		return "", fmt.Errorf("now only support ipv4 in shared vpc cidr")
	}
	if !ipv4.Mask(net.IPv4Mask(255, 0, 0, 0)).Equal(net.IPv4(10, 0, 0, 0)) {
		return "", fmt.Errorf("cidr of shared vpc must be in 10.0.0.0/8")
	}
	ones, bits := ipNet.Mask.Size()
	if ones != 16 || bits != 32 {
		return "", fmt.Errorf("cidr of shared vpc must be 10.x.0.0/16")
	}

	group := ipv4[1] / 8
	pos := 8 - ipv4[1]%8

	// Service CIDR can not overlap with VPC CIDR for EKS
	// We allocate svc cidr with 19 bits mask and from 10.255.0.0 to 10.228.0.0
	// 8 * 10.x.0.0/16 will consume 1 10.x.0.0/16 as their svc cidr
	// 228 = 256 - 256/9
	//
	// e.g. vpc is 10.2.0.0/16
	// group is 0
	// pos is 6
	// svc cidr: 10.255.192.0
	svcCIDR := net.IPNet{
		IP:   net.IPv4(ipv4[0], 255-group, pos<<5, 0),
		Mask: net.CIDRMask(19, 32),
	}

	return svcCIDR.String(), nil
}
