package utils

import (
	"context"
	stderrors "errors"
	"math"
	"math/rand"
	"time"

	"github.com/go-logr/logr"
	"github.com/tidbcloud/aws-shared-provider/pkg/metrics"
	"golang.org/x/exp/constraints"
	"k8s.io/apimachinery/pkg/util/errors"
	"sigs.k8s.io/controller-runtime/pkg/reconcile"
)

func Max[T constraints.Ordered](a, b T) T {
	if a > b {
		return a
	}
	return b
}

func Min[T constraints.Ordered](a, b T) T {
	if a < b {
		return a
	}
	return b
}

func RandSleep(s int) {
	time.Sleep(time.Duration(float64(rand.Intn(s*10)) / 10 * float64(time.Second)))
}

func SleepWithContext(ctx context.Context, duration time.Duration) {
	select {
	case <-time.After(duration):
	case <-ctx.Done():
	}
}

// IgnoreRequeue collects *utils.RequeueAfterError into accum via errors.Join,
// and returns a non-requeue (fatal) error as-is.
func IgnoreRequeue(reqErrors *error, err error) error {
	if err == nil {
		return nil
	}
	var rq *RequeueAfterError
	if stderrors.As(err, &rq) { // include wrapped requeue errors too
		if reqErrors != nil {
			*reqErrors = stderrors.Join(*reqErrors, err)
		}
		return nil
	}
	return err
}

// ReconcileErr return relate reconcile.Result and non error if err only contains RequeueAfterError.
// Otherwise, return the origin err.
func ReconcileErr(logger logr.Logger, controllerName string, resourceName string, err error) (reconcile.Result, error) {
	result, reconcileErr := resolveReconcileErr(logger, err)
	if reconcileErr != nil {
		metrics.ObserveReconcileFailure(resourceName, controllerName)
	} else if !result.Requeue && result.RequeueAfter == 0 {
		metrics.ClearReconcileFailure(resourceName)
	}
	return result, reconcileErr
}

func resolveReconcileErr(logger logr.Logger, err error) (reconcile.Result, error) {
	// single error
	if requeueAfter, ok := err.(*RequeueAfterError); ok {
		if requeueAfter.Cause != nil {
			logger.Info("re-queue resource", "after", requeueAfter.RequeueAfter, "cause", requeueAfter.Cause.Error())
		}
		return reconcile.Result{Requeue: true, RequeueAfter: requeueAfter.RequeueAfter}, nil
	}

	// an error list is raised, recursively resolve each err
	if errList, ok := err.(errors.Aggregate); ok {
		minRequeue := time.Duration(math.MaxInt64)
		for _, errItem := range errList.Errors() {
			result, err2 := resolveReconcileErr(logger, errItem)
			if err2 != nil {
				return reconcile.Result{}, err
			}

			// use the minimum requeue interval requested
			if result.RequeueAfter < minRequeue {
				minRequeue = result.RequeueAfter
			}
		}
		return reconcile.Result{Requeue: true, RequeueAfter: minRequeue}, nil
	}
	// an Unwrap error list(like errors.Join/multierr) is raised, recursively resolve each err
	if u, ok := err.(interface{ Unwrap() []error }); ok {
		minRequeue := time.Duration(math.MaxInt64)
		for _, errItem := range u.Unwrap() {
			result, err2 := resolveReconcileErr(logger, errItem)
			if err2 != nil {
				return reconcile.Result{}, err
			}

			// use the minimum requeue interval requested
			if result.RequeueAfter < minRequeue {
				minRequeue = result.RequeueAfter
			}
		}
		return reconcile.Result{Requeue: true, RequeueAfter: minRequeue}, nil
	}
	// an Unwrap error is raised, recursively resolve each err
	if u, ok := err.(interface{ Unwrap() error }); ok {
		result, err2 := resolveReconcileErr(logger, u.Unwrap())
		if err2 != nil {
			return reconcile.Result{}, err
		}
		return reconcile.Result{Requeue: true, RequeueAfter: result.RequeueAfter}, nil
	}

	return reconcile.Result{}, err
}
