package k8s

import (
	"context"
	"time"

	"code.cloudfoundry.org/korifi/controllers/webhooks/validation"
	"github.com/go-logr/logr"
	k8serrors "k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/util/wait"
	"k8s.io/client-go/util/retry"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

func NewDefaultBackoff() wait.Backoff {
	return wait.Backoff{
		Duration: 5 * time.Millisecond,
		Factor:   2,
		Steps:    10,
	}
}

type RetryingClient struct {
	client.WithWatch
	predicate func(error) bool
	backoff   wait.Backoff
}

func NewRetryingClient(c client.WithWatch, predicate func(error) bool, backoff wait.Backoff) client.WithWatch {
	return RetryingClient{
		WithWatch: c,
		predicate: predicate,
		backoff:   backoff,
	}
}

func (a RetryingClient) Get(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error {
	return a.retryOnError(ctx, func() error {
		return a.WithWatch.Get(ctx, key, obj, opts...)
	}, "get")
}

func (a RetryingClient) List(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error {
	return a.retryOnError(ctx, func() error {
		return a.WithWatch.List(ctx, list, opts...)
	}, "list")
}

func (a RetryingClient) Create(ctx context.Context, obj client.Object, opts ...client.CreateOption) error {
	return a.retryOnError(ctx, func() error {
		return a.WithWatch.Create(ctx, obj, opts...)
	}, "create")
}

func (a RetryingClient) Delete(ctx context.Context, obj client.Object, opts ...client.DeleteOption) error {
	return a.retryOnError(ctx, func() error {
		return a.WithWatch.Delete(ctx, obj, opts...)
	}, "delete")
}

func (a RetryingClient) Update(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error {
	return a.retryOnError(ctx, func() error {
		return a.WithWatch.Update(ctx, obj, opts...)
	}, "update")
}

func (a RetryingClient) Patch(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.PatchOption) error {
	return a.retryOnError(ctx, func() error {
		return a.WithWatch.Patch(ctx, obj, patch, opts...)
	}, "patch")
}

func (a RetryingClient) DeleteAllOf(ctx context.Context, obj client.Object, opts ...client.DeleteAllOfOption) error {
	return a.retryOnError(ctx, func() error {
		return a.WithWatch.DeleteAllOf(ctx, obj, opts...)
	}, "deleteAllOf")
}

func (a RetryingClient) retryOnError(ctx context.Context, fn func() error, op string) error {
	count := 0
	return retry.OnError(a.backoff, a.predicate, func() error {
		logger := logr.FromContextOrDiscard(ctx).WithName("retrying-client")
		err := fn()
		if err != nil {
			count++
			logger.Info("k8s client returned error", "op", op, "count", count, "error", err)
		}
		return err
	})
}

// isForbidden returns true for forbidden errors that are NOT korifi webhook
// validation errors, false otherwise upon webhook validation errors it makes
// no sense to retry the operation as the webhook is expected to consistently
// return the same validation error
func IsForbidden(err error) bool {
	if !k8serrors.IsForbidden(err) {
		return false
	}

	if _, isValidationErr := validation.WebhookErrorToValidationError(err); isValidationErr {
		return false
	}

	return true
}
