package privatelink

import (
	"context"
	"fmt"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/service/ec2"
	"github.com/aws/aws-sdk-go/service/ec2/ec2iface"
	ec2v1alpha1 "github.com/crossplane-contrib/provider-aws/apis/ec2/v1alpha1"
	"github.com/crossplane/crossplane-runtime/pkg/meta"
	"github.com/tidbcloud/infra-provider/pkg/consts"
	"github.com/tidbcloud/infra-provider/pkg/namer"
	"github.com/tidbcloud/infra-provider/pkg/procedure"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/types"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
)

const (
	vpcEpResourceType = "vpc-endpoint"
	vpcEpName         = vpcEpResourceType
)

type vpcEp struct {
	plMngr privateLinkMngr
	scheme *runtime.Scheme
}

// Deploy deploys resource for the specified tenant.
func (f *vpcEp) Deploy(ctx context.Context, _, infraCli client.Client, info ProcedureInfo) error {
	svcName, err := f.plMngr.getEndpointServiceName(ctx, infraCli, info, namer.UniqueIdentifier(info.privatelink.ObjectMeta))
	if err != nil {
		return err
	}
	ep := getVpcEndpoint(info, &svcName)
	_, err = controllerutil.CreateOrUpdate(ctx, infraCli, ep, func() error {
		mutateVpcEndpointSpec(ep, info, &svcName)
		err := controllerutil.SetControllerReference(info.privatelink, ep, f.scheme)
		if procedure.IsAlreadyOwnedError(err) {
			return nil
		}
		return err
	})
	return err
}

func mutateVpcEndpointSpec(ep *ec2v1alpha1.VPCEndpoint, info ProcedureInfo, svcName *string) {
	ep.Spec.ProviderConfigReference = getProviderConfigRef(info.privatelink.Spec.ClientProviderRef)
	ep.Spec.ForProvider.Region = info.Region
	ep.Spec.ForProvider.ServiceName = svcName
	ep.Spec.ForProvider.VPCID = &info.VpcID
	ep.Spec.ForProvider.VPCEndpointType = aws.String("Interface")
	ep.Spec.ForProvider.SecurityGroupIDs = aws.StringSlice(info.SecurityGroupIDs)
	ep.Spec.ForProvider.SubnetIDs = aws.StringSlice(info.SubnetIDs)
	if len(ep.Spec.ForProvider.TagSpecifications) != 1 || aws.StringValue(ep.Spec.ForProvider.TagSpecifications[0].ResourceType) != vpcEpResourceType {
		ep.Spec.ForProvider.TagSpecifications = []*ec2v1alpha1.TagSpecification{
			{
				ResourceType: aws.String(vpcEpResourceType),
				Tags: []*ec2v1alpha1.Tag{
					{
						Key:   aws.String(consts.TagKeyCrossplaneVendor),
						Value: aws.String(consts.TagValCrossplaneVendor),
					},
				},
			},
		}
		return
	}
	for _, tag := range ep.Spec.ForProvider.TagSpecifications[0].Tags {
		if tag == nil || aws.StringValue(tag.Key) != consts.TagKeyCrossplaneVendor {
			continue
		}
		tag.Value = aws.String(consts.TagValCrossplaneVendor)
		return
	}
	ep.Spec.ForProvider.TagSpecifications[0].Tags = append(ep.Spec.ForProvider.TagSpecifications[0].Tags, &ec2v1alpha1.Tag{
		Key:   aws.String(consts.TagKeyCrossplaneVendor),
		Value: aws.String(consts.TagValCrossplaneVendor),
	})
}

func getVpcEndpoint(info ProcedureInfo, svcName *string) *ec2v1alpha1.VPCEndpoint {
	ep := &ec2v1alpha1.VPCEndpoint{}
	ep.Name = namer.UniqueIdentifier(info.privatelink.ObjectMeta)
	mutateVpcEndpointSpec(ep, info, svcName)
	return ep
}

// Status retrieves the status for the deploying step.
func (f *vpcEp) Status(ctx context.Context, baseCli client.Reader, infraCli client.Client, info ProcedureInfo) (bool, error) {
	ep := &ec2v1alpha1.VPCEndpoint{}
	err := infraCli.Get(ctx, types.NamespacedName{Name: namer.UniqueIdentifier(info.privatelink.ObjectMeta)}, ep)
	if err != nil {
		return false, err
	}
	if isEndpointReady(ep) {
		err = f.addEndpointDNSAnnotation(ctx, infraCli, info)
		if err != nil {
			return false, err
		}
		return true, nil
	}
	// need to accept the private link
	epSvc := &ec2v1alpha1.VPCEndpointServiceConfiguration{}
	err = infraCli.Get(ctx, types.NamespacedName{Name: namer.UniqueIdentifier(info.privatelink.ObjectMeta)}, epSvc)
	if err != nil {
		return false, err
	}
	// accept the private link
	// TODO: move this to a dedicated procedure when issue
	// https://github.com/crossplane-contrib/provider-aws/issues/1043 is resolved.
	if ep.Annotations == nil || ep.Annotations[meta.AnnotationKeyExternalName] == "" {
		return false, fmt.Errorf("vpc endpoint external name is empty")
	}
	if epSvc.Annotations == nil || epSvc.Annotations[meta.AnnotationKeyExternalName] == "" {
		return false, fmt.Errorf("vpc endpoint service external name is empty")
	}
	err = f.plMngr.acceptPrivateLink(ctx, epSvc.Annotations[meta.AnnotationKeyExternalName], ep.Annotations[meta.AnnotationKeyExternalName], info)
	if err != nil {
		return false, err
	}

	return false, nil
}

// addEndpointDNSAnnotation adds endpoint DNS annotation to private link object
func (f *vpcEp) addEndpointDNSAnnotation(ctx context.Context, infraCli client.Client, info ProcedureInfo) error {
	plObj := info.privatelink.DeepCopy()
	_, err := controllerutil.CreateOrUpdate(ctx, infraCli, plObj, func() error {
		// retrieve the vpc endpoint DNS name
		epDNSName, err := f.plMngr.getEndpointDNS(ctx, infraCli, namer.UniqueIdentifier(info.privatelink.ObjectMeta), info)
		if err != nil {
			return err
		}
		if plObj.Annotations == nil {
			plObj.Annotations = map[string]string{}
		}
		plObj.Annotations[procedure.AnnotationKeyEndpointDNS] = epDNSName
		return nil
	})
	return err
}

// isEndpointReady return true if the deployment is ready.
func isEndpointReady(epSvc *ec2v1alpha1.VPCEndpoint) bool {
	return procedure.IsCrossplaneObjReady(epSvc.Status.ConditionedStatus)
}

// Name returns the name of the step.
func (f *vpcEp) Name() string {
	return vpcEpName
}

// NewVpcEp returns the procedure for creating vpc endpoint.
func NewVpcEp(plMngr privateLinkMngr, s *runtime.Scheme) *vpcEp {
	return &vpcEp{plMngr: plMngr, scheme: s}
}

var _ Procedure = &vpcEp{}

type acceptPL interface {
	acceptPrivateLink(ctx context.Context, endpointSvcID, endpointID string) error
}

type vpcEndpointServiceNameGetter struct {
	ec2 ec2iface.EC2API
}

// acceptPrivateLink accepts the private link.
func (f *vpcEndpointServiceNameGetter) acceptPrivateLink(ctx context.Context, endpointSvcID, endpointID string) error {
	_, err := f.ec2.AcceptVpcEndpointConnectionsWithContext(ctx, &ec2.AcceptVpcEndpointConnectionsInput{
		ServiceId:      aws.String(endpointSvcID),
		VpcEndpointIds: aws.StringSlice([]string{endpointID}),
	})
	return err
}

func NewEndpointServiceNameGetter(svc ec2iface.EC2API) acceptPL {
	return &vpcEndpointServiceNameGetter{ec2: svc}
}

var _ acceptPL = &vpcEndpointServiceNameGetter{}
