package eks

import (
	"context"
	"fmt"
	"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"
	"github.com/samber/lo"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	clusterctlpb "github.com/tidbcloud/infra-provider/api/tidbcloud/cluster-server/v1"
	"github.com/tidbcloud/infra-provider/pkg/cloud/aws"
	"github.com/tidbcloud/infra-provider/pkg/deploy"
	"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/utils"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/controllers/privatelinkservicemigration/task"
	corev1 "k8s.io/api/core/v1"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

const (
	defaultRequeueInterval = 5 * time.Second
)

type CutoverPls struct {
	*task.BaseTask
	awsCli aws.Client
}

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

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

func (t *CutoverPls) Create(ctx context.Context) deploy.Result {
	// Fetch APPRoute
	approute, err := t.getAPPRoute(ctx)
	if err != nil {
		return t.requeueOnError(err)
	}

	// Get load balancer status
	lb := v1alpha1.LoadBalancer{
		ID:        t.PrivateLinkServiceMigration.Spec.TargetTidbGroupID,
		ClusterID: t.PrivateLinkServiceMigration.Spec.TargetClusterID,
	}
	lbStatus, err := t.getLoadBalancer(ctx, lb)
	if err != nil {
		return t.requeueOnError(err)
	}

	// Validate load balancer is ready
	if err := t.validateLoadBalancerReady(lb, lbStatus); err != nil {
		return t.requeueOnError(err)
	}

	// Update private link service with new load balancer and labels
	if err := t.updatePrivateLinkService(ctx, lb, lbStatus, approute); err != nil {
		return t.requeueOnError(err)
	}

	// Update VPC endpoint service configuration
	if err := t.updateVPCEndpointService(ctx, lbStatus, approute); err != nil {
		return t.requeueOnError(err)
	}

	// Update VPC endpoint service tags
	if err := t.updateVPCEndpointServiceTags(ctx); err != nil {
		return t.requeueOnError(err)
	}

	return deploy.Done()
}

// requeueOnError returns a panic result with requeue after default interval
func (t *CutoverPls) requeueOnError(err error) deploy.Result {
	return deploy.Panic(err).WithRequeuAfter(defaultRequeueInterval)
}

// getAPPRoute fetches the APPRoute for the target cluster
func (t *CutoverPls) getAPPRoute(ctx context.Context) (*v1alpha1.APPRoute, error) {
	approute := &v1alpha1.APPRoute{}
	key := client.ObjectKey{
		Namespace: t.PrivateLinkServiceMigration.Spec.PrivateLinkServiceNamespace,
		Name:      t.PrivateLinkServiceMigration.Spec.TargetClusterID,
	}
	if err := t.APPRouteR.Get(ctx, key, approute); err != nil {
		return nil, fmt.Errorf("failed to get APPRoute: %w", err)
	}
	return approute, nil
}

// validateLoadBalancerReady checks if the load balancer has a CloudResourceId
func (t *CutoverPls) validateLoadBalancerReady(lb v1alpha1.LoadBalancer, lbStatus v1alpha1.LoadBalancerStatus) error {
	if lbStatus.CloudResourceId == "" {
		t.Logger.Info("load balancer CloudResourceId is empty, waiting for it to be ready",
			"clusterId", lb.ClusterID, "loadbalancerId", lb.ID)
		return fmt.Errorf("load balancer CloudResourceId is empty, clusterId: %s, loadbalancerId: %s", lb.ClusterID, lb.ID)
	}
	return nil
}

// updatePrivateLinkService updates the private link service spec, labels, and status
func (t *CutoverPls) updatePrivateLinkService(ctx context.Context, lb v1alpha1.LoadBalancer, lbStatus v1alpha1.LoadBalancerStatus, approute *v1alpha1.APPRoute) error {
	// Update load balancer spec
	t.PrivateLinkService.Spec.LoadBalancers = []v1alpha1.LoadBalancer{lb}

	// Update labels
	if err := t.updatePrivateLinkServiceLabels(ctx, lb, approute); err != nil {
		return err
	}

	// Update spec
	if err := t.PrivateLinkServiceRW.Update(ctx, t.PrivateLinkService); err != nil {
		return fmt.Errorf("failed to update private link service: %w", err)
	}

	// Update status
	t.PrivateLinkService.Status.LoadBalancers = []v1alpha1.LoadBalancerStatus{lbStatus}
	if err := t.PrivateLinkServiceRW.UpdateStatus(ctx, t.PrivateLinkService); err != nil {
		return fmt.Errorf("failed to update private link service status: %w", err)
	}

	return nil
}

// updatePrivateLinkServiceLabels updates the labels on the private link service
func (t *CutoverPls) updatePrivateLinkServiceLabels(ctx context.Context, lb v1alpha1.LoadBalancer, approute *v1alpha1.APPRoute) error {
	// Ensure labels map exists
	if t.PrivateLinkService.Labels == nil {
		t.PrivateLinkService.Labels = make(map[string]string)
	}

	// Set load balancer and cluster labels
	t.PrivateLinkService.Labels[v1alpha1.LoadBalancerIDKey] = lb.ID
	t.PrivateLinkService.Labels[v1alpha1.ClusterIDKey] = lb.ClusterID
	t.PrivateLinkService.Labels[v1alpha1.TagClusterKey] = t.PrivateLinkServiceMigration.Spec.TargetClusterID

	// Set project labels from APPRoute
	approuteLabels := approute.GetLabels()
	if projectID := approuteLabels[v1alpha1.TagProjectKey]; projectID != "" {
		t.PrivateLinkService.Labels[v1alpha1.ProjectIDKey] = projectID
		t.PrivateLinkService.Labels[v1alpha1.TagProjectKey] = projectID
	}

	// Set VPC labels
	return t.updateVPCLabels(ctx, approute)
}

// updateVPCLabels updates VPC-related labels on the private link service
func (t *CutoverPls) updateVPCLabels(ctx context.Context, approute *v1alpha1.APPRoute) error {
	vpcName := approute.GetLabels()[v1alpha1.VPCNameKey]
	if vpcName == "" {
		t.PrivateLinkService.Labels[v1alpha1.VPCNameKey] = ""
		t.PrivateLinkService.Labels[v1alpha1.VPCIDKey] = ""
		return nil
	}

	// Fetch target VPC
	targetVpc := &v1alpha1.VPC{}
	key := client.ObjectKey{
		Namespace: approute.Namespace,
		Name:      vpcName,
	}
	if err := t.Local.Get(ctx, key, targetVpc); err != nil {
		return fmt.Errorf("failed to get target VPC: %w", err)
	}

	t.PrivateLinkService.Labels[v1alpha1.VPCNameKey] = targetVpc.Name
	t.PrivateLinkService.Labels[v1alpha1.VPCIDKey] = targetVpc.Status.VPC.VPCID
	return nil
}

// updateVPCEndpointService updates the VPC endpoint service configuration
func (t *CutoverPls) updateVPCEndpointService(ctx context.Context, lbStatus v1alpha1.LoadBalancerStatus, approute *v1alpha1.APPRoute) error {
	ves := &ec2v1alpha1.VPCEndpointServiceConfiguration{}
	key := client.ObjectKey{Name: utils.GetEndpointServiceName(t.PrivateLinkService)}
	if err := t.Local.Get(ctx, key, ves); err != nil {
		return fmt.Errorf("failed to get VPCEndpointServiceConfiguration: %w", err)
	}

	// Prepare tags
	tags := cloudv1.TagsFromLabels(t.PrivateLinkService.Labels)
	tags = util.AddResourceName(tags, t.PrivateLinkService.Name)

	// Update VPC endpoint service spec
	ves.Spec.ForProvider.NetworkLoadBalancerARNs = []*string{lo.ToPtr(lbStatus.CloudResourceId)}
	ves.Spec.ForProvider.TagSpecifications = []*ec2v1alpha1.TagSpecification{
		{
			ResourceType: lo.ToPtr("vpc-endpoint-service"),
			Tags:         util.GetEC2V1AlphaTags(tags),
		},
	}

	// Update labels
	if ves.Labels == nil {
		ves.Labels = make(map[string]string)
	}
	ves.Labels[v1alpha1.VPCNameKey] = approute.GetLabels()[v1alpha1.VPCNameKey]

	if err := t.Local.Update(ctx, ves); err != nil {
		return fmt.Errorf("failed to update VPCEndpointServiceConfiguration: %w", err)
	}

	return nil
}

// updateVPCEndpointServiceTags updates tags on the VPC endpoint service in AWS
func (t *CutoverPls) updateVPCEndpointServiceTags(ctx context.Context) error {
	// Validate ServiceID exists
	if t.PrivateLinkService.Status.EndpointService.ServiceID == "" {
		t.Logger.Info("endpoint service ServiceID is empty, waiting for it to be ready")
		return fmt.Errorf("endpoint service ServiceID is empty")
	}

	// Prepare tags
	tags := cloudv1.TagsFromLabels(t.PrivateLinkService.Labels)
	tags = util.AddResourceName(tags, t.PrivateLinkService.Name)

	// Create tags in AWS
	cli := t.awsCli.EC2(&aws.AssumedRole{RoleARN: t.RoleARN()})
	_, err := cli.CreateTags(ctx, &ec2.CreateTagsInput{
		Resources: []string{t.PrivateLinkService.Status.EndpointService.ServiceID},
		Tags:      util.GetAWSEC2Tags(tags),
	}, func(opts *ec2.Options) {
		opts.Region = t.PrivateLinkService.Spec.Region
	})
	if err != nil {
		return fmt.Errorf("failed to create tags on VPC endpoint service: %w", err)
	}

	return nil
}

// make sure vpc endpoint service configuration is ready
func (t *CutoverPls) PostCreate(ctx context.Context) deploy.Result {
	ves := &ec2v1alpha1.VPCEndpointServiceConfiguration{}
	if err := t.Local.Get(ctx, client.ObjectKey{
		Name: utils.GetEndpointServiceName(t.PrivateLinkService),
	}, ves); err != nil {
		return t.requeueOnError(err)
	}
	if ves.Status.GetCondition(xpv1.TypeReady).Status != corev1.ConditionTrue {
		t.Logger.Info("EndpointService is not ready", "status", ves.Status.GetCondition(xpv1.TypeReady).Status)
		return t.requeueOnError(fmt.Errorf("EndpointService is not ready"))
	}
	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(defaultRequeueInterval)
	}
	if len(sc.NetworkLoadBalancerARNs) == 0 {
		t.Logger.Error(fmt.Errorf("EndpointService is not ready"), "network load balancer ARN is nil")
		return deploy.Error(fmt.Errorf("network load balancer ARN is nil")).WithRequeuAfter(defaultRequeueInterval)
	}
	lbARN := sc.NetworkLoadBalancerARNs[0]
	if lbARN == nil {
		t.Logger.Error(fmt.Errorf("EndpointService is not ready"), "network load balancer ARN is nil")
		return deploy.Error(fmt.Errorf("network load balancer ARN is nil")).WithRequeuAfter(defaultRequeueInterval)
	}
	for _, lbStatus := range t.PrivateLinkService.Status.LoadBalancers {
		if lbStatus.CloudResourceId == lo.FromPtr(lbARN) {
			return deploy.Done()
		}
	}
	return t.requeueOnError(fmt.Errorf("network load balancer ARN is not found"))
}

func (t *CutoverPls) getLoadBalancer(ctx context.Context, lb v1alpha1.LoadBalancer) (v1alpha1.LoadBalancerStatus, error) {
	info, err := t.ClusterCli.GetTidbGroup(ctx, &clusterctlpb.GetTidbGroupRequest{
		ClusterId:   lb.ClusterID,
		TidbGroupId: lb.ID,
	})
	if err != nil {
		t.Logger.Error(err, "get loadbalancer failed", "clusterId", lb.ClusterID, "loadbalancerId", lb.ID)
		return v1alpha1.LoadBalancerStatus{}, err
	}
	if info == nil {
		t.Logger.Error(nil, "loadbalancer not found", "clusterId", lb.ClusterID, "loadbalancerId", lb.ID)
		return v1alpha1.LoadBalancerStatus{}, fmt.Errorf("loadbalancer not found, clusterId: %s, loadbalancerId: %s", lb.ClusterID, lb.ID)
	}
	if !info.EnablePrivateLb || info.PrivateLb == nil {
		t.Logger.Error(nil, "private loadbalancer not found", "clusterId", lb.ClusterID, "loadbalancerId", lb.ID)
		return v1alpha1.LoadBalancerStatus{}, fmt.Errorf("private loadbalancer not found, clusterId: %s, loadbalancerId: %s", lb.ClusterID, lb.ID)
	}
	phase := v1alpha1.LoadBalancerPhaseActive
	if info.PrivateLb.Host == "" || info.PrivateLb.CloudResourceId == "" {
		t.Logger.Info("loadbalancer is not ready", "clusterId", lb.ClusterID, "loadbalancerId", lb.ID, "host", info.PrivateLb.Host, "cloudResourceId", info.PrivateLb.CloudResourceId)
		phase = v1alpha1.LoadBalancerPhaseCreating
	}
	return v1alpha1.LoadBalancerStatus{
		ID:              lb.ID,
		CloudResourceId: info.PrivateLb.CloudResourceId,
		Port:            info.PrivateLb.Port,
		Host:            info.PrivateLb.Host,
		Type:            clusterctlpb.LoadBalancer_PRIVATE.String(),
		Phase:           phase,
	}, nil
}
