package procedure

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/provider-aws/apis/ec2/v1alpha1"
	"github.com/tidbcloud/eks-provider/pkg/consts"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
)

const (
	frpsEpName         = "frps-endpoint"
	frpsEpResourceType = "vpc-endpoint"

	crossplaneExternalName = "crossplane.io/external-name"
)

type frpsEp struct {
	plMngr privateLinkMngr
}

// Deploy deploys resource for the specified tenant.
func (f *frpsEp) Deploy(ctx context.Context, _, infraCli client.Client, info ProcedureInfo) error {
	vpcID := info.VpcID
	epSvcKey := ResourceKey(info, frpsEpSvcName)
	svcName, err := f.plMngr.getEndpointServiceName(ctx, infraCli, info, epSvcKey.Name)
	if err != nil {
		return err
	}
	ep := getFrpsEndpoint(info, &vpcID, &svcName)
	_, err = controllerutil.CreateOrUpdate(ctx, infraCli, ep, func() error {
		mutateFrpsEndpointSpec(ep, info, &svcName)
		return nil
	})
	return err
}

func mutateFrpsEndpointSpec(ep *ec2v1alpha1.VPCEndpoint, info ProcedureInfo, svcName *string) {
	ep.Spec.ProviderConfigReference = GetCrossplaneProvider(info)
	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) != frpsEpResourceType {
		ep.Spec.ForProvider.TagSpecifications = []*ec2v1alpha1.TagSpecification{
			&ec2v1alpha1.TagSpecification{
				ResourceType: aws.String(frpsEpResourceType),
				Tags: []*ec2v1alpha1.Tag{
					&ec2v1alpha1.Tag{
						Key:   aws.String(consts.CrossplaneVendorKey),
						Value: aws.String(consts.CrossplaneVendorValue),
					},
				},
			},
		}
		return
	}
	for _, tag := range ep.Spec.ForProvider.TagSpecifications[0].Tags {
		if tag == nil || aws.StringValue(tag.Key) != consts.CrossplaneVendorKey {
			continue
		}
		tag.Value = aws.String(consts.CrossplaneVendorValue)
		return
	}
	ep.Spec.ForProvider.TagSpecifications[0].Tags = append(ep.Spec.ForProvider.TagSpecifications[0].Tags, &ec2v1alpha1.Tag{
		Key:   aws.String(consts.CrossplaneVendorKey),
		Value: aws.String(consts.CrossplaneVendorValue),
	})
}

func getFrpsEndpoint(info ProcedureInfo, vpcID, svcName *string) *ec2v1alpha1.VPCEndpoint {
	key := ResourceKey(info, frpsEpName)
	ep := &ec2v1alpha1.VPCEndpoint{}
	ep.Name = key.Name
	mutateFrpsEndpointSpec(ep, info, svcName)
	setOwnerRef(&ep.ObjectMeta, info)
	return ep
}

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

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

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

// NewFrpsEp returns the procedure for frps deployment.
func NewFrpsEp(plMngr privateLinkMngr) *frpsEp {
	return &frpsEp{plMngr: plMngr}
}

var _ Procedure = &frpsEp{}

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

type frpsGetEndpointServiceName struct {
	ec2 ec2iface.EC2API
}

// acceptPrivateLink accepts the private link.
func (f *frpsGetEndpointServiceName) 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 &frpsGetEndpointServiceName{ec2: svc}
}

var _ acceptPL = &frpsGetEndpointServiceName{}
