package eks

import (
	"context"
	"fmt"
	"time"

	ec2v1alpha1 "github.com/crossplane-contrib/provider-aws/apis/ec2/v1alpha1"
	"github.com/crossplane-contrib/provider-aws/apis/eks/manualv1alpha1"
	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	"github.com/crossplane/crossplane-runtime/pkg/meta"
	"github.com/samber/lo"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"gomodules.xyz/jsonpatch/v2"
	corev1 "k8s.io/api/core/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/log"

	"github.com/tidbcloud/infra-provider/pkg/consts"
	"github.com/tidbcloud/infra-provider/pkg/k8s"
	"github.com/tidbcloud/infra-provider/pkg/nodegroup"
	"github.com/tidbcloud/infra-provider/pkg/util"
	"github.com/tidbcloud/infra-provider/regional-server/api/v1alpha1"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/controllers/upgradeoperation/common"
)

const (
	launchTemplateReadyTimeout = 10 * time.Minute
	nodeGroupReadyTimeout      = 20 * time.Minute
	readyPollInterval          = 10 * time.Second
)

func (u *upgrader) upgradeAdminNodesAl2ToBottlerocket(ctx context.Context, uo *v1alpha1.UpgradeOperation, appBox *v1alpha1.APPBox) (ctrl.Result, error) {
	logger := log.FromContext(ctx)
	logger.Info("upgrade admin nodes AL2 to Bottlerocket")

	adminNodegroupName := getAdminNodegroupName(appBox)
	// step 1: ensure new launch template is created
	common.LogAndEmitEvent(logger, u.eventRecorder, uo, common.NormalEvent, "ensuring new launch template is created", nil)
	if err := u.ensureNewLaunchTemplate(ctx, appBox, adminNodegroupName, getBottlerocketAdminNodegroupName(adminNodegroupName)); err != nil {
		common.LogAndEmitEvent(logger, u.eventRecorder, uo, common.WarningEvent, "failed to ensure new launch template is created", err)
		return ctrl.Result{}, err
	}
	// step 2: ensure new node group with Bottlerocket AMI is created and ready
	common.LogAndEmitEvent(logger, u.eventRecorder, uo, common.NormalEvent, "ensuring new node group with Bottlerocket AMI is created and ready", nil)
	if err := u.ensureNewNodeGroup(ctx, uo, appBox, adminNodegroupName, getBottlerocketAdminNodegroupName(adminNodegroupName)); err != nil {
		common.LogAndEmitEvent(logger, u.eventRecorder, uo, common.WarningEvent, "failed to ensure new node group with Bottlerocket AMI is created and ready", err)
		return ctrl.Result{}, err
	}
	// step 3: delete old node groups and launch template
	common.LogAndEmitEvent(logger, u.eventRecorder, uo, common.NormalEvent, "deleting old node groups and launch template", nil)
	if err := u.deleteOldNodeGroup(ctx, adminNodegroupName); err != nil {
		common.LogAndEmitEvent(logger, u.eventRecorder, uo, common.WarningEvent, "failed to delete old node groups", err)
		return ctrl.Result{}, err
	}
	if err := u.deleteOldLaunchTemplate(ctx, adminNodegroupName); err != nil {
		common.LogAndEmitEvent(logger, u.eventRecorder, uo, common.WarningEvent, "failed to delete old launch template", err)
		return ctrl.Result{}, err
	}
	// step 4: update appbox annotations to point to new node group
	common.LogAndEmitEvent(logger, u.eventRecorder, uo, common.NormalEvent, "updating appbox annotations to point to new node group", nil)
	if err := u.updateAppboxAnnotations(ctx, appBox, getBottlerocketAdminNodegroupName(adminNodegroupName)); err != nil {
		common.LogAndEmitEvent(logger, u.eventRecorder, uo, common.WarningEvent, "failed to update appbox annotations to point to new node group", err)
		return ctrl.Result{}, err
	}

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

func (u *upgrader) ensureNewLaunchTemplate(ctx context.Context, appBox *v1alpha1.APPBox, oldLTName string, newLTName string) error {
	logger := log.FromContext(ctx)
	logger.Info("ensure new launch template")

	// check if new launch template exists
	newLaunchTemplate := &ec2v1alpha1.LaunchTemplate{}
	if err := u.infraCli.Get(ctx, client.ObjectKey{Name: newLTName}, newLaunchTemplate); err != nil {
		if !apierrors.IsNotFound(err) {
			return fmt.Errorf("failed to get new launch template %s: %w", newLTName, err)
		}
		oldLaunchTemplate := &ec2v1alpha1.LaunchTemplate{}
		if err := u.infraCli.Get(ctx, client.ObjectKey{Name: oldLTName}, oldLaunchTemplate); err != nil {
			return fmt.Errorf("failed to get old launch template %s: %w", oldLTName, err)
		}
		newLaunchTemplate = oldLaunchTemplate.DeepCopy()
		resetObjectForApply(newLaunchTemplate)
		if newLaunchTemplate.Spec.ForProvider.LaunchTemplateData == nil {
			newLaunchTemplate.Spec.ForProvider.LaunchTemplateData = &ec2v1alpha1.RequestLaunchTemplateData{}
		}
		newLaunchTemplate.Spec.ForProvider.LaunchTemplateData.BlockDeviceMappings = nodegroup.GetBlockDeviceMappingsForBottlerocket(u.appboxConfig.NodeConfig.SystemVolumeSize)
		newLaunchTemplate.Name = newLTName
		newLaunchTemplate.Spec.ForProvider.LaunchTemplateName = lo.ToPtr(newLTName)
		replaceLaunchTemplateTags(newLaunchTemplate, appBox, newLTName)
		if err := k8s.Apply(ctx, u.infraCli, newLaunchTemplate); err != nil {
			return fmt.Errorf("failed to create new launch template %s: %w", newLTName, err)
		}
	}
	if err := waitLaunchTemplateReady(ctx, u.infraCli, newLTName, launchTemplateReadyTimeout); err != nil {
		return fmt.Errorf("failed to wait for new launch template %s to be ready: %w", newLTName, err)
	}
	return nil
}

func replaceLaunchTemplateTags(lt *ec2v1alpha1.LaunchTemplate, appBox *v1alpha1.APPBox, ltName string) {
	if lt == nil || appBox == nil {
		return
	}
	tags := cloudv1.TagsFromLabels(appBox.Labels)
	tags = util.AddResourceName(tags, ltName)

	// add node group name to master node asg tags
	// cluster-autoscaler will use this tag to find out the node group
	tags["tidbcloud.com/nodegroup-name"] = ltName

	// Tags on the launch template itself (if any).
	for _, spec := range lt.Spec.ForProvider.TagSpecifications {
		if spec == nil {
			continue
		}
		spec.Tags = util.GetEC2V1AlphaTags(tags)
	}

	if lt.Spec.ForProvider.LaunchTemplateData == nil {
		lt.Spec.ForProvider.LaunchTemplateData = &ec2v1alpha1.RequestLaunchTemplateData{}
	}
	// If old launch template doesn't have tagSpecifications, create the default set (consistent with
	// `pkg/controllers/appbox/task/eks/nodegroup.go`).
	if len(lt.Spec.ForProvider.LaunchTemplateData.TagSpecifications) == 0 {
		lt.Spec.ForProvider.LaunchTemplateData.TagSpecifications = []*ec2v1alpha1.LaunchTemplateTagSpecificationRequest{
			{ResourceType: lo.ToPtr("volume")},
			{ResourceType: lo.ToPtr("instance")},
			{ResourceType: lo.ToPtr("network-interface")},
		}
	}
	for _, spec := range lt.Spec.ForProvider.LaunchTemplateData.TagSpecifications {
		if spec == nil {
			continue
		}
		// replace tags with new tags
		spec.Tags = util.GetEC2V1AlphaTags(tags)
	}
}

func (u *upgrader) ensureNewNodeGroup(ctx context.Context, uo *v1alpha1.UpgradeOperation, appBox *v1alpha1.APPBox, oldNGName string, newNGName string) error {
	logger := log.FromContext(ctx)
	logger.Info("ensure new node group")

	// Check if upgrade operation has current step
	if uo.Status.CurrentStep == nil {
		return fmt.Errorf("upgrade operation current step is nil")
	}

	// check new node group exists
	newNodeGroup := &manualv1alpha1.NodeGroup{}
	if err := u.infraCli.Get(ctx, client.ObjectKey{Name: newNGName}, newNodeGroup); err != nil {
		if !apierrors.IsNotFound(err) {
			return fmt.Errorf("failed to get new node group %s: %w", newNGName, err)
		}
		// New node group doesn't exist, need to create it from old one
		oldNodeGroup := &manualv1alpha1.NodeGroup{}
		if err := u.infraCli.Get(ctx, client.ObjectKey{Name: oldNGName}, oldNodeGroup); err != nil {
			if apierrors.IsNotFound(err) {
				return fmt.Errorf("both old node group %s and new node group %s not found, cannot proceed with upgrade", oldNGName, newNGName)
			}
			return fmt.Errorf("failed to get old node group %s: %w", oldNGName, err)
		}
		// Create new node group from old one
		newNodeGroup = oldNodeGroup.DeepCopy()
		resetObjectForApply(newNodeGroup)
		newNodeGroup.Name = newNGName
		replaceNodegroupTags(newNodeGroup, appBox, newNGName)
		// Update launch template reference to point to new launch template
		if newNodeGroup.Spec.ForProvider.LaunchTemplate != nil && newNodeGroup.Spec.ForProvider.LaunchTemplate.NameRef != nil {
			newLTName := getBottlerocketAdminNodegroupName(oldNGName)
			newNodeGroup.Spec.ForProvider.LaunchTemplate.NameRef.Name = newLTName
			newNodeGroup.Spec.ForProvider.LaunchTemplate.Name = lo.ToPtr(newLTName)
		}
		setNodeGroupVersionAndAMIType(newNodeGroup, uo.Status.CurrentStep.Version, nodegroup.BottleRocketX86AMIType)
		if err := k8s.Apply(ctx, u.infraCli, newNodeGroup); err != nil {
			return fmt.Errorf("failed to create new node group %s: %w", newNGName, err)
		}
		logger.Info("created new node group", "newNGName", newNGName)
	} else {
		logger.Info("new node group already exists", "newNGName", newNGName)
	}

	if err := waitNodeGroupReady(ctx, u.infraCli, newNGName, nodeGroupReadyTimeout); err != nil {
		return fmt.Errorf("failed to wait for new node group %s to be ready: %w", newNGName, err)
	}
	return nil
}

func replaceNodegroupTags(ng *manualv1alpha1.NodeGroup, appBox *v1alpha1.APPBox, ngName string) {
	if ng == nil || appBox == nil {
		return
	}
	tags := cloudv1.TagsFromLabels(appBox.Labels)
	tags = util.AddResourceName(tags, ngName)
	// add node group name to master node asg tags
	// cluster-autoscaler will use this tag to find out the node group
	tags["tidbcloud.com/nodegroup-name"] = ngName

	ng.Spec.ForProvider.Tags = tags
}

func (u *upgrader) deleteOldNodeGroup(ctx context.Context, oldNGName string) error {
	logger := log.FromContext(ctx)
	logger.Info("delete old node group")

	oldNodeGroup := &manualv1alpha1.NodeGroup{}
	if err := u.infraCli.Get(ctx, client.ObjectKey{Name: oldNGName}, oldNodeGroup); err != nil {
		if !apierrors.IsNotFound(err) {
			return fmt.Errorf("failed to get old node group %s: %w", oldNGName, err)
		}
		return nil
	}
	if err := k8s.DeleteObject(ctx, u.infraCli, oldNodeGroup); err != nil {
		return fmt.Errorf("failed to delete old node group %s: %w", oldNGName, err)
	}
	if err := k8s.PatchObject(ctx, u.infraCli, oldNodeGroup, k8s.RemoveCrossplaneExternalCreatePending, k8s.DeletionTrigger); err != nil {
		return fmt.Errorf("failed to patch old node group %s: %w", oldNGName, err)
	}
	return nil
}

func (u *upgrader) deleteOldLaunchTemplate(ctx context.Context, oldLTName string) error {
	logger := log.FromContext(ctx)
	logger.Info("delete old launch template")
	oldLaunchTemplate := &ec2v1alpha1.LaunchTemplate{}
	if err := u.infraCli.Get(ctx, client.ObjectKey{Name: oldLTName}, oldLaunchTemplate); err != nil {
		if !apierrors.IsNotFound(err) {
			return fmt.Errorf("failed to get old launch template %s: %w", oldLTName, err)
		}
		return nil
	}
	if err := k8s.DeleteObject(ctx, u.infraCli, oldLaunchTemplate); err != nil {
		return fmt.Errorf("failed to delete old launch template %s: %w", oldLTName, err)
	}
	if err := k8s.PatchObject(ctx, u.infraCli, oldLaunchTemplate, k8s.RemoveCrossplaneExternalCreatePending, k8s.DeletionTrigger); err != nil {
		return fmt.Errorf("failed to patch old launch template %s: %w", oldLTName, err)
	}

	return nil
}

func (u *upgrader) updateAppboxAnnotations(ctx context.Context, appBox *v1alpha1.APPBox, newNGName string) error {
	logger := log.FromContext(ctx)
	logger.Info("update appbox annotations")

	updateNodeGroupAnnotation := func(obj client.Object) *jsonpatch.Operation {
		annotations := obj.GetAnnotations()
		op := "replace"
		if annotations == nil || annotations[v1alpha1.AnnotationKeyOwnCrossplaneNameNodeGroup] == "" {
			op = "add"
		}
		return &jsonpatch.Operation{
			Operation: op,
			Path:      "/metadata/annotations/cloud.pingcap.com~1own-crossplane-name-nodegroup",
			Value:     newNGName,
		}
	}

	if err := k8s.PatchObject(ctx, u.infraCli, appBox, updateNodeGroupAnnotation); err != nil {
		return fmt.Errorf("failed to patch appbox annotations: %w", err)
	}
	return nil
}

func resetObjectForApply(obj metav1.Object) {
	obj.SetResourceVersion("")
	obj.SetUID("")
	obj.SetGeneration(0)
	obj.SetSelfLink("")
	obj.SetManagedFields(nil)
	obj.SetFinalizers(nil)
	obj.SetOwnerReferences(nil)
	obj.SetCreationTimestamp(metav1.Time{})

	annotations := obj.GetAnnotations()
	if annotations != nil {
		delete(annotations, consts.LastAppliedConfigAnnotation)
		delete(annotations, meta.AnnotationKeyExternalName)
		delete(annotations, meta.AnnotationKeyExternalCreatePending)
		delete(annotations, meta.AnnotationKeyExternalCreateSucceeded)
		delete(annotations, meta.AnnotationKeyExternalCreateFailed)
		delete(annotations, meta.AnnotationKeyReconciliationPaused)
		if len(annotations) == 0 {
			obj.SetAnnotations(nil)
		} else {
			obj.SetAnnotations(annotations)
		}
	}
}

func waitLaunchTemplateReady(ctx context.Context, cli client.Client, name string, timeout time.Duration) error {
	ticker := time.NewTicker(readyPollInterval)
	defer ticker.Stop()

	deadline := time.Now().Add(timeout)
	var (
		conditionStatus  = corev1.ConditionUnknown
		conditionReason  string
		conditionMessage string
	)
	for {
		var lt ec2v1alpha1.LaunchTemplate
		switch err := cli.Get(ctx, client.ObjectKey{Name: name}, &lt); {
		case err == nil:
			condition := lt.Status.GetCondition(xpv1.TypeReady)
			// GetCondition always returns a Condition struct, check if it's actually set
			if condition.LastTransitionTime.IsZero() {
				// Condition not set yet, keep polling
			} else {
				conditionStatus = condition.Status
				conditionReason = string(condition.Reason)
				conditionMessage = condition.Message
				if condition.Status == corev1.ConditionTrue {
					return nil
				}
			}
		case apierrors.IsNotFound(err):
			// The Crossplane controller may not have created the object yet; keep polling.
		default:
			return fmt.Errorf("failed to get launch template %s: %w", name, err)
		}

		if time.Now().After(deadline) {
			return fmt.Errorf("timeout waiting for launch template %s ready (condition=%s, reason=%s, message=%s)",
				name, conditionStatus, conditionReason, conditionMessage)
		}

		select {
		case <-ctx.Done():
			return fmt.Errorf("context canceled while waiting for launch template %s ready: %w", name, ctx.Err())
		case <-ticker.C:
		}
	}
}

func waitNodeGroupReady(ctx context.Context, cli client.Client, name string, timeout time.Duration) error {
	ticker := time.NewTicker(readyPollInterval)
	defer ticker.Stop()

	deadline := time.Now().Add(timeout)
	var (
		conditionStatus  = corev1.ConditionUnknown
		atProviderStatus manualv1alpha1.NodeGroupStatusType
		conditionReason  string
		conditionMessage string
	)
	for {
		var ng manualv1alpha1.NodeGroup
		switch err := cli.Get(ctx, client.ObjectKey{Name: name}, &ng); {
		case err == nil:
			condition := ng.Status.GetCondition(xpv1.TypeReady)
			// GetCondition always returns a Condition struct, check if it's actually set
			if condition.LastTransitionTime.IsZero() {
				// Condition not set yet, keep polling
			} else {
				conditionStatus = condition.Status
				atProviderStatus = ng.Status.AtProvider.Status
				conditionReason = string(condition.Reason)
				conditionMessage = condition.Message
				if condition.Status == corev1.ConditionTrue && ng.Status.AtProvider.Status == manualv1alpha1.NodeGroupStatusActive {
					return nil
				}
			}
		case apierrors.IsNotFound(err):
			// NodeGroup recreation might still be in-flight; keep polling until it shows up.
		default:
			return fmt.Errorf("failed to get node group %s: %w", name, err)
		}

		if time.Now().After(deadline) {
			return fmt.Errorf("timeout waiting for node group %s ready (condition=%s, status=%s, reason=%s, message=%s)",
				name, conditionStatus, string(atProviderStatus), conditionReason, conditionMessage)
		}

		select {
		case <-ctx.Done():
			return fmt.Errorf("context canceled while waiting for node group %s ready: %w", name, ctx.Err())
		case <-ticker.C:
		}
	}
}

func setNodeGroupVersionAndAMIType(ng *manualv1alpha1.NodeGroup, version string, amiType string) {
	ng.Spec.ForProvider.Version = lo.ToPtr(version)
	ng.Spec.ForProvider.ReleaseVersion = nil
	ng.Spec.ForProvider.AMIType = lo.ToPtr(amiType)
}

func isAL2NodeGroup(ng *manualv1alpha1.NodeGroup) bool {
	return ng.Spec.ForProvider.AMIType != nil && *ng.Spec.ForProvider.AMIType == nodegroup.X86AMIType
}
