package privatelinkservice

import (
	"context"
	"strings"

	"github.com/aws/aws-sdk-go-v2/service/ec2"
	perrors "github.com/pingcap/errors"
	"sigs.k8s.io/controller-runtime/pkg/client"

	"github.com/tidbcloud/infra-provider/pkg/cloud/aws"
	"github.com/tidbcloud/infra-provider/pkg/consts"
	"github.com/tidbcloud/infra-provider/pkg/util"
	"github.com/tidbcloud/infra-provider/regional-server/api/v1alpha1"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/common/endpointservice"
	bizerr "github.com/tidbcloud/infra-provider/regional-server/pkg/common/errors"
)

func (s *PrivateLinkServiceService) validateObservedGroupCanDisableIPv6(
	ctx context.Context,
	normal *normalPrivateLinkService,
	available bool,
) error {
	if !available {
		return bizerr.New(bizerr.PrivateLinkAddressPending, "cannot disable IPv6: the normal PrivateLinkService Endpoint Service is not available")
	}
	cli, err := s.endpointServiceEC2Client(normal)
	if err != nil {
		return bizerr.New(bizerr.AWSClientError, "failed to create Endpoint Service client: "+err.Error())
	}
	return s.validateGroupIPv4DowngradeConnections(ctx, cli, normal.pls)
}

func (s *PrivateLinkServiceService) validateGroupIPv4DowngradeConnections(
	ctx context.Context,
	cli *ec2.Client,
	pls *v1alpha1.PrivateLinkService,
) error {
	blockers, pending, unknown, err := endpointservice.ClassifyIPv4DowngradeConnections(ctx, cli, pls.Status.EndpointService.ServiceID)
	if err != nil {
		return bizerr.New(bizerr.AWSClientError, "failed to observe VPC endpoint connections: "+err.Error())
	}
	if len(blockers) > 0 {
		return bizerr.New(
			bizerr.PrivateLinkAddressBlocked,
			"cannot disable IPv6 for the TiDB Group: non-deleted IPv6-capable connections: "+strings.Join(blockers, ", "),
		)
	}
	if len(unknown) > 0 {
		return bizerr.New(
			bizerr.AWSClientError,
			"cannot classify VPC endpoint connection address types or states: "+strings.Join(unknown, ", "),
		)
	}
	if len(pending) > 0 {
		return bizerr.New(
			bizerr.PrivateLinkAddressPending,
			"waiting for deleting IPv6-capable VPC endpoint connections: "+strings.Join(pending, ", "),
		)
	}
	return nil
}

type normalPrivateLinkService struct {
	pls           *v1alpha1.PrivateLinkService
	vpc           *v1alpha1.VPC
	privateNLBARN string
}

// normalGroupPrivateLinkService resolves the Group's native or imported legacy
// PLS using the VPC and NLB identity already resolved for this request. The CR
// name is not part of the identity.
func (s *PrivateLinkServiceService) normalGroupPrivateLinkService(
	ctx context.Context,
	clusterID string,
	groupID string,
	vpcObj *v1alpha1.VPC,
	privateNLBARN string,
) (*normalPrivateLinkService, error) {
	if s.cli == nil {
		return nil, bizerr.New(bizerr.InternalServerError, "Kubernetes client is not configured")
	}
	if vpcObj == nil {
		return nil, bizerr.New(bizerr.InternalServerError, "TiDB Group VPC was not resolved")
	}

	plsList := &v1alpha1.PrivateLinkServiceList{}
	if err := s.cli.List(
		ctx,
		plsList,
		client.InNamespace(vpcObj.Namespace),
		client.MatchingLabels{v1alpha1.ClusterIDKey: clusterID},
	); err != nil {
		return nil, bizerr.New(bizerr.K8SClientError, "failed to list PrivateLinkServices for TiDB Group")
	}

	candidates := make([]*v1alpha1.PrivateLinkService, 0, 1)
	for i := range plsList.Items {
		pls := &plsList.Items[i]
		if privateLinkServiceBindsGroup(pls, clusterID, groupID) {
			candidates = append(candidates, pls)
		}
	}
	if len(candidates) == 0 {
		return nil, bizerr.New(bizerr.PrivateLinkAddressPending, "the normal PrivateLinkService for the TiDB Group is not ready")
	}
	if len(candidates) > 1 {
		return nil, bizerr.NewErrorf(
			bizerr.PreconditionFailed,
			"expected exactly one normal PrivateLinkService for the TiDB Group, got %d",
			len(candidates),
		)
	}

	pls := candidates[0]
	if len(pls.Spec.LoadBalancers) != 1 {
		return nil, bizerr.NewErrorf(
			bizerr.PreconditionFailed,
			"normal PrivateLinkService %s/%s must bind exactly one TiDB Group load balancer, got %d",
			pls.Namespace,
			pls.Name,
			len(pls.Spec.LoadBalancers),
		)
	}
	paused := pls.Annotations[v1alpha1.AnnotationKeyReconciliationPaused] == "true"
	if !pls.DeletionTimestamp.IsZero() {
		return nil, bizerr.New(
			bizerr.PrivateLinkAddressPending,
			"the normal PrivateLinkService for the TiDB Group is not ready",
		)
	}
	if pls.Spec.Provider != consts.ProviderTypeEKS || paused {
		return nil, bizerr.New(
			bizerr.PreconditionFailed,
			"the normal PrivateLinkService for the TiDB Group is not ready",
		)
	}
	vpcRef := v1alpha1.Reference{Name: vpcObj.Name, Namespace: vpcObj.Namespace}
	if pls.Spec.VPCRef != vpcRef {
		return nil, bizerr.NewErrorf(
			bizerr.PreconditionFailed,
			"normal PrivateLinkService %s/%s references VPC %s/%s, not the TiDB Group APPRoute VPC %s/%s",
			pls.Namespace,
			pls.Name,
			pls.Spec.VPCRef.Namespace,
			pls.Spec.VPCRef.Name,
			vpcRef.Namespace,
			vpcRef.Name,
		)
	}
	if vpcObj.Spec.Region == "" {
		return nil, bizerr.New(
			bizerr.PreconditionFailed,
			"the TiDB Group APPRoute VPC region is not configured",
		)
	}
	if pls.Spec.Region != vpcObj.Spec.Region {
		return nil, bizerr.NewErrorf(
			bizerr.PreconditionFailed,
			"normal PrivateLinkService %s/%s uses region %s, not the TiDB Group VPC region %s",
			pls.Namespace,
			pls.Name,
			pls.Spec.Region,
			vpcObj.Spec.Region,
		)
	}
	if privateNLBARN == "" {
		return nil, bizerr.New(
			bizerr.PrivateLinkAddressPending,
			"the private NLB for the TiDB Group is not ready",
		)
	}
	if pls.Status.EndpointService.ServiceID == "" {
		return nil, bizerr.New(
			bizerr.PrivateLinkAddressPending,
			"the normal PrivateLinkService Endpoint Service is not ready",
		)
	}
	return &normalPrivateLinkService{
		pls:           pls.DeepCopy(),
		vpc:           vpcObj.DeepCopy(),
		privateNLBARN: privateNLBARN,
	}, nil
}

func privateLinkServiceBindsGroup(pls *v1alpha1.PrivateLinkService, clusterID, groupID string) bool {
	for _, binding := range pls.Spec.LoadBalancers {
		if binding.ClusterID == clusterID && binding.ID == groupID {
			return true
		}
	}
	return false
}

func (s *PrivateLinkServiceService) observeEndpointServiceAddressType(
	ctx context.Context,
	normal *normalPrivateLinkService,
) (v1alpha1.IPAddressType, bool, error) {
	cli, err := s.endpointServiceEC2Client(normal)
	if err != nil {
		return "", false, err
	}
	return observeEndpointServiceAddressTypeWithClient(ctx, cli, normal)
}

func observeEndpointServiceAddressTypeWithClient(
	ctx context.Context,
	cli endpointservice.DescribeClient,
	normal *normalPrivateLinkService,
) (v1alpha1.IPAddressType, bool, error) {
	pls := normal.pls
	observation, err := endpointservice.Observe(ctx, cli, pls.Status.EndpointService.ServiceID)
	if err != nil {
		return "", false, err
	}
	// This controller does not create legacy Endpoint Services, so validate
	// their NLB identity before using the observation.
	if pls.Labels[v1alpha1.IsLegacyKey] == "true" && !observation.UsesNetworkLoadBalancer(normal.privateNLBARN) {
		return "", false, bizerr.NewErrorf(
			bizerr.PreconditionFailed,
			"Endpoint Service %s must be associated only with the TiDB Group private NLB %s, got %s",
			pls.Status.EndpointService.ServiceID,
			normal.privateNLBARN,
			strings.Join(observation.NetworkLoadBalancerARNs, ", "),
		)
	}
	return observation.IPAddressType, observation.Available, nil
}

func (s *PrivateLinkServiceService) endpointServiceEC2Client(
	normal *normalPrivateLinkService,
) (*ec2.Client, error) {
	if s.awsCli == nil {
		return nil, perrors.New("AWS client is not initialized")
	}
	if normal == nil || normal.pls == nil || normal.vpc == nil {
		return nil, perrors.New("PrivateLink Service context is not initialized")
	}
	pls := normal.pls
	if pls.Status.EndpointService.ServiceID == "" {
		return nil, perrors.New("Endpoint Service ID is empty")
	}
	if normal.vpc.Spec.AccountRef == nil {
		return nil, perrors.New("VPC account reference is not configured")
	}
	return s.awsCli.EC2(
		&aws.AssumedRole{RoleARN: util.GetRoleARN(normal.vpc.Spec.AccountRef.AccountID, normal.vpc.Spec.AccountRef.Role)},
		aws.WithRegion(normal.vpc.Spec.Region),
	), nil
}
