package eks

import (
	"context"
	"fmt"
	"time"

	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/controllers"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/controllers/appbox/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/namer"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/util"
	eksv1beta1 "github.com/upbound/provider-aws/apis/eks/v1beta1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	k8serrors "k8s.io/apimachinery/pkg/util/errors"
	"k8s.io/utils/pointer"
	"k8s.io/utils/ptr"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

type Addons struct {
	*task.BaseTask
}

func NewAddons(ctx *task.Context) *Addons {
	return &Addons{BaseTask: &task.BaseTask{Context: ctx}}
}

func (t *Addons) Name() string {
	return "EKSAddons"
}

func (t *Addons) Create(ctx context.Context) deploy.Result {
	t.BeginAt = util.Pointer(time.Now())

	targetVersion := t.APPBox.Spec.KubernetesConfig.Version

	// do not try to upgrade addons if the EKS is not upgraded to the target version
	if err := CheckEKSVersionReady(ctx, t.Local, t.Cluster.GetName(), targetVersion); err != nil {
		return deploy.Error(err)
	}

	addons, versionExists := t.AddonTmplConfig.EKS[targetVersion]
	if !versionExists {
		return deploy.Error(fmt.Errorf("AddonTemplateNotFound for k8s version %s", targetVersion))
	}

	// create/update crossplane addon CR
	// TODO: whether to remove addons CR that are not present in current template.
	errList := make([]error, 0)
	for _, addonConf := range addons {
		addonName := addonConf.AddonName
		addonCRName := controllers.UniqueIdentifier(t.APPBox.ObjectMeta, namer.WithPrefix(addonName), namer.WithPrefix(t.Config.ResourceNamePrefix))
		existing := &eksv1beta1.Addon{}
		if err := t.Local.Get(ctx, client.ObjectKey{Name: addonCRName}, existing); client.IgnoreNotFound(err) != nil {
			return deploy.Error(fmt.Errorf("get addon %s: %v", addonCRName, err))
		}
		addon := &eksv1beta1.Addon{
			ObjectMeta: metav1.ObjectMeta{
				Name:   addonCRName,
				Labels: t.Namer.Labels(),
			},
			Spec: eksv1beta1.AddonSpec{
				ForProvider: eksv1beta1.AddonParameters{
					AddonName:           pointer.String(addonConf.AddonName),
					AddonVersion:        pointer.String(addonConf.AddonVersion),
					Region:              ptr.To(t.APPBox.Spec.Region),
					ClusterName:         ptr.To(t.Cluster.GetName()),
					ConfigurationValues: addonConf.Values,
				},
				ResourceSpec: xpv1.ResourceSpec{
					ProviderConfigReference: &xpv1.Reference{Name: t.ProviderConfigName()},
					ManagementPolicies:      controllers.GetResourceManagementPolicies(),
				},
			},
		}

		// Keep the existing resolve conflicts setting if the addon is already created.
		// This is a workaround for the issue that the addon config is not compatible with the existing addon
		// For some migrated appbox, the addon config is not compatible with the existing addon.
		// and ResolveConflicts will be set to `overwrite` when to update the eks.
		if existing.Spec.ForProvider.ResolveConflicts != nil {
			addon.Spec.ForProvider.ResolveConflicts = existing.Spec.ForProvider.ResolveConflicts
		}

		if err := k8s.Apply(ctx, t.Local, addon); err != nil {
			errList = append(errList, fmt.Errorf("create or update addon %s: %v", addon.Name, err))
		}
	}

	return deploy.ErrorOrDone(k8serrors.NewAggregate(errList))
}

// for migrated appbox we do not check status for addon aws-ebs-csi-driver
func (t *Addons) PostCreate(ctx context.Context) deploy.Result {
	addons, exist := t.AddonTmplConfig.EKS[t.APPBox.Spec.KubernetesConfig.Version]
	if !exist {
		return deploy.Error(fmt.Errorf("AddonTemplateNotFound for k8s version %s", t.APPBox.Spec.KubernetesConfig.Version))
	}
	skipAddonCSI := t.APPBox.Annotations[v1alpha1.AnnotationKeyLegacyAPPBoxSkipAddonAWSEBSCSIDriver] == "true"
	skipAddonVPCCNI := t.APPBox.Annotations[v1alpha1.AnnotationKeyLegacyAPPBoxSkipAddonVPCCNI] == "true"
	skipCoreDNS := t.APPBox.Annotations[v1alpha1.AnnotationKeyLegacyAPPBoxSkipAddonCoreDNS] == "true"
	skipKubeProxy := t.APPBox.Annotations[v1alpha1.AnnotationKeyLegacyAPPBoxSkipAddonKubeProxy] == "true"
	addonStatus := []v1alpha1.AddonStatus{}
	for _, addonConf := range addons {
		checkStatus := true
		if skipAddonCSI && addonConf.AddonName == "aws-ebs-csi-driver" {
			checkStatus = false
		}
		if skipAddonVPCCNI && addonConf.AddonName == "vpc-cni" {
			checkStatus = false
		}
		if skipCoreDNS && addonConf.AddonName == "coredns" {
			checkStatus = false
		}
		if skipKubeProxy && addonConf.AddonName == "kube-proxy" {
			checkStatus = false
		}
		addon := &eksv1beta1.Addon{
			ObjectMeta: metav1.ObjectMeta{
				Name: controllers.UniqueIdentifier(t.APPBox.ObjectMeta, namer.WithPrefix(addonConf.AddonName), namer.WithPrefix(t.Config.ResourceNamePrefix)),
			},
		}
		if err := t.Local.Get(ctx, client.ObjectKeyFromObject(addon), addon); err != nil {
			return deploy.Error(err)
		}
		if checkStatus {
			if !controllers.IsResourceReady(addon.Status.ResourceStatus) {
				return deploy.Error(fmt.Errorf("addon components %s is not active", addon.Name))
			}
			if err := CheckAddonVersionReady(addon, addonConf.AddonVersion); err != nil {
				return deploy.Error(err)
			}
		}
		addonStatus = append(addonStatus, v1alpha1.AddonStatus{
			Name:    *addon.Spec.ForProvider.AddonName,
			Version: *addon.Spec.ForProvider.AddonVersion,
		})
	}
	t.APPBox.Status.Addons = addonStatus
	return deploy.Done()
}

func (t *Addons) Delete(ctx context.Context) deploy.Result {
	addons := &eksv1beta1.AddonList{}
	ops := k8s.ListOpts[*eksv1beta1.Addon]{
		ObjectList: addons,
		Unpack:     func() []*eksv1beta1.Addon { return k8s.MapPtr(addons.Items) },
	}

	addonList, err := ops.List(ctx, t.Local, client.MatchingLabels(t.Namer.Labels()))
	if err != nil {
		return deploy.Error(err)
	}

	for _, addon := range addonList {
		addon.Spec.ResourceSpec.ManagementPolicies = xpv1.ManagementPolicies{xpv1.ManagementActionAll}
		if err := k8s.PatchObject(ctx, t.Local, addon, controllers.ManagementPoliciesPatch); client.IgnoreNotFound(err) != nil {
			return deploy.Error(err)
		}
	}
	err = ops.DeleteFilterBy(ctx, t.Local, func(addon *eksv1beta1.Addon) bool {
		return true
	}, client.MatchingLabels(t.Namer.Labels()))

	if err != nil {
		return deploy.Error(fmt.Errorf("delete addons failed: %v", err))
	}

	return deploy.Done()
}

func (t *Addons) PostDelete(ctx context.Context) deploy.Result {
	addonList := &eksv1beta1.AddonList{}
	ops := k8s.ListOpts[*eksv1beta1.Addon]{
		ObjectList: addonList,
		Unpack:     func() []*eksv1beta1.Addon { return k8s.MapPtr(addonList.Items) },
	}

	addons, err := ops.List(ctx, t.Local, client.MatchingLabels(t.Namer.Labels()))
	if err != nil {
		return deploy.Error(err)
	}
	if len(addons) > 0 {
		return deploy.Error(fmt.Errorf("there are %d addons still exist", len(addons)))
	}
	return deploy.Result{}
}

// CheckAddonVersionReady checks if the addon version is ready.
// It returns an error if the addon is currently upgrading (Spec.Version != Status.Version)
// NOTE: this function does not check if the addon is active.
func CheckAddonVersionReady(addon *eksv1beta1.Addon, targetVersion string) error {
	if addon.Spec.ForProvider.AddonVersion != nil && addon.Status.AtProvider.AddonVersion != nil &&
		*addon.Status.AtProvider.AddonVersion != *addon.Spec.ForProvider.AddonVersion {
		return fmt.Errorf("addon %s is upgrading from version %s to %s, waiting for upgrade to complete",
			*addon.Spec.ForProvider.AddonName, *addon.Status.AtProvider.AddonVersion, *addon.Spec.ForProvider.AddonVersion)
	}

	if addon.Status.AtProvider.AddonVersion != nil && *addon.Status.AtProvider.AddonVersion != targetVersion {
		return fmt.Errorf("addon %s version %s is not ready for target version %s, waiting for addon upgrade",
			*addon.Spec.ForProvider.AddonName, *addon.Status.AtProvider.AddonVersion, targetVersion)
	}

	return nil
}
