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/elbv2"
	ec2v1alpha1 "github.com/crossplane/provider-aws/apis/ec2/v1alpha1"
	corev1 "k8s.io/api/core/v1"

	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
)

const (
	frpsEpSvcName = "frps-endpoint-svc"
)

type frpsEpSvc struct {
	plClient frpsPrivateLinkClient
}

// Deploy deploys resource for the specified tenant.
func (f *frpsEpSvc) Deploy(ctx context.Context, baseCli, infraCli client.Client, info ProcedureInfo) error {
	baseReader := getReader(baseCli)
	nlbArn, err := f.plClient.getFrpsNlbArn(ctx, baseReader, info)
	if err != nil {
		return err
	}
	// defensively check if the NLB is active
	if nlbArn == "" {
		return fmt.Errorf("expect frps NLB to be active")
	}
	epSvc := getFrpsEndpointSvc(info, nlbArn)
	_, err = controllerutil.CreateOrUpdate(ctx, infraCli, epSvc, func() error {
		mutateFrpsEndpointSvc(epSvc, info, nlbArn)
		return nil
	})
	return err
}

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

func getFrpsEndpointSvc(info ProcedureInfo, nlbArn string) *ec2v1alpha1.VPCEndpointServiceConfiguration {
	key := ResourceKey(info, frpsEpSvcName)
	epSvc := &ec2v1alpha1.VPCEndpointServiceConfiguration{}
	epSvc.Name = key.Name
	mutateFrpsEndpointSvc(epSvc, info, nlbArn)
	setOwnerRef(&epSvc.ObjectMeta, info)
	// TODO: set AcceptanceRequired and limit access
	return epSvc
}

// Status retrieves the status for the deploying step.
func (f *frpsEpSvc) Status(ctx context.Context, baseCli, infraCli client.Reader, info ProcedureInfo) (bool, error) {
	key := ResourceKey(info, frpsEpSvcName)
	key.Namespace = ""
	epSvc := &ec2v1alpha1.VPCEndpointServiceConfiguration{}
	err := infraCli.Get(ctx, key, epSvc)
	if err != nil {
		return false, err
	}
	return isEndpointSvcReady(epSvc), nil
}

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

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

// NewFrpsEpSvc returns the procedure for frps deployment.
func NewFrpsEpSvc(plClient frpsPrivateLinkClient) *frpsEpSvc {
	if plClient == nil {
		plClient = &defaultFrpsPrivatelinkHelper{}
	}
	return &frpsEpSvc{plClient: plClient}
}

var _ Procedure = &frpsEpSvc{}

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

type defaultFrpsPrivatelinkHelper struct {
	aws AWSService
}

// getFrpsNlbArn returns the NLB arn for the Frps service.
// if the NLB is not ready, returns an empty string.
func (d *defaultFrpsPrivatelinkHelper) getFrpsNlbArn(ctx context.Context, cli client.Reader, info ProcedureInfo) (string, error) {
	// retrieve service
	svc := &corev1.Service{}
	svcKey := ResourceKey(info, frpsSvcName)
	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 frps endpoint service.
func (d *defaultFrpsPrivatelinkHelper) addAllowedPrincipal(ctx context.Context, cli client.Reader, info ProcedureInfo) error {
	key := ResourceKey(info, frpsEpSvcName)
	key.Namespace = ""
	epSvc := &ec2v1alpha1.VPCEndpointServiceConfiguration{}
	err := cli.Get(ctx, key, epSvc)
	if err != nil {
		return err
	}
	if epSvc.Annotations == nil || epSvc.Annotations[crossplaneExternalName] == "" {
		return fmt.Errorf("vpc endpoint service external name is empty")
	}
	rootPrincipal, err := getRootPrincipal(info.assumeRole)
	if err != nil {
		return err
	}
	_, err = d.aws.EC2API(info.getAssumeRole()).ModifyVpcEndpointServicePermissionsWithContext(ctx, &ec2.ModifyVpcEndpointServicePermissionsInput{
		ServiceId: aws.String(epSvc.Annotations[crossplaneExternalName]),
		AddAllowedPrincipals: []*string{
			aws.String(rootPrincipal),
		},
	})
	if err != nil {
		return err
	}
	return nil
}

func NewFrpsPrivatelinkHelper(aws AWSService) *defaultFrpsPrivatelinkHelper {
	return &defaultFrpsPrivatelinkHelper{
		aws: aws,
	}
}

var _ frpsPrivateLinkClient = &defaultFrpsPrivatelinkHelper{}
