package eks

import (
	"context"
	"fmt"
	"reflect"
	"time"

	"github.com/aws/aws-sdk-go-v2/service/ec2"
	ec2v1alpha1 "github.com/crossplane-contrib/provider-aws/apis/ec2/v1alpha1"
	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-provider/pkg/cloud/aws"
	"github.com/tidbcloud/infra-provider/pkg/deploy"
	"github.com/tidbcloud/infra-provider/pkg/k8s"
	"github.com/tidbcloud/infra-provider/pkg/util"
	"github.com/tidbcloud/infra-provider/regional-server/api/v1alpha1"
	commonendpointservice "github.com/tidbcloud/infra-provider/regional-server/pkg/common/endpointservice"
	regionalutils "github.com/tidbcloud/infra-provider/regional-server/pkg/common/utils"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/controllers/privatelinkservice/task"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/utils/pointer"
)

const (
	// This annotations mark whether allowed principals are set
	// annoKeyPrivateLinkAllowedPrincipals = "private-link.aws.cloud.pingcap.com/allowed-principals"
	privateLinkAllowedPrincipalsAll = "*"
)

type endpointServiceEC2Client interface {
	commonendpointservice.DescribeClient
	commonendpointservice.MutationClient
	commonendpointservice.ConnectionDescribeClient
	DescribeAvailabilityZones(context.Context, *ec2.DescribeAvailabilityZonesInput, ...func(*ec2.Options)) (*ec2.DescribeAvailabilityZonesOutput, error)
	ModifyVpcEndpointServicePermissions(context.Context, *ec2.ModifyVpcEndpointServicePermissionsInput, ...func(*ec2.Options)) (*ec2.ModifyVpcEndpointServicePermissionsOutput, error)
}

type EndpointService struct {
	*task.BaseTask
	awsCli aws.Client
	ec2Cli endpointServiceEC2Client
}

func NewEndpointService(c *task.Context, awsCli aws.Client) *EndpointService {
	return &EndpointService{BaseTask: &task.BaseTask{Context: c}, awsCli: awsCli}
}

func (t *EndpointService) Name() string {
	return "EndpointService"
}

func (t *EndpointService) Create(ctx context.Context) deploy.Result {
	if t.PrivateLinkService.Labels[v1alpha1.IsLegacyKey] == "true" {
		return deploy.Done()
	}
	tags := cloudv1.TagsFromLabels(t.PrivateLinkService.Labels)
	tags = util.AddResourceName(tags, t.PrivateLinkService.Name)

	ves := &ec2v1alpha1.VPCEndpointServiceConfiguration{
		ObjectMeta: metav1.ObjectMeta{
			Name: t.endpointServiceName(),
			// TODO: Add owner labels
			// Labels: labels.NewOwnerLabels(t.PrivateLinkService, nil),
		},
		Spec: ec2v1alpha1.VPCEndpointServiceConfigurationSpec{
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{
					Name: t.ProviderConfigName(),
				},
			},
			ForProvider: ec2v1alpha1.VPCEndpointServiceConfigurationParameters{
				Region:             t.PrivateLinkService.Spec.Region,
				AcceptanceRequired: pointer.Bool(true),
				PrivateDNSName:     pointer.String(t.privateDNSName()),
				TagSpecifications: []*ec2v1alpha1.TagSpecification{
					{
						ResourceType: pointer.String("vpc-endpoint-service"),
						Tags:         util.GetEC2V1AlphaTags(tags),
					},
				},
			},
		},
	}

	lbArns := []*string{}
	for _, lb := range t.PrivateLinkService.Status.LoadBalancers {
		lbArns = append(lbArns, pointer.String(lb.CloudResourceId))
	}
	ves.Spec.ForProvider.CustomVPCEndpointServiceConfigurationParameters.NetworkLoadBalancerARNs = lbArns

	if err := k8s.Apply(ctx, t.Local, ves); err != nil {
		return deploy.Panic(err).WithRequeuAfter(time.Second * 30)
	}
	return deploy.Done()
}

func (t *EndpointService) reconcileManagedAddressType(ctx context.Context, ves *ec2v1alpha1.VPCEndpointServiceConfiguration) deploy.Result {
	target := t.PrivateLinkService.Spec.TargetIPAddressType
	if target == "" {
		return deploy.Done()
	}

	cli := t.endpointServiceClient()
	serviceID := pointer.StringDeref(ves.Status.AtProvider.ServiceConfiguration.ServiceID, "")
	observation, err := commonendpointservice.Observe(ctx, cli, serviceID)
	if err != nil {
		return deploy.Wait(time.Second*30, "%s", err.Error())
	}
	if err := t.validateLegacyEndpointServiceNLBs(observation); err != nil {
		return deploy.Wait(time.Second*30, "%s", err.Error())
	}
	if !observation.Available {
		return deploy.Wait(time.Second*30, "waiting for the AWS Endpoint Service to become available")
	}
	observed := observation.IPAddressType

	if observed == target {
		return deploy.Done()
	}
	if observed == v1alpha1.IPAddressTypeDualstack && target == v1alpha1.IPAddressTypeIPv4 {
		blockers, pending, unknown, err := commonendpointservice.ClassifyIPv4DowngradeConnections(ctx, cli, serviceID)
		if err != nil {
			return deploy.Wait(time.Second*30, "failed to observe VPC endpoint connections: %s", err.Error())
		}
		if len(blockers) > 0 {
			return deploy.Wait(time.Second*30, "IPv6-capable VPC endpoint connections must be deleted before disabling IPv6: %v", blockers)
		}
		if len(pending) > 0 {
			return deploy.Wait(time.Second*30, "waiting for IPv6-capable VPC endpoint connections to be deleted: %v", pending)
		}
		if len(unknown) > 0 {
			return deploy.Wait(time.Second*30, "waiting to observe VPC endpoint connection address types or states: %v", unknown)
		}
	}
	err = commonendpointservice.ModifyIPAddressType(ctx, cli, serviceID, observed, target)
	if err != nil {
		return deploy.Wait(time.Second*30, "%s", err.Error())
	}
	return deploy.Wait(time.Second*30, "waiting to observe Endpoint Service address type %s", target)
}

func (t *EndpointService) validateLegacyEndpointServiceNLBs(
	observation commonendpointservice.Observation,
) error {
	if t.PrivateLinkService.Labels[v1alpha1.IsLegacyKey] != "true" {
		return nil
	}
	expected := ""
	if len(t.PrivateLinkService.Status.LoadBalancers) == 1 {
		expected = t.PrivateLinkService.Status.LoadBalancers[0].CloudResourceId
	}
	actual := observation.NetworkLoadBalancerARNs
	if !observation.UsesNetworkLoadBalancer(expected) {
		return fmt.Errorf(
			"legacy Endpoint Service NLBs %v do not match current PrivateLinkService NLB %s",
			actual,
			expected,
		)
	}
	return nil
}

func (t *EndpointService) endpointServiceClient() endpointServiceEC2Client {
	if t.ec2Cli != nil {
		return t.ec2Cli
	}
	return t.awsCli.EC2(
		&aws.AssumedRole{RoleARN: t.RoleARN()},
		aws.WithRegion(t.PrivateLinkService.Spec.Region),
	)
}

func (t *EndpointService) privateDNSName() string {
	prefix := t.PrivateLinkService.Name
	if len(prefix) > 8 {
		prefix = prefix[:8]
	}
	return fmt.Sprintf("privatelink-%s.%s.%s", prefix, t.PrivateLinkService.Status.DNSIdentify, t.Config.PublicHostedZoneDomain)
}

func (t *EndpointService) endpointServiceName() string {
	return regionalutils.GetEndpointServiceName(t.PrivateLinkService)
}

func (t *EndpointService) PostCreate(ctx context.Context) deploy.Result {
	ves := &ec2v1alpha1.VPCEndpointServiceConfiguration{}
	if err := t.Local.Get(ctx, types.NamespacedName{Name: t.endpointServiceName()}, ves); err != nil {
		return deploy.Panic(err).WithRequeuAfter(time.Second * 30)
	}
	if ves.Status.GetCondition(xpv1.TypeReady).Status != corev1.ConditionTrue {
		t.Logger.Info("EndpointService is not ready", "status", ves.Status.GetCondition(xpv1.TypeReady).Status)
		return deploy.Panic(fmt.Errorf("EndpointService is not ready")).WithRequeuAfter(time.Second * 30)
	}
	sc := ves.Status.AtProvider.ServiceConfiguration
	if sc == nil || sc.ServiceID == nil {
		t.Logger.Error(fmt.Errorf("EndpointService is not ready"), "service configuration is nil")
		return deploy.Error(fmt.Errorf("service configuration is nil")).WithRequeuAfter(time.Second * 30)
	}
	if err := t.setEndpointServiceStatus(ctx, ves); err != nil {
		return deploy.Panic(err).WithRequeuAfter(time.Second * 30)
	}

	principals := []string{privateLinkAllowedPrincipalsAll}
	// If allowed principals are already set, skip
	if reflect.DeepEqual(t.PrivateLinkService.Status.AllowedPrincipals, principals) {
		t.Logger.Info("allowed principals are already set", "principals", principals)
		return t.reconcileManagedAddressType(ctx, ves)
	}
	if err := t.setAllowedPrincipals(ctx, ves, principals); err != nil {
		return deploy.Panic(err).WithRequeuAfter(time.Second * 30)
	}
	if err := t.setAllowedPrincipalsStatus(ctx, ves, principals); err != nil {
		return deploy.Panic(err).WithRequeuAfter(time.Second * 30)
	}

	return t.reconcileManagedAddressType(ctx, ves)
}

// no need to set principals again for migrated resource.
func (t *EndpointService) setAllowedPrincipals(ctx context.Context, ves *ec2v1alpha1.VPCEndpointServiceConfiguration, principals []string) error {
	if t.PrivateLinkService.Labels[v1alpha1.IsLegacyKey] == "true" {
		return nil
	}
	res, err := t.endpointServiceClient().ModifyVpcEndpointServicePermissions(ctx,
		&ec2.ModifyVpcEndpointServicePermissionsInput{
			ServiceId:            ves.Status.AtProvider.ServiceConfiguration.ServiceID,
			AddAllowedPrincipals: principals,
		})
	if err != nil {
		return err
	}
	if res.ReturnValue == nil || !*res.ReturnValue {
		return fmt.Errorf("modify vpc endpoint service permission to '*' failed")
	}
	return nil
}

func (t *EndpointService) setAllowedPrincipalsStatus(ctx context.Context, _ *ec2v1alpha1.VPCEndpointServiceConfiguration, principals []string) error {
	t.PrivateLinkService.Status.AllowedPrincipals = principals
	return t.PrivateLinkServiceRW.UpdateStatus(ctx, t.PrivateLinkService)
}

func (t *EndpointService) setEndpointServiceStatus(ctx context.Context, ves *ec2v1alpha1.VPCEndpointServiceConfiguration) error {
	es := v1alpha1.EndpointServiceStatus{
		Name:              t.endpointServiceName(),
		ServiceID:         *ves.Status.AtProvider.ServiceConfiguration.ServiceID,
		ServiceName:       *ves.Status.AtProvider.ServiceConfiguration.ServiceName,
		PrivateDNSName:    *ves.Status.AtProvider.ServiceConfiguration.PrivateDNSName,
		Port:              t.getEndpointServicePort(),
		AvailabilityZones: t.PrivateLinkService.Status.EndpointService.AvailabilityZones,
	}
	if len(es.AvailabilityZones) != len(ves.Status.AtProvider.ServiceConfiguration.AvailabilityZones) {
		azs := make([]string, 0, len(ves.Status.AtProvider.ServiceConfiguration.AvailabilityZones))
		for _, az := range ves.Status.AtProvider.ServiceConfiguration.AvailabilityZones {
			azs = append(azs, *az)
		}
		azIDs, err := t.getAzoneIDs(ctx, azs)
		if err != nil {
			return err
		}
		es.AvailabilityZones = azIDs
	}
	t.PrivateLinkService.Status.EndpointService = es
	return t.PrivateLinkServiceRW.UpdateStatus(ctx, t.PrivateLinkService)
}

func (t *EndpointService) getAzoneIDs(ctx context.Context, azs []string) ([]string, error) {
	ids := []string{}
	res, err := t.endpointServiceClient().DescribeAvailabilityZones(ctx, &ec2.DescribeAvailabilityZonesInput{
		ZoneNames: azs,
	})
	if err != nil {
		return ids, fmt.Errorf("cannot describe zones: %w", err)
	}

	for _, az := range res.AvailabilityZones {
		ids = append(ids, *az.ZoneId)
	}
	return ids, nil
}

func (t *EndpointService) getEndpointServicePort() int32 {
	if len(t.PrivateLinkService.Status.LoadBalancers) != 1 {
		return v1alpha1.DefaultPrivateSQLEndpointPort
	}
	if t.PrivateLinkService.Status.LoadBalancers[0].Port == 0 {
		return v1alpha1.DefaultPrivateSQLEndpointPort
	}
	return t.PrivateLinkService.Status.LoadBalancers[0].Port
}

func (t *EndpointService) Delete(ctx context.Context) deploy.Result {
	ves := &ec2v1alpha1.VPCEndpointServiceConfiguration{
		ObjectMeta: metav1.ObjectMeta{
			Name: t.endpointServiceName(),
		},
	}
	if err := k8s.DeleteObject(ctx, t.Local, ves); err != nil {
		return deploy.Error(err)
	}
	return deploy.Done()
}

func (t *EndpointService) PostDelete(ctx context.Context) deploy.Result {
	return deploy.ErrorOrDone(k8s.GetDeletedObject(ctx, t.Local, types.NamespacedName{Name: t.endpointServiceName()}, &ec2v1alpha1.VPCEndpointServiceConfiguration{}))
}
