package eks

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

	"github.com/aws/aws-sdk-go-v2/service/eks"
	"github.com/crossplane-contrib/provider-aws/apis/eks/manualv1alpha1"
	"github.com/crossplane-contrib/provider-aws/apis/eks/v1beta1"
	"github.com/samber/lo"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/client-go/tools/record"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/log"

	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-provider/pkg/cloud/aws"
	"github.com/tidbcloud/infra-provider/pkg/consts"
	"github.com/tidbcloud/infra-provider/pkg/k8s"
	"github.com/tidbcloud/infra-provider/pkg/namer"
	"github.com/tidbcloud/infra-provider/regional-server/api/v1alpha1"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/common/acscli"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/common/clustercli"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/config"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/controllers"
	. "github.com/tidbcloud/infra-provider/regional-server/pkg/controllers/upgradeoperation/common"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/resourcemanager/appbox"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/resourcemanager/upgradeoperation"
)

type upgrader struct {
	infraCli      client.Client
	eventRecorder record.EventRecorder
	uoWriter      upgradeoperation.Writer
	appBoxRW      appbox.APPBoxReaderWriter
	clusterCli    clustercli.ClusterClient
	manager       *controllers.Manager
	tmplConfig    config.TemplatesConfig
	awsCli        aws.Client
	appboxConfig  config.APPBoxConfig
	acsCli        acscli.Interface
}

var _ Upgrader = &upgrader{}

func NewUpgrader(infraCli client.Client, cli clustercli.ClusterClient, eventRecorder record.EventRecorder, manager *controllers.Manager, tmpConfig config.TemplatesConfig, awsCli aws.Client, appboxConfig config.APPBoxConfig, acsCli acscli.Interface) Upgrader {
	return &upgrader{
		uoWriter:      upgradeoperation.NewWriter(infraCli),
		appBoxRW:      appbox.NewAPPBoxReaderWriter(infraCli),
		infraCli:      infraCli,
		eventRecorder: eventRecorder,
		clusterCli:    cli,
		manager:       manager,
		tmplConfig:    tmpConfig,
		awsCli:        awsCli,
		appboxConfig:  appboxConfig,
		acsCli:        acsCli,
	}
}

func (u *upgrader) UpgradeControlPlane(ctx context.Context, uo *v1alpha1.UpgradeOperation, appBox *v1alpha1.APPBox) (ctrl.Result, error) {
	logger := log.FromContext(ctx)
	logger.Info("UpgradeControlPlane")

	desiredVersion := uo.Status.CurrentStep.Version
	eks, err := getEKSCR(ctx, u.infraCli, appBox)
	if err != nil {
		logger.Error(err, "unable to get the eks cluster CR")
		if apierrors.IsNotFound(err) {
			uo.Status.Phase = v1alpha1.UpgradeOperationPhaseFailed
			uo.Status.Reason = fmt.Sprintf("The EKS CR of appbox %s/%s is not found: %s", appBox.Namespace, appBox.Name, err.Error())
			return ctrl.Result{}, u.uoWriter.UpdateStatus(ctx, uo)
		}
		return ctrl.Result{}, err
	}

	switch eks.Status.AtProvider.Status {
	case v1beta1.ClusterStatusUpdating:
		LogAndEmitEvent(logger, u.eventRecorder, uo, NormalEvent, "The EKS cluster is updating, wait for the next reconcile", nil)
		return ctrl.Result{RequeueAfter: 1 * time.Minute}, nil

	case v1beta1.ClusterStatusActive:
		if eks.Status.AtProvider.Version == desiredVersion {
			LogAndEmitEvent(logger, u.eventRecorder, uo, NormalEvent, "The EKS cluster's control plane is already upgraded", nil)
			return FinishUpgradingResult(ctx, u.infraCli, u.uoWriter, uo)
		} else if ExpectToBeUpgraded(desiredVersion, eks.Status.AtProvider.Version) {
			logger.Info("start to upgrade the eks cluster's control plane")
			if appBox.Spec.KubernetesConfig.Version != desiredVersion {
				appBox.Spec.KubernetesConfig.Version = desiredVersion
				if err := u.appBoxRW.Update(ctx, appBox); err != nil {
					logger.Error(err, "unable to upgrade appbox CR")
				}
			}

			eks.Spec.ForProvider.Version = &desiredVersion
			if err := k8s.Apply(ctx, u.infraCli, eks); err != nil {
				logger.Error(err, "unable to upgrade eks cluster CR")
				return ctrl.Result{}, err
			}
			LogAndEmitEvent(logger, u.eventRecorder, uo, NormalEvent, "The eks CR is updated, wait for the next reconcile", nil)
			return ctrl.Result{RequeueAfter: 5 * time.Minute}, nil
		} else {
			LogAndEmitEvent(logger, u.eventRecorder, uo, WarningEvent, "The EKS cluster is in unexpected version", nil)
		}

	default:
		LogAndEmitEvent(logger, u.eventRecorder, uo, WarningEvent, "The EKS cluster is in unexpected status", nil)
	}

	return ctrl.Result{}, nil
}

func (u *upgrader) UpgradeAdminNodes(ctx context.Context, uo *v1alpha1.UpgradeOperation, appBox *v1alpha1.APPBox) (ctrl.Result, error) {
	logger := log.FromContext(ctx)
	logger.Info("UpgradeAdminNodes")

	adminNodegroupName := getAdminNodegroupName(appBox)
	logger.Info("admin nodegroup name", "name", adminNodegroupName)
	var adminNodegroup manualv1alpha1.NodeGroup
	if err := u.infraCli.Get(ctx, client.ObjectKey{
		Name: adminNodegroupName,
	}, &adminNodegroup); err != nil {
		if apierrors.IsNotFound(err) {
			// check if the bottlerocket admin nodegroup exists
			// This happens when AL2 to Bottlerocket migration is in progress
			var bottlerocketAdminNodegroup manualv1alpha1.NodeGroup
			if err := u.infraCli.Get(ctx, client.ObjectKey{
				Name: getBottlerocketAdminNodegroupName(adminNodegroupName),
			}, &bottlerocketAdminNodegroup); err != nil {
				if apierrors.IsNotFound(err) {
					// Neither old nor new nodegroup exists - this is an error condition
					LogAndEmitEvent(logger, u.eventRecorder, uo, WarningEvent, "neither admin nodegroup nor bottlerocket admin nodegroup found", nil)
					return ctrl.Result{}, fmt.Errorf("admin nodegroup %s and bottlerocket admin nodegroup %s not found", adminNodegroupName, getBottlerocketAdminNodegroupName(adminNodegroupName))
				}
				LogAndEmitEvent(logger, u.eventRecorder, uo, WarningEvent, "unable to get the bottlerocket admin nodegroup CR", err)
				return ctrl.Result{}, err
			}
			// old nodegroup is gone, but bottlerocket admin nodegroup exists, represent the upgrade is not finished yet
			logger.Info("admin nodegroup not found, but bottlerocket admin nodegroup exists, upgrade admin nodes AL2 to Bottlerocket is in progress")
			return u.upgradeAdminNodesAl2ToBottlerocket(ctx, uo, appBox)
		}
		// if not NotFound error, return error
		logger.Error(err, "unable to get the admin nodegroup CR")
		return ctrl.Result{}, err
	}

	if isAL2NodeGroup(&adminNodegroup) {
		return u.upgradeAdminNodesAl2ToBottlerocket(ctx, uo, appBox)
	}
	return u.upgradeManagedNodeGroups(ctx, uo, appBox, &manualv1alpha1.NodeGroupList{Items: []manualv1alpha1.NodeGroup{adminNodegroup}}, v1alpha1.AdminNodes)
}

func getBottlerocketAdminNodegroupName(name string) string {
	const k8sNameMaxLen = 63

	// Remove suffix if already present
	base := strings.TrimSuffix(name, bottlerocketSuffix)

	// Calculate maximum length for base to ensure result fits within k8s limit
	baseMaxLen := k8sNameMaxLen - len(bottlerocketSuffix)
	// Truncate base if needed
	if len(base) > baseMaxLen {
		base = base[:baseMaxLen]
		// Remove trailing dashes to avoid invalid names (RFC1123 label must end with alphanumeric)
		base = strings.TrimRight(base, "-")
	}

	return base + bottlerocketSuffix
}

func getAdminNodegroupName(appBox *v1alpha1.APPBox) string {
	if appBox.Labels[v1alpha1.IsLegacyKey] == "true" {
		return appBox.Annotations[v1alpha1.AnnotationKeyOwnCrossplaneNameNodeGroup]
	}
	return namer.UniqueIdentifier(appBox.ObjectMeta, namer.WithPrefix(consts.TiDBCloudPrefix))
}

func (u *upgrader) UpgradeDataNodes(ctx context.Context, uo *v1alpha1.UpgradeOperation, appBox *v1alpha1.APPBox) (ctrl.Result, error) {
	logger := log.FromContext(ctx)
	logger.Info("UpgradeDataNodes")

	// Get desired and current control plane versions.
	desiredVersion := uo.Status.CurrentStep.Version
	eksCR, err := getEKSCR(ctx, u.infraCli, appBox)
	if err != nil {
		logger.Error(err, "unable to get the EKS CR")
		return ctrl.Result{}, err
	}
	currentCPVersion := eksCR.Status.AtProvider.Version

	// Check if the control plane is upgraded.
	if !isControlPlaneUpgraded(eksCR, desiredVersion, currentCPVersion) {
		LogAndEmitEvent(logger, u.eventRecorder, uo, WarningEvent, ControlPlaneNotUpgraded, nil)
		return ctrl.Result{}, uo.Fail(ctx, u.infraCli, ControlPlaneNotUpgraded)
	}

	msg, done, err := UpgradeDataNodes(ctx, u.clusterCli, uo, desiredVersion)
	if err != nil {
		LogAndEmitEvent(logger, u.eventRecorder, uo, WarningEvent, "failed to upgrade data nodes", err)
		return ctrl.Result{}, err
	}

	if done {
		LogAndEmitEvent(logger, u.eventRecorder, uo, NormalEvent, "tidb nodes are already upgraded", nil)
		return FinishUpgradingResult(ctx, u.infraCli, u.uoWriter, uo)
	}

	uo.Status.Reason = msg
	LogAndEmitEvent(logger, u.eventRecorder, uo, NormalEvent, msg, nil)
	return ctrl.Result{Requeue: true}, u.uoWriter.UpdateStatus(ctx, uo)
}

func (u *upgrader) UpgradeEmptyNodePools(ctx context.Context, uo *v1alpha1.UpgradeOperation, appBox *v1alpha1.APPBox) (ctrl.Result, error) {
	logger := log.FromContext(ctx)
	logger.Info("UpgradeEmptyNodePools")

	desiredVersion := uo.Status.CurrentStep.Version

	eksCR, err := getEKSCR(ctx, u.infraCli, appBox)
	if err != nil {
		logger.Error(err, "unable to get the eks cluster CR")
		return ctrl.Result{}, err
	}

	providerConfigName := ""
	if eksCR.Spec.ResourceSpec.ProviderConfigReference != nil {
		providerConfigName = eksCR.Spec.ResourceSpec.ProviderConfigReference.Name
	}
	if providerConfigName == "" {
		return ctrl.Result{}, fmt.Errorf("failed to get provider config name from EKS cluster CR %s", eksCR.Name)
	}

	assumeRole, err := getAssumedRoleFromProviderConfig(ctx, u.infraCli, providerConfigName)
	if err != nil {
		logger.Error(err, "unable to get assumed role from provider config", "providerConfig", providerConfigName)
		return ctrl.Result{}, err
	}

	clusterName := appBox.Status.KubernetesClusterName
	if clusterName == "" {
		return ctrl.Result{}, fmt.Errorf("appbox %s/%s has empty kubernetes cluster name", appBox.Namespace, appBox.Name)
	}

	eksClient := u.awsCli.EKS(assumeRole)

	// List EKS nodegroups via AWS SDK, then filter by node labels: usedby=customer-tidb.
	nodegroupNames := make([]string, 0)
	p := eks.NewListNodegroupsPaginator(eksClient, &eks.ListNodegroupsInput{
		ClusterName: lo.ToPtr(clusterName),
	})
	for p.HasMorePages() {
		out, err := p.NextPage(ctx)
		if err != nil {
			logger.Error(err, "unable to list EKS nodegroups via SDK", "clusterName", clusterName)
			return ctrl.Result{}, err
		}
		nodegroupNames = append(nodegroupNames, out.Nodegroups...)
	}

	emptyNodeGroups := make([]nodeGroupCR, 0)
	for _, ngName := range nodegroupNames {
		desc, err := eksClient.DescribeNodegroup(ctx, &eks.DescribeNodegroupInput{
			ClusterName:   lo.ToPtr(clusterName),
			NodegroupName: lo.ToPtr(ngName),
		})
		if err != nil {
			logger.Error(err, "unable to describe nodegroup via SDK", "clusterName", clusterName, "nodegroup", ngName)
			continue
		}
		if desc.Nodegroup == nil {
			continue
		}

		labels := desc.Nodegroup.Labels
		if labels == nil || (labels[consts.TagKeyUsedBy] != consts.UsedByCustomerTiDB &&
			labels[cloudv1.TagLabelPrefix+consts.TagKeyUsedBy] != consts.UsedByCustomerTiDB) {
			continue
		}

		desiredSize := int32(0)
		if desc.Nodegroup.ScalingConfig != nil && desc.Nodegroup.ScalingConfig.DesiredSize != nil {
			desiredSize = *desc.Nodegroup.ScalingConfig.DesiredSize
		}
		if desiredSize != 0 {
			continue
		}

		ngCR, err := fetchNodeGroupCR(ctx, u.infraCli, ngName)
		if err != nil {
			logger.Error(err, "unable to get nodegroup CR for empty nodegroup", "nodegroup", ngName)
			continue
		}
		logger.Info("nodegroup does not have nodes", "nodegroup", ngName, "version", ngCR.GetVersion())
		emptyNodeGroups = append(emptyNodeGroups, ngCR)
	}

	// Delete empty nodegroups if the delete option is set.
	if uo.Status.CurrentStep != nil && uo.Status.CurrentStep.Options["delete"] == "true" {
		LogAndEmitEvent(logger, u.eventRecorder, uo, NormalEvent, "Start to delete empty node pools", nil)

		for i := range emptyNodeGroups {
			ng := emptyNodeGroups[i]

			// Only delete nodegroups whose version is lower than the desired version.
			lower, err := IsVersionLower(ng.GetVersion(), desiredVersion)
			if err != nil {
				logger.Error(err, "skip deleting empty nodegroup due to invalid version comparison",
					"name", ng.GetName(), "version", ng.GetVersion(), "desiredVersion", desiredVersion)
				continue
			}
			if !lower {
				logger.Info("skip deleting the empty nodegroup because its version is not lower than the desired version",
					"name", ng.GetName(), "version", ng.GetVersion(), "desiredVersion", desiredVersion)
				continue
			}

			logger.Info("start to delete the empty nodegroup", "name", ng.GetName(), "version", ng.GetVersion())
			if err := ng.Delete(ctx, u.infraCli); err != nil {
				logger.Error(err, "unable to delete the empty nodegroup", "name", ng.GetName())
			}
		}

		return FinishUpgradingResult(ctx, u.infraCli, u.uoWriter, uo)
	}

	LogAndEmitEvent(logger, u.eventRecorder, uo, NormalEvent, "Start to upgrade empty node pools", nil)
	return u.upgradeEmptyNodeGroups(ctx, uo, appBox, emptyNodeGroups)
}

func (u *upgrader) upgradeEmptyNodeGroups(ctx context.Context, uo *v1alpha1.UpgradeOperation, appBox *v1alpha1.APPBox, ngs []nodeGroupCR) (ctrl.Result, error) {
	logger := log.FromContext(ctx)

	if len(ngs) == 0 {
		logger.Info("no empty nodegroups")
		return FinishUpgradingResult(ctx, u.infraCli, u.uoWriter, uo)
	}

	desiredVersion := uo.Status.CurrentStep.Version

	eksCR, err := getEKSCR(ctx, u.infraCli, appBox)
	if err != nil {
		logger.Error(err, "unable to get the eks cluster CR")
		return ctrl.Result{}, err
	}
	currentVersion := eksCR.Status.AtProvider.Version
	if !isControlPlaneUpgraded(eksCR, desiredVersion, currentVersion) {
		logger.Info("it's not ready to upgrade the nodegroup, please check the eks cluster's version", "current version", currentVersion, "desired version", desiredVersion)
		return ctrl.Result{}, nil
	}

	done := 0
	for _, ng := range ngs {
		status := strings.ToLower(ng.GetStatus())
		logger.Info("node group", "nodeType", v1alpha1.EmptyNodePools, "name", ng.GetName(), "version", ng.GetVersion(), "status", ng.GetStatus())

		switch status {
		case strings.ToLower(string(manualv1alpha1.NodeGroupStatusUpdating)), "updating":
			LogAndEmitEvent(logger, u.eventRecorder, uo, NormalEvent, "The node group is updating, wait for next reconcile", nil)

			ng.SetVersion(desiredVersion)
			if err := ng.Apply(ctx, u.infraCli); err != nil {
				logger.Error(err, "unable to upgrade the node group CR", "name", ng.GetName())
				return ctrl.Result{}, err
			}

		case strings.ToLower(string(manualv1alpha1.NodeGroupStatusActive)), "active":
			if ng.GetVersion() == desiredVersion {
				if IsCRReadyAndSynced(ng.GetConditions()) {
					done++
					LogAndEmitEvent(logger, u.eventRecorder, uo, NormalEvent, fmt.Sprintf("The node group %s is already upgraded", ng.GetName()), nil)
				} else {
					ng.SetVersion(desiredVersion)
					if err := ng.Apply(ctx, u.infraCli); err != nil {
						logger.Error(err, "unable to upgrade the node group CR", "name", ng.GetName())
						return ctrl.Result{}, err
					}
				}
			} else {
				logger.Info("start to upgrade the node group", "current version", ng.GetVersion(), "desired version", desiredVersion)
				ng.SetVersion(desiredVersion)
				if err := ng.Apply(ctx, u.infraCli); err != nil {
					logger.Error(err, "unable to upgrade the node group CR", "name", ng.GetName())
					return ctrl.Result{}, err
				}
			}

		default:
			logger.Info("the node group is in unexpected status", "name", ng.GetName(), "status", ng.GetStatus())
		}
	}

	if done == len(ngs) {
		LogAndEmitEvent(logger, u.eventRecorder, uo, NormalEvent, "All empty nodegroups are upgraded", nil)
		return FinishUpgradingResult(ctx, u.infraCli, u.uoWriter, uo)
	}
	return ctrl.Result{RequeueAfter: 1 * time.Minute}, nil
}

func (u *upgrader) UpgradeO11yNodes(ctx context.Context, uo *v1alpha1.UpgradeOperation, appBox *v1alpha1.APPBox) (ctrl.Result, error) {
	return DoUpgradeO11yNodes(ctx, u.infraCli, u.acsCli, u.uoWriter, u.eventRecorder, uo, appBox)
}

func (u *upgrader) upgradeManagedNodeGroups(ctx context.Context, uo *v1alpha1.UpgradeOperation, appBox *v1alpha1.APPBox, ngs *manualv1alpha1.NodeGroupList, nodeType v1alpha1.UpgradeOperationTarget) (ctrl.Result, error) {
	logger := log.FromContext(ctx)
	logger.Info("upgradeManagedNodeGroups")

	if len(ngs.Items) == 0 {
		logger.Info("no node groups", "nodeType", nodeType)
		return FinishUpgradingResult(ctx, u.infraCli, u.uoWriter, uo)
	}

	desiredVersion := uo.Status.CurrentStep.Version
	options := uo.Status.CurrentStep.Options
	eks, err := getEKSCR(ctx, u.infraCli, appBox)
	if err != nil {
		logger.Error(err, "unable to get the eks cluster CR")
		return ctrl.Result{}, err
	}
	currentVersion := eks.Status.AtProvider.Version

	if !isControlPlaneUpgraded(eks, desiredVersion, currentVersion) {
		logger.Info("it's not ready to upgrade the nodegroup, please check the eks cluster's version", "current version", currentVersion, "desired version", desiredVersion)
		return ctrl.Result{}, nil
	}

	done := 0
	for i := range ngs.Items {
		ng := &ngs.Items[i]
		logger.Info("node group", "nodeType", nodeType, "name", ng.Name, "spec version", ng.Spec.ForProvider.Version, "status version", ng.Status.AtProvider.Version, "status", ng.Status.AtProvider.Status)

		switch ng.Status.AtProvider.Status {
		case manualv1alpha1.NodeGroupStatusUpdating:
			LogAndEmitEvent(logger, u.eventRecorder, uo, NormalEvent, "The node group is updating, wait for next reconcile", nil)

			// The version may be modified back by other controllers, so update it again.
			if err := updateNodeGroupVersion(ctx, u.infraCli, ng, desiredVersion); err != nil {
				logger.Error(err, "unable to upgrade the node group CR")
				return ctrl.Result{}, err
			}

		case manualv1alpha1.NodeGroupStatusActive:
			if ng.Status.AtProvider.Version == desiredVersion {
				if IsCRReadyAndSynced(ng.Status.Conditions) {
					done++
					if err = resetNodeGroupUpdateConfig(ctx, u.infraCli, ng); err != nil {
						LogAndEmitEvent(logger, u.eventRecorder, uo, WarningEvent, fmt.Sprintf("failed to reset node group %s's update config", ng.Name), err)
					} else {
						LogAndEmitEvent(logger, u.eventRecorder, uo, NormalEvent, fmt.Sprintf("The node group %s is already upgraded", ng.Name), nil)
					}
				} else {
					if err := updateNodeGroupVersion(ctx, u.infraCli, ng, desiredVersion); err != nil {
						logger.Error(err, "unable to upgrade the node group CR")
						return ctrl.Result{}, err
					}
				}
			} else {
				logger.Info("start to upgrade the node group", "current version", ng.Status.AtProvider.Version, "desired version", desiredVersion)

				// Once the node group is updating, we can't update the update config.
				// So we need set the update config before updating the version.
				if setNodeGroupUpdateConfig(ng, options) {
					LogAndEmitEvent(logger, u.eventRecorder, uo, NormalEvent, fmt.Sprintf("The node group %s's update config is updated, wait for next reconcile", ng.Name), nil)
					return ctrl.Result{Requeue: true}, u.infraCli.Update(ctx, ng)
				}

				if err := updateNodeGroupVersion(ctx, u.infraCli, ng, desiredVersion); err != nil {
					logger.Error(err, "unable to upgrade the node group CR")
					return ctrl.Result{}, err
				}
			}
		default:
			logger.Info("the node group is in unexpected status", "status", ng.Status.AtProvider.Status)
		}
	}

	if done == len(ngs.Items) {
		LogAndEmitEvent(logger, u.eventRecorder, uo, NormalEvent, fmt.Sprintf("All node groups of %s are upgraded", nodeType), nil)
		return FinishUpgradingResult(ctx, u.infraCli, u.uoWriter, uo)
	}

	return ctrl.Result{Requeue: true}, nil
}
