package privatelink

import (
	"context"
	"fmt"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/arn"
	"github.com/aws/aws-sdk-go/aws/credentials"
	"github.com/aws/aws-sdk-go/aws/credentials/stscreds"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/ec2"
	ec2v1alpha1 "github.com/crossplane-contrib/provider-aws/apis/ec2/v1alpha1"
	cpv1beta1 "github.com/crossplane-contrib/provider-aws/apis/v1beta1"
	cpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	"github.com/crossplane/crossplane-runtime/pkg/meta"
	"github.com/pingcap/errors"
	eksv1alpha1 "github.com/tidbcloud/eks-provider/api/v1alpha1"
	"github.com/tidbcloud/infra-provider/pkg/namer"
	"github.com/tidbcloud/infra-provider/pkg/procedure"

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

// GetPrivatelinkProcedureInfo returns the privatelink info.
func GetPrivatelinkProcedureInfo(ctx context.Context, cli client.Client, pl *eksv1alpha1.Privatelink, namespace string) (info ProcedureInfo, err error) {
	if pl == nil {
		return ProcedureInfo{}, fmt.Errorf("failed to get service for private link")
	}
	// user should either specify service object in base cluster, or specify a NLB ARN
	if (pl.Spec.Service.Target == nil) == (pl.Spec.Service.ARN == "") {
		return ProcedureInfo{}, fmt.Errorf("need to either specify service object in base cluster, or specify a NLB ARN")
	}
	if namespace == "" {
		namespace = procedure.ProcedureNamespace
	}
	info.privatelink = pl
	info.namespace = namespace
	if pl.Spec.Service.Target != nil {
		info.Service = types.NamespacedName{
			Name:      pl.Spec.Service.Target.Name,
			Namespace: pl.Spec.Service.Target.Namespace,
		}
	}
	info.NLBARN = pl.Spec.Service.ARN
	info.Region = pl.Spec.VpcInfo.Region
	info.VpcID = pl.Spec.VpcInfo.VpcID
	info.SubnetIDs = pl.Spec.VpcInfo.SubnetIDs
	info.SecurityGroupIDs = pl.Spec.VpcInfo.SecurityGroupIDs
	if providerInfo, err := getCrossplaneProviderRef(ctx, cli, pl.Spec.ServerProviderRef); err != nil {
		return ProcedureInfo{}, err
	} else if providerInfo.RoleARN != "" {
		info.serverProviderInfo = providerInfo
	}
	if providerInfo, err := getCrossplaneProviderRef(ctx, cli, pl.Spec.ClientProviderRef); err != nil {
		return ProcedureInfo{}, err
	} else if providerInfo.RoleARN != "" {
		info.clientProviderInfo = providerInfo
	}
	return info, nil
}

func getCrossplaneProviderRef(ctx context.Context, cli client.Reader, providerRef *string) (*procedure.AssumedRole, error) {
	if providerRef == nil {
		return nil, nil
	}
	providerRefName := aws.StringValue(providerRef)
	provider := cpv1beta1.ProviderConfig{}
	err := cli.Get(ctx, client.ObjectKey{Name: providerRefName}, &provider)
	if err != nil {
		return nil, err
	}
	// TODO(tangent): Populate the field from the provider spec once downstream has synced the change.
	var externalId string
	if provider.Annotations != nil {
		externalId = provider.Annotations["external-id"]
	}
	return &procedure.AssumedRole{
		RoleARN:    aws.StringValue(provider.Spec.AssumeRoleARN),
		ExternalID: externalId,
	}, nil
}

// getEndpointServiceID returns the endpoint service ID for the EKS info.
func getEndpointServiceID(ctx context.Context, cli client.Reader, info ProcedureInfo, epSvcName string) (string, error) {
	epSvc := &ec2v1alpha1.VPCEndpointServiceConfiguration{}
	err := cli.Get(ctx, types.NamespacedName{Name: namer.UniqueIdentifier(info.privatelink.ObjectMeta)}, epSvc)
	if err != nil {
		return "", err
	}
	// the annotation exteral name specifies the endpoint service ID with the format like  "vpce-svc-07d8722e7e7887f5c"
	return epSvc.Annotations[meta.AnnotationKeyExternalName], nil
}

// ConditionEqual checks if the condition is equal to the given status except for the lastTransitionTime.
func ConditionEqual(cond1, cond2 eksv1alpha1.Condition) bool {
	return cond1.Type == cond2.Type &&
		cond1.Status == cond2.Status &&
		cond1.Reason == cond2.Reason &&
		cond1.Message == cond2.Message
}

// privateLinkMngr provides utility functions for managing private links.
type privateLinkMngr interface {
	getEndpointServiceName(ctx context.Context, cli client.Reader, info ProcedureInfo, epSvcName string) (string, error)
	getEndpointDNS(ctx context.Context, cli client.Reader, epName string, info ProcedureInfo) (string, error)
	acceptPrivateLink(ctx context.Context, endpointSvcID, endpointID string, info ProcedureInfo) error
}

// plMngr returns the private link manager for the EKS info.
type plMngr struct {
	aws procedure.AWSService
}

func (p *plMngr) getEndpointService(ctx context.Context, cli client.Reader, info ProcedureInfo, epSvcName string) (*ec2.ServiceConfiguration, error) {
	epSvcID, err := getEndpointServiceID(ctx, cli, info, epSvcName)
	if err != nil {
		return nil, err
	}
	epSvcs, err := p.aws.EC2API(info.getServerAssumeRole()).DescribeVpcEndpointServiceConfigurationsWithContext(ctx, &ec2.DescribeVpcEndpointServiceConfigurationsInput{
		ServiceIds: []*string{aws.String(epSvcID)},
	})
	if err != nil {
		return nil, err
	}
	if len(epSvcs.ServiceConfigurations) == 0 {
		return nil, fmt.Errorf("endpoint service %q not found", epSvcID)
	}
	return epSvcs.ServiceConfigurations[0], nil
}

func (p *plMngr) getEndpointServiceName(ctx context.Context, cli client.Reader, info ProcedureInfo, epSvcName string) (string, error) {
	epSvc, err := p.getEndpointService(ctx, cli, info, epSvcName)
	if err != nil {
		return "", err
	}
	return aws.StringValue(epSvc.ServiceName), nil
}

func (p *plMngr) getEndpointDNS(ctx context.Context, cli client.Reader, epName string, info ProcedureInfo) (string, error) {
	ep := &ec2v1alpha1.VPCEndpoint{}
	epKey := types.NamespacedName{
		Name: epName,
	}
	err := cli.Get(ctx, epKey, ep)
	if err != nil {
		return "", err
	}
	// currently there's some issue in tidbcloud's fork on provider-aws that
	// the observed state for vpc endpoint is not in .status.atProvider
	// field. To metigate this, we can directly send API request to retrieve
	// the DNS entry information for now.
	// if ep.Status.AtProvider.VPCEndpoint == nil || len(ep.Status.AtProvider.VPCEndpoint.DNSEntries) == 0 {
	// 	return "", fmt.Errorf("empty DNS entries of endpoint %q", epName)
	// }

	if ep.Annotations == nil || ep.Annotations[meta.AnnotationKeyExternalName] == "" {
		return "", fmt.Errorf("empty external name of vpc endpoint %q", epName)
	}
	vpceID := ep.Annotations[meta.AnnotationKeyExternalName]

	vpces, err := p.aws.EC2API(info.getClientAssumeRole()).DescribeVpcEndpointsWithContext(ctx, &ec2.DescribeVpcEndpointsInput{
		VpcEndpointIds: []*string{aws.String(vpceID)},
	})

	if err != nil {
		return "", errors.Annotatef(err, "failed to describe vpc endpoints")
	}

	if len(vpces.VpcEndpoints) != 1 {
		return "", fmt.Errorf("expect exactly one vpc endpoint '%s' but got %d", vpceID, len(vpces.VpcEndpoints))
	}
	vpce := vpces.VpcEndpoints[0]
	if vpce.DnsEntries == nil || len(vpce.DnsEntries) == 0 {
		return "", fmt.Errorf("empty DNS entries of vpc endpoint %q", epName)
	}

	// Assuming the first DNS entry is the one for all AZs.
	// It should in the form "vpce-0430449365e0683e9-w49plspa.vpce-svc-08d4bd8696f2bf724.us-west-2.vpce.amazonaws.com".
	// If it's AZ specific DNS entry (in the form "vpce-0430449365e0683e9-w49plspa-us-west-2a.vpce-svc-08d4bd8696f2bf724.us-west-2.vpce.amazonaws.com"),
	// then it would only be resolved to ENI in only one AZ.
	// return aws.StringValue(ep.Status.AtProvider.VPCEndpoint.DNSEntries[0].DNSName), nil
	return aws.StringValue(vpce.DnsEntries[0].DnsName), nil
}

func (p *plMngr) acceptPrivateLink(ctx context.Context, endpointSvcID, endpointID string, info ProcedureInfo) error {
	_, err := p.aws.EC2API(info.getServerAssumeRole()).AcceptVpcEndpointConnectionsWithContext(ctx, &ec2.AcceptVpcEndpointConnectionsInput{
		ServiceId:      aws.String(endpointSvcID),
		VpcEndpointIds: aws.StringSlice([]string{endpointID}),
	})
	return err
}

func NewPlMngr(aws procedure.AWSService) *plMngr {
	return &plMngr{aws: aws}
}

var _ privateLinkMngr = &plMngr{}

// getRootPrincipal returns the root principal for the given tenant role ARN.
func getRootPrincipal(arnStr string) (string, error) {
	parsedArn, err := arn.Parse(arnStr)
	if err != nil {
		return "", err
	}
	parsedArn.Resource = "root"
	return parsedArn.String(), nil
}

func getProviderConfigRef(provider *string) *cpv1.Reference {
	if provider == nil {
		return nil
	}
	return &cpv1.Reference{
		Name: aws.StringValue(provider),
	}
}

func (info *ProcedureInfo) getServerAssumeRole() *procedure.AssumedRole {
	return info.serverProviderInfo
}

func (info *ProcedureInfo) getClientAssumeRole() *procedure.AssumedRole {
	return info.clientProviderInfo
}

func assumeRoleCred(sess *session.Session, role *procedure.AssumedRole) *credentials.Credentials {
	return stscreds.NewCredentials(sess, role.RoleARN, func(provider *stscreds.AssumeRoleProvider) {
		provider.ExternalID = aws.String(role.ExternalID)
	})
}
