package eks

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

	"github.com/tidbcloud/infra-provider/regional-server/pkg/controllers"
	"sigs.k8s.io/controller-runtime/pkg/client"

	"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/tidbcloud/infra-provider/regional-server/api/v1alpha1"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/cloud/aws"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/consts"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/controllers/privatelinkservice/task"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/deploy"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/k8s"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/util"
	ec2vv1beta1 "github.com/upbound/provider-aws/apis/ec2/v1beta1"
	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 EndpointService struct {
	*task.BaseTask
	awsCli aws.Client
}

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 {
	t.BeginAt = util.Pointer(time.Now())

	ves := t.buildEndpointService()
	if serviceName := t.PrivateLinkService.Annotations[v1alpha1.AnnotationKeyPrivateLinkServiceName]; serviceName != "" {
		if err := t.prepareImportExisting(ctx, ves, serviceName); err != nil {
			return deploy.Error(err).WithRequeuAfter(time.Second * 30)
		}
	}

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

func (t *EndpointService) buildEndpointService() *ec2vv1beta1.VPCEndpointService {
	tags := t.EffectiveCloudTags(consts.ProviderTypeEKS)
	tags = util.BuildAWSTagWithName(tags, t.PrivateLinkService.Name)

	ves := &ec2vv1beta1.VPCEndpointService{
		ObjectMeta: metav1.ObjectMeta{
			Name: t.EndpointServiceName(),
			// TODO: Add owner labels
			// Labels: labels.NewOwnerLabels(t.PrivateLinkService, nil),
			Labels: t.PrivateLinkService.Labels,
		},
		Spec: ec2vv1beta1.VPCEndpointServiceSpec{
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{
					Name: t.ProviderConfigName(),
				},
				ManagementPolicies: controllers.GetResourceManagementPolicies(),
			},
			ForProvider: ec2vv1beta1.VPCEndpointServiceParameters{
				Region:             pointer.String(t.PrivateLinkService.Spec.Region),
				AcceptanceRequired: pointer.Bool(true),
				PrivateDNSName:     pointer.String(t.privateDNSName()),
				Tags:               util.ToMapPointer(tags),
			},
		},
	}

	lbArns := []*string{
		pointer.String(t.PrivateLinkService.Status.LoadBalancer.CloudResourceId),
	}
	ves.Spec.ForProvider.NetworkLoadBalancerArns = lbArns

	return ves
}

func (t *EndpointService) privateDNSName() string {
	prefix := t.PrivateLinkService.Status.DNSSubIdentify
	if prefix == "" {
		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.DNSProviderConfig.AwsRoute53.Domain)
}

func (t *EndpointService) PostCreate(ctx context.Context) deploy.Result {
	ves := &ec2vv1beta1.VPCEndpointService{}
	if err := t.Local.Get(ctx, types.NamespacedName{Name: t.EndpointServiceName()}, ves); err != nil {
		return deploy.Panic(err).WithRequeuAfter(time.Second * 30)
	}
	if !controllers.IsResourceReady(ves.Status.ResourceStatus) {
		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)
	}

	if ves.Status.AtProvider.ID == 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 deploy.Done()
	}
	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 deploy.Done()
}

// no need to set principals again for migrated resource.
func (t *EndpointService) setAllowedPrincipals(ctx context.Context, ves *ec2vv1beta1.VPCEndpointService, principals []string) error {
	cli := t.awsCli.EC2(&aws.AssumedRole{RoleARN: t.RoleARN()})
	res, err := cli.ModifyVpcEndpointServicePermissions(ctx,
		&ec2.ModifyVpcEndpointServicePermissionsInput{
			ServiceId:            ves.Status.AtProvider.ID,
			AddAllowedPrincipals: principals,
		}, func(opts *ec2.Options) {
			opts.Region = t.PrivateLinkService.Spec.Region
		})
	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, _ *ec2vv1beta1.VPCEndpointService, principals []string) error {
	t.PrivateLinkService.Status.AllowedPrincipals = principals
	return t.PrivateLinkServiceRW.UpdateStatus(ctx, t.PrivateLinkService)
}

func (t *EndpointService) setEndpointServiceStatus(ctx context.Context, ves *ec2vv1beta1.VPCEndpointService) error {
	es := v1alpha1.EndpointServiceStatus{
		Name:              t.EndpointServiceName(),
		ServiceID:         *ves.Status.AtProvider.ID,
		ServiceName:       *ves.Status.AtProvider.ServiceName,
		PrivateDNSName:    *ves.Status.AtProvider.PrivateDNSName,
		Port:              t.GetEndpointServicePort(),
		Ports:             t.GetEndpointServicePorts(),
		AvailabilityZones: t.PrivateLinkService.Status.EndpointService.AvailabilityZones,
	}
	if len(es.AvailabilityZones) != len(ves.Status.AtProvider.AvailabilityZones) {
		azs := []string{}
		for _, az := range ves.Status.AtProvider.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{}
	cli := t.awsCli.EC2(&aws.AssumedRole{RoleARN: t.RoleARN()})
	res, err := cli.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
}

// prepareImportExisting adopts an existing AWS VPC Endpoint Service identified by
// serviceName (e.g. com.amazonaws.vpce.us-east-1.vpce-svc-0abc1234). It only adds
// crossplane.io/external-name to the same CR that the default create path builds.
func (t *EndpointService) prepareImportExisting(ctx context.Context, ves *ec2vv1beta1.VPCEndpointService, serviceName string) error {
	if err := t.validateExistingService(ctx, serviceName); err != nil {
		return err
	}

	// Extract service ID from service name: last dot-separated segment.
	// com.amazonaws.vpce.us-east-1.vpce-svc-0abc1234 → vpce-svc-0abc1234
	parts := strings.Split(serviceName, ".")
	serviceID := parts[len(parts)-1]

	v1alpha1.SetExternalName(ves, serviceID)
	return nil
}

// validateExistingService calls DescribeVpcEndpointServiceConfigurations to verify that
// the named endpoint service is suitable for import.
func (t *EndpointService) validateExistingService(ctx context.Context, serviceName string) error {
	cli := t.awsCli.EC2(&aws.AssumedRole{RoleARN: t.RoleARN()})
	res, err := cli.DescribeVpcEndpointServiceConfigurations(ctx,
		&ec2.DescribeVpcEndpointServiceConfigurationsInput{
			Filters: []ec2types.Filter{
				{Name: pointer.String("service-name"), Values: []string{serviceName}},
			},
		}, func(opts *ec2.Options) {
			opts.Region = t.PrivateLinkService.Spec.Region
		})
	if err != nil {
		return fmt.Errorf("describe vpc endpoint service %q: %w", serviceName, err)
	}

	// Check 1: exactly one result.
	if len(res.ServiceConfigurations) == 0 {
		return fmt.Errorf("vpc endpoint service %q not found", serviceName)
	}
	if len(res.ServiceConfigurations) > 1 {
		return fmt.Errorf("vpc endpoint service %q: ambiguous, found %d services", serviceName, len(res.ServiceConfigurations))
	}

	svc := res.ServiceConfigurations[0]

	// Check 2: state must be Available.
	if svc.ServiceState != ec2types.ServiceStateAvailable {
		return fmt.Errorf("vpc endpoint service %q state is %q, expected Available", serviceName, svc.ServiceState)
	}

	// Check 3: service type must be Interface.
	isInterface := false
	for _, st := range svc.ServiceType {
		if st.ServiceType == ec2types.ServiceTypeInterface {
			isInterface = true
			break
		}
	}
	if !isInterface {
		return fmt.Errorf("vpc endpoint service %q is not of type Interface", serviceName)
	}

	// Check 4: AcceptanceRequired must be true.
	if svc.AcceptanceRequired == nil || !*svc.AcceptanceRequired {
		return fmt.Errorf("vpc endpoint service %q: AcceptanceRequired must be true", serviceName)
	}

	return nil
}

func (t *EndpointService) Delete(ctx context.Context) deploy.Result {
	ves := &ec2vv1beta1.VPCEndpointService{
		ObjectMeta: metav1.ObjectMeta{
			Name: t.EndpointServiceName(),
		},
	}
	if err := t.Local.Get(ctx, client.ObjectKeyFromObject(ves), ves); client.IgnoreNotFound(err) != nil {
		return deploy.Error(err)
	}

	ves.Spec.ResourceSpec.ManagementPolicies = xpv1.ManagementPolicies{xpv1.ManagementActionAll}

	if err := k8s.PatchObject(ctx, t.Local, ves, controllers.ManagementPoliciesPatch); client.IgnoreNotFound(err) != nil {
		return deploy.Error(err)
	}

	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()}, &ec2vv1beta1.VPCEndpointService{}))
}
