package common

import (
	"context"
	"time"

	"github.com/crossplane/crossplane-runtime/pkg/meta"
	"github.com/go-logr/logr"
	"github.com/pingcap/errors"
	"go.uber.org/multierr"
	"golang.org/x/time/rate"
	corev1 "k8s.io/api/core/v1"
	k8serrors "k8s.io/apimachinery/pkg/util/errors"
	"k8s.io/client-go/util/workqueue"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
	"sigs.k8s.io/controller-runtime/pkg/reconcile"

	reconcileerr "github.com/tidbcloud/infra-api/controllers/common/error"
)

// EnsureFinalizers ensures the given finalizer exists in the object
func EnsureFinalizers(ctx context.Context, c client.Client, o client.Object, finalizerNames ...string) error {
	for _, finalizerName := range finalizerNames {
		if !controllerutil.ContainsFinalizer(o, finalizerName) {
			controllerutil.AddFinalizer(o, finalizerName)
		}
	}
	return c.Update(ctx, o)
}

// AtLeastOnce ensures an action is performed at least once for the target object, using annotations as checkpoint,
// caller is expected to retry AtLeastOnce if any error returned
func AtLeastOnce(ctx context.Context, c client.Client, o client.Object, actionKey string, action func() error) error {
	// see if we have ever performed this action on the object
	if _, ok := o.GetAnnotations()[actionKey]; ok {
		return nil
	}
	if err := action(); err != nil {
		return err
	}
	// checkpoint action completion to annotations
	meta.AddAnnotations(o, map[string]string{
		actionKey: string(corev1.ConditionTrue),
	})
	if err := c.Update(ctx, o); err != nil {
		return err
	}
	return nil
}

func ReconcileErr(logger logr.Logger, err error) (reconcile.Result, error) {
	// an error list is raised, recursively resolve each err
	// multierr does not export an interface
	errList := multierr.Errors(err)
	if len(errList) == 1 {
		err = errList[0]
		errList = []error{}
	}
	if len(errList) == 0 {
		if errAgg, ok := err.(k8serrors.Aggregate); ok {
			errList = errAgg.Errors()
		}
	}

	if len(errList) > 0 && (errList[0] != err || len(errList) > 1) {
		minimumRequeue := 0 * time.Minute
		for _, errItem := range errList {
			result, err := ReconcileErr(logger, errItem)
			if err != nil {
				return reconcile.Result{}, err
			}
			// use the minimum requeue interval requested
			if result.Requeue && (minimumRequeue == 0 || result.RequeueAfter < minimumRequeue) {
				minimumRequeue = result.RequeueAfter
			}
		}

		return reconcile.Result{Requeue: true, RequeueAfter: minimumRequeue}, nil
	}

	findRequeue := func(e error) bool {
		_, ok := e.(*reconcileerr.RequeueAfterError)
		return ok
	}
	if requeuedErr := errors.Find(err, findRequeue); requeuedErr != nil {
		requeueAfter := requeuedErr.(*reconcileerr.RequeueAfterError)
		if requeueAfter.Caused != nil {
			logger.Info("requeque", "cause", requeueAfter.Caused.Error(), "after", requeueAfter.RequeueAfter)
		}
		return reconcile.Result{Requeue: true, RequeueAfter: requeueAfter.RequeueAfter}, nil
	}

	if err != nil {
		logger.Info("requeque", "err", err.Error())
	}

	return reconcile.Result{}, err
}

func DefaultRateLimiter() workqueue.TypedRateLimiter[reconcile.Request] {
	return workqueue.NewTypedMaxOfRateLimiter(
		// capped with 1 minutes
		workqueue.NewTypedItemExponentialFailureRateLimiter[reconcile.Request](5*time.Millisecond, 1*time.Minute),
		// 50 qps, 500 bucket size.  This is only for retry speed and its only the overall factor (not per item)
		&workqueue.TypedBucketRateLimiter[reconcile.Request]{Limiter: rate.NewLimiter(rate.Limit(50), 500)},
	)
}
