package eks

import (
	"context"
	"errors"
	"time"

	"github.com/crossplane-contrib/provider-aws/apis/ec2/v1beta1"
	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"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"
	"github.com/tidbcloud/infra-provider/regional-server/api/v1alpha1"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/controllers/vpc/task"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/utils/pointer"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
)

type SecurityGroup struct {
	*task.BaseTask
}

func NewSecurityGroup(ctx *task.Context) *SecurityGroup {
	return &SecurityGroup{&task.BaseTask{Context: ctx}}
}

// Name return the name of the task
func (s *SecurityGroup) Name() string {
	return "SecurityGroup"
}

func (s *SecurityGroup) getSecurityGroupName() string {
	if name := s.VPC.Annotations[v1alpha1.AnnotationKeyOwnCrossplaneNameSecurityGroup]; name != "" {
		return name
	}
	return namer.UniqueIdentifier(s.VPC.ObjectMeta)
}

func (s *SecurityGroup) Create(ctx context.Context) deploy.Result {
	name := s.getSecurityGroupName()
	tags := cloudv1.TagsFromLabels(s.VPC.Labels)
	key := types.NamespacedName{
		Name: name,
	}

	vpcRes := &v1beta1.VPC{}
	err := s.Local.Get(ctx, key, vpcRes)
	if err != nil {
		return deploy.Panic(err).WithRequeuAfter(time.Second * 30)
	}
	sg := &v1beta1.SecurityGroup{
		ObjectMeta: metav1.ObjectMeta{
			Name: name,
		},
		Spec: v1beta1.SecurityGroupSpec{
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{Name: s.ProviderConfigName()},
			},
			ForProvider: v1beta1.SecurityGroupParameters{
				Region:      s.VPC.Spec.Region,
				GroupName:   name,
				Description: "secure group for eks",
				Egress: []v1beta1.IPPermission{
					// Allow all traffic
					{
						IPProtocol: "-1",
						IPRanges: []v1beta1.IPRange{
							{
								CIDRIP: "0.0.0.0/0",
							},
						},
					},
				},
				Tags: util.GetEC2V1BetaTags(util.AddResourceName(tags, name)),
				VPCIDRef: &xpv1.Reference{
					Name: name,
				},
			},
		},
	}

	ingress := []v1beta1.IPPermission{
		{
			IPProtocol: "tcp",
			FromPort:   pointer.Int32(443),
			ToPort:     pointer.Int32(443),
			IPRanges: []v1beta1.IPRange{
				{
					CIDRIP: "0.0.0.0/0",
				},
			},
		},
		// For metrics agents send data to o11y infra via interface endpoints.
		{
			IPProtocol: "tcp",
			FromPort:   pointer.Int32(80),
			ToPort:     pointer.Int32(80),
			IPRanges: []v1beta1.IPRange{
				{
					CIDRIP: "0.0.0.0/0",
				},
			},
		},
		// For metrics agents send data to seed vector via interface endpoint.
		{
			IPProtocol: "tcp",
			FromPort:   pointer.Int32(9090),
			ToPort:     pointer.Int32(9090),
			IPRanges: []v1beta1.IPRange{
				{
					CIDRIP: "0.0.0.0/0",
				},
			},
		},
		// All Private Internets, See http://www.faqs.org/rfcs/rfc1918.html for more detail
		// CDC X-Region create a VPC peering connection between the source network and the target network in the other region.
		// Add security group rules so that downstream PD can access upstream TiKV
		{
			// all protocols
			IPProtocol: "-1",
			IPRanges: []v1beta1.IPRange{
				{
					CIDRIP: "10.0.0.0/8",
				},
				{
					CIDRIP: "172.16.0.0/12",
				},
				{
					CIDRIP: "192.168.0.0/16",
				},
			},
		},
	}
	mutateFn := func() error {
		err := controllerutil.SetControllerReference(vpcRes, sg, s.Local.Scheme())
		if err != nil {
			return err
		}

		// TODO: check if it is necessary to add rule for private link endpoint
		// ruleForEp := t.addSecureGroupRuleForPrivateLinkEndPoint(ctx, name)
		// if ruleForEp != nil {
		// 	// support to access private link endpoint
		// 	ingress = append(ingress, *ruleForEp)
		// }
		sg.Spec.ForProvider.Ingress = ingress

		return nil
	}
	// only reset the ownerReference for migreated resource.
	if s.VPC.Labels[v1alpha1.IsLegacyKey] == "true" {
		mutateFn = func() error {
			err := controllerutil.SetControllerReference(vpcRes, sg, s.Local.Scheme())
			if err != nil {
				return err
			}
			return nil
		}
	}
	_, err = controllerutil.CreateOrUpdate(ctx, s.Local, sg, mutateFn)
	if err != nil {
		return deploy.Panic(err).WithRequeuAfter(time.Second * 30)
	}
	return deploy.Done()
}

func (s *SecurityGroup) PostCreate(ctx context.Context) deploy.Result {
	name := s.getSecurityGroupName()
	key := types.NamespacedName{
		Name: name,
	}

	sg := &v1beta1.SecurityGroup{}
	if err := s.Local.Get(ctx, key, sg); err != nil {
		return deploy.Panic(err).WithRequeuAfter(time.Second * 30)
	}

	if sg.Status.AtProvider.SecurityGroupID == "" {
		return deploy.Panic(errors.New("security group is not available")).WithRequeuAfter(time.Second * 30)
	}

	if err := s.setSecurityGroupStatus(ctx, sg); err != nil {
		return deploy.Panic(err)
	}

	return deploy.Done()
}

func (s *SecurityGroup) setSecurityGroupStatus(ctx context.Context, sgRes *v1beta1.SecurityGroup) error {
	s.VPC.Status.SecurityGroup = v1alpha1.SecurityGroup{
		Name:            sgRes.Name,
		SecurityGroupID: sgRes.Status.AtProvider.SecurityGroupID,
	}
	// this update won't trigger reconcile
	return s.VPCRW.UpdateStatus(ctx, s.VPC)
}

func (s *SecurityGroup) Delete(ctx context.Context) deploy.Result {
	name := s.getSecurityGroupName()
	sg := &v1beta1.SecurityGroup{
		ObjectMeta: metav1.ObjectMeta{
			Name: name,
		},
	}

	if err := k8s.DeleteObject(ctx, s.Local, sg); err != nil {
		return deploy.Error(err)
	}
	return deploy.Done()
}

func (s *SecurityGroup) PostDelete(ctx context.Context) deploy.Result {
	name := s.getSecurityGroupName()
	return deploy.ErrorOrDone(k8s.GetDeletedObject(ctx, s.Local, types.NamespacedName{Name: name}, &v1beta1.SecurityGroup{}))
}
