package k8s

import (
	"context"

	"k8s.io/apimachinery/pkg/api/equality"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
)

// UpdateHook is used to patch the object and achieve atomic operation when the object
// needs to be updated
type UpdateHook func() error

// CreateOrUpdate creates or updates an object. Before updating an object, the update hook will patch the object
// before calling 'Update', which will achieve atomic operation.
func CreateOrUpdate(ctx context.Context, c client.Client, obj client.Object, f controllerutil.MutateFn,
	updateHooks ...UpdateHook) (controllerutil.OperationResult, error) {
	key := client.ObjectKeyFromObject(obj)
	if err := c.Get(ctx, key, obj); err != nil {
		if !apierrors.IsNotFound(err) {
			return controllerutil.OperationResultNone, err
		}
		if err := f(); err != nil {
			return controllerutil.OperationResultNone, err
		}
		if err := c.Create(ctx, obj); err != nil {
			return controllerutil.OperationResultNone, err
		}
		return controllerutil.OperationResultCreated, nil
	}

	existing := obj.DeepCopyObject() //nolint
	if err := f(); err != nil {
		return controllerutil.OperationResultNone, err
	}

	if equality.Semantic.DeepEqual(existing, obj) {
		return controllerutil.OperationResultNone, nil
	}

	if len(updateHooks) > 0 {
		for _, uk := range updateHooks {
			if err := uk(); err != nil {
				return controllerutil.OperationResultNone, err
			}
		}
		// check again to avoid the false update
		if equality.Semantic.DeepEqual(existing, obj) {
			return controllerutil.OperationResultNone, nil
		}
	}

	if err := c.Update(ctx, obj); err != nil {
		return controllerutil.OperationResultNone, err
	}

	return controllerutil.OperationResultUpdated, nil
}
