package eks

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

	"github.com/aws/aws-sdk-go-v2/service/eks"
	eksv1alpha1 "github.com/crossplane-contrib/provider-aws/apis/eks/v1alpha1"
	xpawsv1beta1 "github.com/crossplane-contrib/provider-aws/apis/v1beta1"
	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	xpv1meta "github.com/crossplane/crossplane-runtime/pkg/meta"
	"github.com/samber/lo"
	"helm.sh/helm/v3/pkg/chart"
	appsv1 "k8s.io/api/apps/v1"
	corev1 "k8s.io/api/core/v1"
	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
	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/cloud/aws"
	"github.com/tidbcloud/infra-provider/pkg/helm"
	"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/config"
	. "github.com/tidbcloud/infra-provider/regional-server/pkg/controllers/upgradeoperation/common"
)

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

	// if the resource is not migrated or addons is already fixed, do not need to fix the addons again
	if appBox.Labels[v1alpha1.IsLegacyKey] != "true" || appBox.Annotations[v1alpha1.AnnotationKeyUpgradeAddonsFixed] == "true" {
		LogAndEmitEvent(logger, u.eventRecorder, uo, NormalEvent, "Skip upgrading addons", nil)
		return FinishUpgradingResult(ctx, u.infraCli, u.uoWriter, uo)
	}

	// make sure the appbox is not paused
	if err := ResumeReconcile(ctx, u.appBoxRW, appBox); err != nil {
		return ctrl.Result{}, fmt.Errorf("failed to unpause appbox: %w", err)
	}
	LogAndEmitEvent(logger, u.eventRecorder, uo, NormalEvent, "Unpaused the appbox", nil)

	addonTemplate, tmplExists := u.tmplConfig.GetAddonTemplate(appBox.Spec.KubernetesConfig.AddonTemplateID)
	if !tmplExists {
		LogAndEmitEvent(logger, u.eventRecorder, uo, WarningEvent, "AddonTemplateNotFound", nil)
		return ctrl.Result{}, fmt.Errorf("addon template not found for k8s version %s", appBox.Spec.KubernetesConfig.Version)
	}

	configAddons, versionExists := addonTemplate.EKS[appBox.Spec.KubernetesConfig.Version]
	if !versionExists {
		LogAndEmitEvent(logger, u.eventRecorder, uo, WarningEvent, "AddonTemplateNotFound", nil)
		return ctrl.Result{}, fmt.Errorf("addon template not found for k8s version %s", appBox.Spec.KubernetesConfig.Version)
	}

	labelSelectors, err := metav1.LabelSelectorAsSelector(&metav1.LabelSelector{
		MatchLabels: map[string]string{
			namer.AppBoxSelectorKey: appBox.Name,
		},
	})
	if err != nil {
		logger.Error(err, "unable to convert label selector")
		return ctrl.Result{}, err
	}

	addons := &eksv1alpha1.AddonList{}
	if err := u.infraCli.List(ctx, addons, client.MatchingLabelsSelector{Selector: labelSelectors}); err != nil {
		logger.Error(err, "unable to list addons")
		return ctrl.Result{}, err
	}

	existAddonsMap := lo.SliceToMap(addons.Items, func(addon eksv1alpha1.Addon) (string, eksv1alpha1.Addon) {
		return *addon.Spec.ForProvider.AddonName, addon
	})

	upgradeDone := true
	for _, addonConfig := range configAddons {
		triggerKey := fmt.Sprintf("%s%s", v1alpha1.AnnotationKeyUpgradeTrigger, addonConfig.AddonName)
		triggerVal, triggerExist := appBox.Annotations[triggerKey]
		addonItem, ok := existAddonsMap[addonConfig.AddonName]

		// Handle missing addon
		if !ok {
			if triggerExist {
				// Check if the trigger time exceeds the threshold
				if isTriggerTimeExceeded(triggerVal, 10*time.Minute, logger, addonConfig.AddonName) {
					msg := fmt.Sprintf("Addon %s is not found, appbox failed to recreate it", addonConfig.AddonName)
					LogAndEmitEvent(logger, u.eventRecorder, uo, WarningEvent, msg, nil)
					return ctrl.Result{}, uo.Fail(ctx, u.infraCli, msg)
				}
				// Requeue if the trigger time is within the threshold
				LogAndEmitEvent(logger, u.eventRecorder, uo, NormalEvent, fmt.Sprintf("Addon %s is not found, waiting for appbox to create it", addonConfig.AddonName), nil)
				return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
			}

			// Set the trigger time if it doesn't exist
			LogAndEmitEvent(logger, u.eventRecorder, uo, NormalEvent, fmt.Sprintf("Addon %s is not found, waiting for appbox to create it", addonConfig.AddonName), nil)
			appBox.Annotations[triggerKey] = time.Now().Format(time.RFC3339)
			if err := u.appBoxRW.Update(ctx, appBox); err != nil {
				logger.Error(err, "unable to update appbox", "appbox", appBox.Name)
				return ctrl.Result{}, err
			}
			return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
		}
		// get the latest addon from the infra client to avoid the addon is updated by other controller
		addon := &eksv1alpha1.Addon{}
		if err := u.infraCli.Get(ctx, client.ObjectKeyFromObject(&addonItem), addon); err != nil {
			logger.Error(err, "unable to get addon", "addon", addonItem.Name)
			return ctrl.Result{}, err
		}
		// Remove trigger annotation if addon exists
		if triggerExist {
			delete(appBox.Annotations, triggerKey)
			if err := u.appBoxRW.Update(ctx, appBox); err != nil {
				logger.Error(err, "unable to update appbox", "appbox", appBox.Name)
				return ctrl.Result{}, err
			}
		}
		status := addon.Status.AtProvider.Status
		if status == nil && len(addon.Status.Conditions) == 0 {
			upgradeDone = false
			LogAndEmitEvent(logger, u.eventRecorder, uo, WarningEvent, fmt.Sprintf("Addon %s is not ready, status is empty, wait for the next reconcile", addon.Name), nil)
			continue
		}
		// if the status is nil and the addon is creating, set the status to creating
		if status == nil && u.addonIsCreating(addon) {
			status = lo.ToPtr(string(eksv1alpha1.AddonStatus_SDK_CREATING))
		}
		switch *status {
		case string(eksv1alpha1.AddonStatus_SDK_CREATING):
			upgradeDone = false
			// if the addon is already exists, fix it directly
			if u.addonIsAlreadyExists(addon) {
				LogAndEmitEvent(logger, u.eventRecorder, uo, NormalEvent, fmt.Sprintf("Addon %s cr synced failed due to exists addon, fix it", addon.Name), nil)
				if err := u.fixAddonAlreadyExists(ctx, addon, uo, appBox); err != nil {
					logger.Error(err, "unable to fix addon already exists", "addon", addon.Name)
					return ctrl.Result{}, err
				}
				// if the addon is not already exists and the pending annotation is set, clean it and trigger the addon creation
			} else if addon.GetAnnotations()[xpv1meta.AnnotationKeyExternalCreatePending] != "" {
				LogAndEmitEvent(logger, u.eventRecorder, uo, NormalEvent, fmt.Sprintf("Addon %s is creating, clean pending annotation", addon.Name), nil)
				if err := u.cleanPendingAnnotationAndTrigger(ctx, addon); err != nil {
					logger.Error(err, "unable to clean pending annotation", "addon", addon.Name)
					return ctrl.Result{}, err
				}
			}
			LogAndEmitEvent(logger, u.eventRecorder, uo, NormalEvent, fmt.Sprintf("Addon %s is creating, wait for the next reconcile", addon.Name), nil)
		case string(eksv1alpha1.AddonStatus_SDK_CREATE_FAILED),
			string(eksv1alpha1.AddonStatus_SDK_UPDATE_FAILED):
			upgradeDone = false
			LogAndEmitEvent(logger, u.eventRecorder, uo, NormalEvent, fmt.Sprintf("Addon %s is in failed state, rebuild it", addon.Name), nil)
			if err := u.handleFailedAddon(ctx, addon, appBox, addonConfig, uo); err != nil {
				logger.Error(err, "unable to handle failed addon", "addon", addon.Name)
				return ctrl.Result{}, err
			}
		case string(eksv1alpha1.AddonStatus_SDK_ACTIVE):
			if u.addonIsAlreadyExists(addon) {
				upgradeDone = false
				LogAndEmitEvent(logger, u.eventRecorder, uo, NormalEvent, fmt.Sprintf("Addon %s cr synced failed due to exists addon, fix it", addon.Name), nil)
				if err := u.fixAddonAlreadyExists(ctx, addon, uo, appBox); err != nil {
					logger.Error(err, "unable to fix addon already exists", "addon", addon.Name)
					return ctrl.Result{}, err
				}
			} else {
				LogAndEmitEvent(logger, u.eventRecorder, uo, NormalEvent, fmt.Sprintf("Addon %s is in active state", addon.Name), nil)
			}
		default:
			LogAndEmitEvent(logger, u.eventRecorder, uo, NormalEvent, fmt.Sprintf("Addon %s is in expected state", addon.Name), nil)
		}
	}

	if upgradeDone {
		LogAndEmitEvent(logger, u.eventRecorder, uo, NormalEvent, "All addons are in expected state", nil)
		appBox.Annotations[v1alpha1.AnnotationKeyUpgradeAddonsFixed] = "true"
		if err := u.appBoxRW.Update(ctx, appBox); err != nil {
			logger.Error(err, "unable to update appbox", "appbox", appBox.Name)
			return ctrl.Result{}, err
		}
		return FinishUpgradingResult(ctx, u.infraCli, u.uoWriter, uo)
	}

	// add more info and update the status
	return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
}

func (u *upgrader) addonIsCreating(addon *eksv1alpha1.Addon) bool {
	_, exists := lo.Find(addon.Status.Conditions, func(c xpv1.Condition) bool {
		return c.Type == xpv1.TypeReady && c.Status != corev1.ConditionTrue
	})
	_, scExists := lo.Find(addon.Status.Conditions, func(c xpv1.Condition) bool {
		return c.Type == xpv1.TypeSynced && c.Status != corev1.ConditionTrue
	})
	return exists && scExists
}

func (u *upgrader) addonIsAlreadyExists(addon *eksv1alpha1.Addon) bool {
	_, exists := lo.Find(addon.Status.Conditions, func(c xpv1.Condition) bool {
		return c.Type == xpv1.TypeReady && c.Status != corev1.ConditionTrue
	})
	syncedCondition, scExists := lo.Find(addon.Status.Conditions, func(c xpv1.Condition) bool {
		return c.Type == xpv1.TypeSynced && c.Status != corev1.ConditionTrue
	})
	return exists && scExists && strings.Contains(syncedCondition.Message, "Addon already exists")
}

func (u *upgrader) fixAddonAlreadyExists(ctx context.Context, addon *eksv1alpha1.Addon, uo *v1alpha1.UpgradeOperation, appBox *v1alpha1.APPBox) error {
	logger := log.FromContext(ctx)
	// addon is not ready due to the addon already exists
	// set the external name to the addon ARN to fix this issue
	LogAndEmitEvent(logger, u.eventRecorder, uo, NormalEvent, fmt.Sprintf("Addon %s is not ready, remove pending annotation", addon.Name), nil)
	if addon.GetAnnotations()[xpv1meta.AnnotationKeyExternalName] == "" {
		LogAndEmitEvent(logger, u.eventRecorder, uo, NormalEvent, fmt.Sprintf("Addon %s is not ready, update external name", addon.Name), nil)
		addonArn := lo.FromPtr(addon.Status.AtProvider.AddonARN)
		if addonArn == "" {
			LogAndEmitEvent(logger, u.eventRecorder, uo, NormalEvent, fmt.Sprintf("Addon %s is not ready, get existing addon from aws", addon.Name), nil)
			// if the addon ARN is not found, get existing addon from aws
			if addon.Spec.ProviderConfigReference == nil {
				return fmt.Errorf("addon %s has no provider config reference", addon.Name)
			}
			providerConfig := &xpawsv1beta1.ProviderConfig{
				ObjectMeta: metav1.ObjectMeta{
					Name: addon.Spec.ProviderConfigReference.Name,
				},
			}
			if err := u.infraCli.Get(ctx, client.ObjectKeyFromObject(providerConfig), providerConfig); err != nil {
				return fmt.Errorf("failed to get provider config %s: %w", addon.Spec.ProviderConfigReference.Name, err)
			}
			if providerConfig.Spec.AssumeRoleARN == nil {
				return fmt.Errorf("provider config %s has no assume role ARN", providerConfig.Name)
			}
			assumeRole := &aws.AssumedRole{RoleARN: lo.FromPtr(providerConfig.Spec.AssumeRoleARN)}
			if providerConfig.Spec.ExternalID != nil {
				assumeRole.ExternalID = providerConfig.Spec.ExternalID
			}
			if addon.Spec.ForProvider.AddonName == nil {
				return fmt.Errorf("addon %s has no addon name", addon.Name)
			}
			if appBox.Status.KubernetesClusterName == "" {
				return fmt.Errorf("appbox %s has no kubernetes cluster name", appBox.Name)
			}

			existingAddon, err := u.awsCli.EKS(assumeRole).DescribeAddon(ctx, &eks.DescribeAddonInput{
				AddonName:   addon.Spec.ForProvider.AddonName,
				ClusterName: lo.ToPtr(appBox.Status.KubernetesClusterName),
			})
			if err != nil {
				LogAndEmitEvent(logger, u.eventRecorder, uo, WarningEvent, fmt.Sprintf("Addon %s is not ready, but the addon ARN is not found", addon.Name), nil)
				return fmt.Errorf("addon %s is not ready, but the addon ARN is not found", addon.Name)
			}
			if existingAddon.Addon == nil || existingAddon.Addon.AddonArn == nil {
				return fmt.Errorf("addon %s is not ready, but the addon ARN is not found", addon.Name)
			}
			addonArn = lo.FromPtr(existingAddon.Addon.AddonArn)
		}
		if err := u.infraCli.Get(ctx, client.ObjectKeyFromObject(addon), addon); err != nil {
			return err
		}
		if addon.Annotations == nil {
			addon.Annotations = make(map[string]string)
		}
		addon.Annotations[xpv1meta.AnnotationKeyExternalName] = addonArn
		if err := u.infraCli.Update(ctx, addon); err != nil {
			logger.Error(err, "unable to update addon", "addon", addon.Name)
			return err
		}
	}
	return nil
}

// cleanPendingAnnotationAndTrigger clean the pending annotation and set the trigger time
// Crossplane-aws will ignore `meta.AnnotationKeyExternalCreatePending` annotation change, so we need to clean it and set the trigger time to trigger the addon creation
// Refer to: https://github.com/crossplane/crossplane-runtime/blob/release-1.14/pkg/resource/predicates.go#L59-L60
func (u *upgrader) cleanPendingAnnotationAndTrigger(ctx context.Context, addon *eksv1alpha1.Addon) error {
	// get the latest addon from the infra client to avoid the addon is updated by other controller
	if err := u.infraCli.Get(ctx, client.ObjectKeyFromObject(addon), addon); err != nil {
		return err
	}
	if addon.GetAnnotations()[xpv1meta.AnnotationKeyExternalCreatePending] == "" {
		return nil
	}
	delete(addon.Annotations, xpv1meta.AnnotationKeyExternalCreatePending)
	triggerKey := fmt.Sprintf("%s%s", v1alpha1.AnnotationKeyUpgradeTrigger, lo.FromPtr(addon.Spec.ForProvider.AddonName))
	addon.Annotations[triggerKey] = time.Now().UTC().Format(time.RFC3339)
	return u.infraCli.Update(ctx, addon)
}

const (
	CSIAddonName       = "aws-ebs-csi-driver"
	VPCCNIAddonName    = "vpc-cni"
	KubeProxyAddonName = "kube-proxy"
	CoreDNSAddonName   = "coredns"
)

type csiConfig struct{}

func (c *csiConfig) GetNamespace() string { return metav1.NamespaceSystem }

func (c *csiConfig) GetName() string { return "csi" }

func (c *csiConfig) ToHelmChart() (result *chart.Chart, err error) { return nil, nil }

func (c *csiConfig) ToHelmValues() map[string]interface{} { return nil }

func (c *csiConfig) GetHelmChartPath() string { return "" }

func (c *csiConfig) GetCRDs() ([]*apiextensionsv1.CustomResourceDefinition, error) { return nil, nil }

func (u *upgrader) handleFailedAddon(ctx context.Context, addon *eksv1alpha1.Addon, appbox *v1alpha1.APPBox, tplConfig config.EKSAddonConfig, uo *v1alpha1.UpgradeOperation) error {
	logger := log.FromContext(ctx)
	addonName := *addon.Spec.ForProvider.AddonName
	remote, err := u.manager.ClientGetter(ctx, client.ObjectKeyFromObject(appbox))
	if err != nil {
		return err
	}
	cli, err := remote.GetClient(ctx)
	if err != nil {
		return err
	}
	appbox.Annotations[fmt.Sprintf("%s%s", v1alpha1.AnnotationKeyUpgradeRebuildAddon, addonName)] = v1alpha1.AnnotationValueUpgradeStepRemoveResource
	if err := u.appBoxRW.Update(ctx, appbox); err != nil {
		logger.Error(err, "unable to update appbox", "appbox", appbox.Name)
		return err
	}
	switch addonName {
	case CSIAddonName:
		LogAndEmitEvent(logger, u.eventRecorder, uo, NormalEvent, fmt.Sprintf("Addon %s is in failed state, uninstall helm release", addon.GetName()), nil)
		if err := u.uninstallHelmCsi(ctx, remote); err != nil {
			return err
		}
	case VPCCNIAddonName:
		LogAndEmitEvent(logger, u.eventRecorder, uo, NormalEvent, fmt.Sprintf("Addon %s is in failed state, remove daemonset", addon.GetName()), nil)
		if err := u.removeDaemonSet(ctx, cli, client.ObjectKey{
			Name:      "aws-node",
			Namespace: metav1.NamespaceSystem,
		}); err != nil {
			return err
		}
	case KubeProxyAddonName:
		LogAndEmitEvent(logger, u.eventRecorder, uo, NormalEvent, fmt.Sprintf("Addon %s is in failed state, remove daemonset", addon.GetName()), nil)
		if err := u.removeDaemonSet(ctx, cli, client.ObjectKey{
			Name:      "kube-proxy",
			Namespace: metav1.NamespaceSystem,
		}); err != nil {
			return err
		}
	case CoreDNSAddonName:
	}

	LogAndEmitEvent(logger, u.eventRecorder, uo, NormalEvent, fmt.Sprintf("Addon %s is in failed state, rebuild addon cr", addon.GetName()), nil)
	if err := u.rebuildAddon(ctx, addon, tplConfig); err != nil {
		LogAndEmitEvent(logger, u.eventRecorder, uo, WarningEvent, fmt.Sprintf("Failed to rebuild addon %s", addon.GetName()), err)
		return err
	}
	appbox.Annotations[fmt.Sprintf("%s%s", v1alpha1.AnnotationKeyUpgradeRebuildAddon, addonName)] = v1alpha1.AnnotationValueUpgradeStepRebuildResource
	return u.appBoxRW.Update(ctx, appbox)
}

func (u *upgrader) uninstallHelmCsi(ctx context.Context, getter k8s.ClientGetter) error {
	return helm.RemoveIfExists(ctx, getter, &csiConfig{})
}

func (u *upgrader) removeDaemonSet(ctx context.Context, cli client.Client, key client.ObjectKey) error {
	return k8s.DeleteObject(ctx, cli, &appsv1.DaemonSet{
		ObjectMeta: metav1.ObjectMeta{
			Name: key.Name, Namespace: key.Namespace,
		},
	})
}

func (u *upgrader) rebuildAddon(ctx context.Context, addon *eksv1alpha1.Addon, tplConfig config.EKSAddonConfig) error {
	naddon := addon.DeepCopy()
	naddon.Spec.ForProvider.ResolveConflicts = lo.ToPtr("Overwrite")
	naddon.Spec.ForProvider.AddonVersion = lo.ToPtr(tplConfig.AddonVersion)
	naddon.Spec.ForProvider.ConfigurationValues = tplConfig.Values
	naddon.SetResourceVersion("")
	naddon.SetFinalizers(nil)

	if err := deleteAndWait(ctx, u.infraCli, addon, 5*time.Minute); err != nil {
		return fmt.Errorf("failed to delete addon %s: %w", addon.Name, err)
	}

	return k8s.Apply(ctx, u.infraCli, naddon)
}
