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/elbv2"
	"github.com/aws/aws-sdk-go/service/sts"
	ec2v1alpha1 "github.com/crossplane-contrib/provider-aws/apis/ec2/v1alpha1"
	"github.com/crossplane/crossplane-runtime/pkg/meta"
	"github.com/tidbcloud/infra-provider/pkg/namer"
	"github.com/tidbcloud/infra-provider/pkg/procedure"
	corev1 "k8s.io/api/core/v1"
	"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 (
	vpcEpSvcName = "vpc-endpoint-svc"
)

type vpcEpSvc struct {
	plClient vpcPrivateLinkClient
	scheme   *runtime.Scheme
}

// Deploy deploys resource for the specified tenant.
func (f *vpcEpSvc) Deploy(ctx context.Context, baseCli, infraCli client.Client, info ProcedureInfo) error {
	baseReader := procedure.AsReader(baseCli)
	nlbArn, err := f.plClient.getSvcNlbArn(ctx, baseReader, info)
	if err != nil {
		return err
	}
	// defensively check if the NLB is active
	if nlbArn == "" {
		return fmt.Errorf("expect vpc NLB to be active")
	}
	epSvc := getVpcEndpointSvc(info, nlbArn)
	_, err = controllerutil.CreateOrUpdate(ctx, infraCli, epSvc, func() error {
		mutateVpcEndpointSvc(epSvc, info, nlbArn)
		err := controllerutil.SetControllerReference(info.privatelink, epSvc, f.scheme)
		if procedure.IsAlreadyOwnedError(err) {
			return nil
		}
		return err
	})
	return err
}

func mutateVpcEndpointSvc(epSvc *ec2v1alpha1.VPCEndpointServiceConfiguration, info ProcedureInfo, nlbArn string) {
	epSvc.Spec.ProviderConfigReference = getProviderConfigRef(info.privatelink.Spec.ServerProviderRef)
	epSvc.Spec.ForProvider.Region = info.Region
	epSvc.Spec.ForProvider.NetworkLoadBalancerARNs = aws.StringSlice([]string{nlbArn})
	epSvc.Spec.ForProvider.AcceptanceRequired = aws.Bool(false)
}

func getVpcEndpointSvc(info ProcedureInfo, nlbArn string) *ec2v1alpha1.VPCEndpointServiceConfiguration {
	epSvc := &ec2v1alpha1.VPCEndpointServiceConfiguration{}
	epSvc.Name = namer.UniqueIdentifier(info.privatelink.ObjectMeta)
	mutateVpcEndpointSvc(epSvc, info, nlbArn)
	// TODO: set AcceptanceRequired and limit access
	return epSvc
}

// Status retrieves the status for the deploying step.
func (f *vpcEpSvc) Status(ctx context.Context, baseCli client.Reader, infraCli client.Client, info ProcedureInfo) (bool, error) {
	infraReader := procedure.AsReader(infraCli)
	epSvc := &ec2v1alpha1.VPCEndpointServiceConfiguration{}
	err := infraReader.Get(ctx, types.NamespacedName{Name: namer.UniqueIdentifier(info.privatelink.ObjectMeta)}, epSvc)
	if err != nil {
		return false, err
	}
	if !isEndpointSvcReady(epSvc) {
		return false, nil
	}
	err = f.plClient.addAllowedPrincipal(ctx, infraReader, info)
	if err != nil {
		return false, err
	}
	return true, nil
}

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

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

// NewVpcEpSvc returns the procedure for vpc deployment.
func NewVpcEpSvc(plClient vpcPrivateLinkClient, s *runtime.Scheme) *vpcEpSvc {
	if plClient == nil {
		plClient = &defaultPrivatelinkHelper{}
	}
	return &vpcEpSvc{plClient: plClient, scheme: s}
}

var _ Procedure = &vpcEpSvc{}

// vpcPrivateLinkClient is the helper for issuing aws requests to build private link.
type vpcPrivateLinkClient interface {
	getSvcNlbArn(ctx context.Context, cli client.Reader, info ProcedureInfo) (string, error)
	addAllowedPrincipal(ctx context.Context, cli client.Reader, info ProcedureInfo) error
}

type defaultPrivatelinkHelper struct {
	aws procedure.AWSService
}

// getSvcNlbArn returns the NLB arn for the LB type service.
// if the NLB is not ready, returns an empty string.
func (d *defaultPrivatelinkHelper) getSvcNlbArn(ctx context.Context, cli client.Reader, info ProcedureInfo) (string, error) {
	if info.NLBARN != "" {
		return info.NLBARN, nil
	}
	// retrieve service
	svc := &corev1.Service{}
	svcKey := info.Service
	err := cli.Get(ctx, svcKey, svc)
	if err != nil {
		return "", err
	}
	if len(svc.Status.LoadBalancer.Ingress) != 1 {
		return "", fmt.Errorf("service %v contains %d ingress, expect exactly 1 ingress", svcKey, len(svc.Status.LoadBalancer.Ingress))
	}
	if svc.Status.LoadBalancer.Ingress[0].Hostname == "" {
		return "", fmt.Errorf("empty hostname of service %v.", svcKey)
	}
	svcDNSName := svc.Status.LoadBalancer.Ingress[0].Hostname

	elbs, err := d.aws.ELBV2API(nil).DescribeLoadBalancersWithContext(ctx, &elbv2.DescribeLoadBalancersInput{})
	if err != nil {
		return "", err
	}
	for _, lb := range elbs.LoadBalancers {
		if aws.StringValue(lb.DNSName) != svcDNSName {
			continue
		}
		if lb.State == nil || aws.StringValue(lb.State.Code) != elbv2.LoadBalancerStateEnumActive {
			return "", nil
		}
		return aws.StringValue(lb.LoadBalancerArn), nil
	}
	return "", fmt.Errorf("cannot find ELB for hostname %s", svcDNSName)
}

// addAllowedPrincipal allows tenant role to allowed principal for the vpc endpoint service.
func (d *defaultPrivatelinkHelper) addAllowedPrincipal(ctx context.Context, cli client.Reader, info ProcedureInfo) error {
	epSvc := &ec2v1alpha1.VPCEndpointServiceConfiguration{}
	err := cli.Get(ctx, types.NamespacedName{Name: namer.UniqueIdentifier(info.privatelink.ObjectMeta)}, epSvc)
	if err != nil {
		return err
	}
	if epSvc.Annotations == nil || epSvc.Annotations[meta.AnnotationKeyExternalName] == "" {
		return fmt.Errorf("vpc endpoint service external name is empty")
	}

	clientRole := ""
	if info.privatelink.Spec.ClientProviderRef != nil {
		clientRole = info.clientProviderInfo.RoleARN
	} else {
		defaultRole, err := d.aws.StsAPI(nil).GetCallerIdentityWithContext(ctx, &sts.GetCallerIdentityInput{})
		if err != nil {
			return err
		}
		clientRole = aws.StringValue(defaultRole.Arn)
	}
	rootPrincipal, err := getRootPrincipal(clientRole)
	if err != nil {
		return err
	}
	_, err = d.aws.EC2API(info.getServerAssumeRole()).ModifyVpcEndpointServicePermissionsWithContext(ctx, &ec2.ModifyVpcEndpointServicePermissionsInput{
		ServiceId: aws.String(epSvc.Annotations[meta.AnnotationKeyExternalName]),
		AddAllowedPrincipals: []*string{
			aws.String(rootPrincipal),
		},
	})
	if err != nil {
		return err
	}

	return nil
}

func NewPrivatelinkHelper(aws procedure.AWSService) *defaultPrivatelinkHelper {
	return &defaultPrivatelinkHelper{
		aws: aws,
	}
}

var _ vpcPrivateLinkClient = &defaultPrivatelinkHelper{}
