package vpc_endpoint

import (
	"context"
	"fmt"
	"strings"

	"github.com/aws/aws-sdk-go-v2/aws"
	ec2api "github.com/aws/aws-sdk-go-v2/service/ec2"
	ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	"github.com/crossplane/provider-aws/apis/ec2/v1alpha1"
	"github.com/pingcap/errors"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"

	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-api/pkg/consts"
	awsapi "github.com/tidbcloud/infra-api/pkg/providers/aws"
)

const (
	routeTableFilterStateActive = "active"
)

var (
	routeTableFilterStateKey = "route.state"
	routeTableFilterVpcIDKey = "vpc-id"
)

func DeployS3GatewayEndpoint(
	ctx context.Context,
	client client.Client,
	ec2Client awsapi.Ec2Client,
	vpcID, k8sIdentifier string,
	labels map[string]string,
	network *cloudv1.Network,
	providerConfig string,
	currentVersion int,
) (controllerutil.OperationResult, string, error) {
	if !ShouldUpdateEndpoint(network.Annotations, currentVersion) {
		return controllerutil.OperationResultNone, "", nil
	}

	// Get the private route table under the vpc first.
	routeTableIn := &ec2api.DescribeRouteTablesInput{
		Filters: []ec2types.Filter{
			{
				Name:   &routeTableFilterStateKey,
				Values: []string{routeTableFilterStateActive},
			},
			{
				Name:   &routeTableFilterVpcIDKey,
				Values: []string{vpcID},
			},
		},
	}
	routeTableOut, err := ec2Client.DescribeRouteTables(ctx, routeTableIn, awsapi.WithEC2Region(network.Spec.Region))
	if err != nil {
		return "", "", errors.Annotate(err, "failed to get route tables")
	}

	routeTableIds := make([]*string, 0, len(routeTableOut.RouteTables))
	// Only keep route tables whose name is like "shoot--staging--18c0aebb-private-us-west-2a".
	for _, routeTable := range routeTableOut.RouteTables {
		if AwsNameTagHasPrefix(routeTable.Tags, fmt.Sprintf("%s-private", k8sIdentifier)) {
			routeTableIds = append(routeTableIds, routeTable.RouteTableId)
		}
	}

	// Then add gateway endpoint routes to the route tables.
	endpointName := buildEndpointName(k8sIdentifier)
	s3ServiceName := fmt.Sprintf("com.amazonaws.%s.s3", network.Spec.Region)

	awsTags := make([]*v1alpha1.Tag, 0, len(labels))
	for k, v := range cloudv1.TagsFromLabels(labels) {
		awsTags = append(awsTags, &v1alpha1.Tag{
			Key:   aws.String(k),
			Value: aws.String(v),
		})
	}
	awsTags = append(awsTags, &v1alpha1.Tag{
		Key:   aws.String(TagNameKey),
		Value: &endpointName,
	})

	endpoint := &v1alpha1.VPCEndpoint{}
	endpoint.Name = endpointName
	endpoint.Spec.ForProvider.Region = network.Spec.Region
	endpoint.Spec.ForProvider.ClientToken = &endpointName
	endpoint.Spec.ForProvider.ServiceName = &s3ServiceName
	endpoint.Spec.ForProvider.VPCEndpointType = aws.String(string(ec2types.VpcEndpointTypeGateway))
	endpoint.Spec.ForProvider.VPCID = &vpcID
	endpoint.Spec.ForProvider.RouteTableIDs = routeTableIds
	endpoint.Spec.ForProvider.TagSpecifications = []*v1alpha1.TagSpecification{
		{
			ResourceType: aws.String(string(ec2types.ResourceTypeVpcEndpoint)),
			Tags:         awsTags,
		},
	}
	endpoint.Spec.ProviderConfigReference = &xpv1.Reference{Name: providerConfig}
	out, err := controllerutil.CreateOrUpdate(ctx, client, endpoint, func() error { /* won't update anything */ return nil })
	if err != nil {
		return out, "", errors.Annotate(err, "failed to create vpc endpoint")
	}

	// update endpoint id to network annotation
	addS3GatewayEndpointNameToNetwork(network, endpointName)
	return out, endpointName, nil
}

func addS3GatewayEndpointNameToNetwork(n *cloudv1.Network, name string) {
	if n.Annotations == nil {
		n.Annotations = make(map[string]string)
	}
	n.Annotations[consts.S3GatewayEndpointNameAnnotationKey] = name
}

func buildEndpointName(k8sIdentifier string) string {
	return strings.Trim(fmt.Sprintf("%s-gateway-endpoint-s3", k8sIdentifier), "-")
}
