/*
Copyright © 2022 - 2025 SUSE LLC

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package controllers

import (
	"bytes"
	"context"
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"time"

	"github.com/google/go-cmp/cmp"
	"github.com/rancher/yip/pkg/schema"
	corev1 "k8s.io/api/core/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/api/meta"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	errorutils "k8s.io/apimachinery/pkg/util/errors"
	"k8s.io/utils/ptr"
	ipamv1 "sigs.k8s.io/cluster-api/exp/ipam/api/v1beta1"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
	"sigs.k8s.io/controller-runtime/pkg/event"
	"sigs.k8s.io/controller-runtime/pkg/predicate"
	"sigs.k8s.io/controller-runtime/pkg/reconcile"

	"gopkg.in/yaml.v3"

	elementalv1 "github.com/rancher/elemental-operator/api/v1beta1"
	systemagent "github.com/rancher/elemental-operator/internal/system-agent"
	"github.com/rancher/elemental-operator/pkg/log"
	"github.com/rancher/elemental-operator/pkg/network"
	"github.com/rancher/elemental-operator/pkg/util"
)

// Timeout to validate machine inventory adoption
const adoptionTimeout = 5

const LocalResetPlanPath = "/oem/reset-cloud-config.yaml"
const LocalResetUnmanagedMarker = "/var/lib/elemental/.unmanaged_reset"

// MachineInventoryReconciler reconciles a MachineInventory object.
type MachineInventoryReconciler struct {
	client.Client
}

// +kubebuilder:rbac:groups=elemental.cattle.io,resources=machineinventories,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=elemental.cattle.io,resources=machineinventories/status,verbs=get;update;patch
// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;watch;create;list
// +kubebuilder:rbac:groups="ipam.cluster.x-k8s.io",resources=ipaddresses,verbs=get;list;watch
// +kubebuilder:rbac:groups="ipam.cluster.x-k8s.io",resources=ipaddresseclaims,verbs=get;list;watch;delete

func (r *MachineInventoryReconciler) SetupWithManager(mgr ctrl.Manager) error {
	return ctrl.NewControllerManagedBy(mgr).
		For(&elementalv1.MachineInventory{}).
		Owns(&corev1.Secret{}).
		WithEventFilter(r.ignoreIncrementalStatusUpdate()).
		Complete(r)
}

func (r *MachineInventoryReconciler) Reconcile(ctx context.Context, req reconcile.Request) (ctrl.Result, error) { //nolint:dupl
	logger := ctrl.LoggerFrom(ctx)

	mInventory := &elementalv1.MachineInventory{}
	err := r.Get(ctx, req.NamespacedName, mInventory)
	if err != nil {
		if apierrors.IsNotFound(err) {
			logger.V(log.DebugDepth).Info("Object was not found, not an error")
			return reconcile.Result{}, nil
		}
		return reconcile.Result{}, fmt.Errorf("failed to get machine inventory object: %w", err)
	}

	// Ensure we patch the latest version otherwise we could erratically overlap with other controllers (e.g. backup and restore)
	patchBase := client.MergeFromWithOptions(mInventory.DeepCopy(), client.MergeFromWithOptimisticLock{})

	// We have to sanitize the conditions because old API definitions didn't have proper validation.
	mInventory.Status.Conditions = util.RemoveInvalidConditions(mInventory.Status.Conditions)

	// Collect errors as an aggregate to return together after all patches have been performed.
	var errs []error

	result, err := r.reconcile(ctx, mInventory)
	if err != nil {
		errs = append(errs, fmt.Errorf("error reconciling machine inventory object: %w", err))
	}

	machineInventoryStatusCopy := mInventory.Status.DeepCopy() // Patch call will erase the status

	if err := r.Patch(ctx, mInventory, patchBase); err != nil {
		errs = append(errs, fmt.Errorf("failed to patch machine inventory object: %w", err))
	}

	mInventory.Status = *machineInventoryStatusCopy

	// If the object was waiting for deletion and we just removed the finalizer, we will get a not found error
	if err := r.Status().Patch(ctx, mInventory, patchBase); err != nil && !apierrors.IsNotFound(err) {
		errs = append(errs, fmt.Errorf("failed to patch status for machine inventory object: %w", err))
	}

	if len(errs) > 0 {
		return ctrl.Result{}, errorutils.NewAggregate(errs)
	}

	return result, nil
}

func (r *MachineInventoryReconciler) reconcile(ctx context.Context, mInventory *elementalv1.MachineInventory) (ctrl.Result, error) {
	logger := ctrl.LoggerFrom(ctx)

	logger.Info("Reconciling machineinventory object")

	if mInventory.GetDeletionTimestamp() == nil || mInventory.GetDeletionTimestamp().IsZero() {
		// The object is not being deleted, so register the finalizer
		if !controllerutil.ContainsFinalizer(mInventory, elementalv1.MachineInventoryFinalizer) {
			controllerutil.AddFinalizer(mInventory, elementalv1.MachineInventoryFinalizer)
			return ctrl.Result{RequeueAfter: time.Second}, nil
		}
	} else {
		// The object is up for deletion
		if controllerutil.ContainsFinalizer(mInventory, elementalv1.MachineInventoryFinalizer) {
			if err := r.reconcileResetPlanSecret(ctx, mInventory); err != nil {
				meta.SetStatusCondition(&mInventory.Status.Conditions, metav1.Condition{
					Type:    elementalv1.ReadyCondition,
					Reason:  elementalv1.PlanFailureReason,
					Status:  metav1.ConditionFalse,
					Message: err.Error(),
				})
				return ctrl.Result{}, fmt.Errorf("reconciling reset plan secret: %w", err)
			}
			return ctrl.Result{}, nil
		}
		return ctrl.Result{}, nil
	}

	if err := r.createPlanSecret(ctx, mInventory); err != nil {
		meta.SetStatusCondition(&mInventory.Status.Conditions, metav1.Condition{
			Type:    elementalv1.ReadyCondition,
			Reason:  elementalv1.PlanCreationFailureReason,
			Status:  metav1.ConditionFalse,
			Message: err.Error(),
		})
		return ctrl.Result{}, fmt.Errorf("failed to create plan secret: %w", err)
	}

	if r.networkNeedsReconcile(*mInventory) {
		meta.SetStatusCondition(&mInventory.Status.Conditions, metav1.Condition{
			Type:    elementalv1.ReadyCondition,
			Reason:  elementalv1.ReconcilingNetworkConfig,
			Status:  metav1.ConditionFalse,
			Message: "NetworkConfig needs reconcile",
		})
		result, err := r.reconcileNetworkConfig(ctx, mInventory)
		if err != nil {
			meta.SetStatusCondition(&mInventory.Status.Conditions, metav1.Condition{
				Type:    elementalv1.ReadyCondition,
				Reason:  elementalv1.NetworkConfigFailure,
				Status:  metav1.ConditionFalse,
				Message: err.Error(),
			})
			return ctrl.Result{}, fmt.Errorf("reconciling network config: %w", err)
		}
		return result, nil
	}
	meta.SetStatusCondition(&mInventory.Status.Conditions, metav1.Condition{
		Type:    elementalv1.NetworkConfigReady,
		Reason:  elementalv1.ReconcilingNetworkConfig,
		Status:  metav1.ConditionTrue,
		Message: "NetworkConfig is ready",
	})

	if err := r.updateInventoryWithPlanStatus(ctx, mInventory); err != nil {
		meta.SetStatusCondition(&mInventory.Status.Conditions, metav1.Condition{
			Type:    elementalv1.ReadyCondition,
			Reason:  elementalv1.PlanFailureReason,
			Status:  metav1.ConditionFalse,
			Message: err.Error(),
		})
		return ctrl.Result{}, fmt.Errorf("failed to update inventory status with plan %w", err)
	}

	if requeue, err := r.updateInventoryWithAdoptionStatus(ctx, mInventory); err != nil {
		meta.SetStatusCondition(&mInventory.Status.Conditions, metav1.Condition{
			Type:    elementalv1.AdoptionReadyCondition,
			Reason:  elementalv1.AdoptionFailureReason,
			Status:  metav1.ConditionFalse,
			Message: err.Error(),
		})
		return ctrl.Result{}, fmt.Errorf("failed to update inventory status with plan %w", err)
	} else if requeue {
		return ctrl.Result{RequeueAfter: time.Second}, nil
	}

	return ctrl.Result{}, nil
}

func (r *MachineInventoryReconciler) reconcileResetPlanSecret(ctx context.Context, mInventory *elementalv1.MachineInventory) error {
	logger := ctrl.LoggerFrom(ctx)

	logger.Info("Reconciling Reset plan")

	resettable, resettableFound := mInventory.Annotations[elementalv1.MachineInventoryResettableAnnotation]
	if !resettableFound || resettable != "true" {
		logger.V(log.DebugDepth).Info("Machine Inventory does not need reset. Removing finalizer.")
		controllerutil.RemoveFinalizer(mInventory, elementalv1.MachineInventoryFinalizer)
		return nil
	}

	if mInventory.Status.Plan == nil || mInventory.Status.Plan.PlanSecretRef == nil {
		return errors.New("machine inventory plan secret does not exist")
	}

	planSecret := &corev1.Secret{}
	if err := r.Get(ctx, types.NamespacedName{
		Namespace: mInventory.Status.Plan.PlanSecretRef.Namespace,
		Name:      mInventory.Status.Plan.PlanSecretRef.Name,
	}, planSecret); err != nil {
		return fmt.Errorf("getting plan secret: %w", err)
	}

	if !util.IsObjectOwned(&planSecret.ObjectMeta, mInventory.UID) {
		return fmt.Errorf("secret already exists and was not created by this controller")
	}

	planType, annotationFound := planSecret.Annotations[elementalv1.PlanTypeAnnotation]

	if !annotationFound || planType != elementalv1.PlanTypeReset {
		logger.V(log.DebugDepth).Info("Non reset plan type found. Updating it with new reset plan.")
		return r.updatePlanSecretWithReset(ctx, mInventory, planSecret)
	}

	logger.V(log.DebugDepth).Info("Reset plan type found. Updating status and determine whether it was successfully applied.")
	if err := r.updateInventoryWithPlanStatus(ctx, mInventory); err != nil {
		return fmt.Errorf("updating inventory with plan status: %w", err)
	}
	if mInventory.Status.Plan.State == elementalv1.PlanApplied {
		logger.V(log.DebugDepth).Info("Reset plan was successfully applied.")
		controllerutil.RemoveFinalizer(mInventory, elementalv1.MachineInventoryFinalizer)
	}

	return nil
}

func (r *MachineInventoryReconciler) updatePlanSecretWithReset(ctx context.Context, mInventory *elementalv1.MachineInventory, planSecret *corev1.Secret) error {
	logger := ctrl.LoggerFrom(ctx)

	logger.Info("Updating Secret with Reset plan")

	var checksum string
	var resetPlan []byte
	var err error

	networkNeedsReset := mInventory.Spec.Network.Configurator != network.ConfiguratorNone

	unmanaged, unmanagedFound := mInventory.Annotations[elementalv1.MachineInventoryOSUnmanagedAnnotation]
	if unmanagedFound && unmanaged == "true" {
		checksum, resetPlan, err = r.newUnmanagedResetPlan(ctx)
	} else {
		checksum, resetPlan, err = r.newResetPlan(ctx, networkNeedsReset)
	}

	if err != nil {
		return fmt.Errorf("getting new reset plan: %w", err)
	}

	patchBase := client.MergeFrom(planSecret.DeepCopy())

	planSecret.Data["applied-checksum"] = []byte("")
	planSecret.Data["failed-checksum"] = []byte("")
	planSecret.Data["plan"] = resetPlan
	planSecret.Annotations = map[string]string{elementalv1.PlanTypeAnnotation: elementalv1.PlanTypeReset}

	if err := r.Patch(ctx, planSecret, patchBase); err != nil {
		return fmt.Errorf("patching plan secret: %w", err)
	}

	// Clear the plan status
	mInventory.Status.Plan = &elementalv1.PlanStatus{
		Checksum: checksum,
		PlanSecretRef: &corev1.ObjectReference{
			Namespace: planSecret.Namespace,
			Name:      planSecret.Name,
		},
	}

	return nil
}

// networkNeedsReconcile checks if there is an IPAddress for each IPPool referenced by the MachineInventory.
func (r *MachineInventoryReconciler) networkNeedsReconcile(mInventory elementalv1.MachineInventory) bool {
	for ipName := range mInventory.Spec.IPAddressPools {
		_, ipExists := mInventory.Spec.Network.IPAddresses[ipName]
		if !ipExists {
			return true
		}
	}
	return false
}

func (r *MachineInventoryReconciler) reconcileNetworkConfig(ctx context.Context, mInventory *elementalv1.MachineInventory) (ctrl.Result, error) {
	logger := ctrl.LoggerFrom(ctx)
	logger.Info("Reconciling Network Config")

	// Loops over the IPAddressPools and create an IPAddressClaim for each
	for name, ipPoolRef := range mInventory.Spec.IPAddressPools {
		ipClaimName := fmt.Sprintf("%s-%s", mInventory.Name, name)
		ipClaim := &ipamv1.IPAddressClaim{
			ObjectMeta: metav1.ObjectMeta{
				Name:      ipClaimName,
				Namespace: mInventory.Namespace,
				// Ownership takes care of IPAddressClaim deletion when the MachineInventory is also deleted.
				OwnerReferences: []metav1.OwnerReference{
					{
						APIVersion: mInventory.APIVersion,
						Kind:       mInventory.Kind,
						Name:       mInventory.Name,
						UID:        mInventory.UID,
						Controller: ptr.To(true),
					},
				},
			},
			Spec: ipamv1.IPAddressClaimSpec{
				PoolRef: *ipPoolRef,
			},
		}
		if err := r.Create(ctx, ipClaim); apierrors.IsAlreadyExists(err) {
			logger.Info("Reusing already existing IPAddressClaim", "IPAddressClaim", ipClaimName)
		} else if err != nil {
			return ctrl.Result{}, fmt.Errorf("creating IPAddressClaim '%s': %w", ipClaimName, err)
		}

		if err := r.Get(ctx, client.ObjectKeyFromObject(ipClaim), ipClaim); err != nil {
			return ctrl.Result{}, fmt.Errorf("getting IPAddressClaim '%s': %w", ipClaimName, err)
		}
		// Just for safety, prevent usage of any IPClaim that is undergoing deletion (we don't have an IPAddressClaim watch for a further reconcile)
		if !ipClaim.DeletionTimestamp.IsZero() {
			return ctrl.Result{}, fmt.Errorf("Waiting for IPAddressClaim deletion '%s'", ipClaimName)
		}

		if mInventory.Spec.IPAddressClaims == nil {
			mInventory.Spec.IPAddressClaims = map[string]*corev1.ObjectReference{}
		}

		mInventory.Spec.IPAddressClaims[name] = &corev1.ObjectReference{
			APIVersion: ipClaim.APIVersion,
			Kind:       ipClaim.Kind,
			Name:       ipClaim.Name,
			Namespace:  ipClaim.Namespace,
			UID:        ipClaim.UID,
		}
	}

	// Loops over the IPAddressClaims and get the IPAddresses
	for name, ipClaimRef := range mInventory.Spec.IPAddressClaims {
		ipAddress := &ipamv1.IPAddress{}
		err := r.Get(ctx, types.NamespacedName{
			Name:      ipClaimRef.Name,
			Namespace: ipClaimRef.Namespace,
		}, ipAddress)
		if apierrors.IsNotFound(err) {
			logger.Info("IPAddress not found. Requeuing.", "IPAddress", ipClaimRef.Name)
			meta.SetStatusCondition(&mInventory.Status.Conditions, metav1.Condition{
				Type:    elementalv1.NetworkConfigReady,
				Reason:  elementalv1.WaitingForIPAddressReason,
				Status:  metav1.ConditionFalse,
				Message: fmt.Sprintf("Waiting to claim IPAddress %s", ipClaimRef.Name),
			})
			return ctrl.Result{RequeueAfter: time.Second}, nil
		}
		if err != nil {
			return ctrl.Result{}, fmt.Errorf("getting IPAddress '%s': %w", ipClaimRef.Name, err)
		}

		if mInventory.Spec.Network.IPAddresses == nil {
			mInventory.Spec.Network.IPAddresses = map[string]string{}
		}

		mInventory.Spec.Network.IPAddresses[name] = ipAddress.Spec.Address
	}

	return ctrl.Result{}, nil
}

func (r *MachineInventoryReconciler) newResetPlan(ctx context.Context, resetNetwork bool) (string, []byte, error) {
	logger := ctrl.LoggerFrom(ctx)

	logger.Info("Creating new Reset plan secret")

	// This is the local cloud-config that the elemental-system-agent will run while in recovery mode
	resetCloudConfig := schema.YipConfig{
		Name: "Elemental Reset",
		Stages: map[string][]schema.Stage{
			"network.after": {
				schema.Stage{
					If:   "[ -f /run/cos/recovery_mode ] || [ -f /run/elemental/recovery_mode ]",
					Name: "Runs elemental reset",
					Commands: []string{
						"systemctl start elemental-register-reset",
					},
				},
			},
		},
	}

	resetCloudConfigBytes, err := yaml.Marshal(resetCloudConfig)
	if err != nil {
		return "", nil, fmt.Errorf("marshalling local reset cloud-config to yaml: %w", err)
	}

	resetInstructions := []systemagent.OneTimeInstruction{}

	if resetNetwork {
		resetInstructions = append(resetInstructions, systemagent.OneTimeInstruction{
			CommonInstruction: systemagent.CommonInstruction{
				Name:    "restore first boot config",
				Command: "elemental-register",
				Args:    []string{"--debug", "--reset-network"},
			},
		})
	}

	resetInstructions = append(resetInstructions, systemagent.OneTimeInstruction{
		CommonInstruction: systemagent.CommonInstruction{
			Name:    "configure next boot to recovery mode",
			Command: "grub2-editenv",
			Args: []string{
				"/oem/grubenv",
				"set",
				"next_entry=recovery",
			},
		},
	})
	resetInstructions = append(resetInstructions, systemagent.OneTimeInstruction{
		CommonInstruction: systemagent.CommonInstruction{
			Name:    "schedule reboot",
			Command: "shutdown",
			Args: []string{
				"-r",
				"+1", // Need to have time to confirm plan execution before rebooting
			},
		},
	})

	// This is the remote plan that should trigger the reboot into recovery and reset
	resetPlan := systemagent.Plan{
		Files: []systemagent.File{
			{
				Content:     base64.StdEncoding.EncodeToString(resetCloudConfigBytes),
				Path:        LocalResetPlanPath,
				Permissions: "0600",
			},
		},
		OneTimeInstructions: resetInstructions,
	}

	var buf bytes.Buffer
	if err := json.NewEncoder(&buf).Encode(resetPlan); err != nil {
		return "", nil, fmt.Errorf("failed to encode plan: %w", err)
	}

	plan := buf.Bytes()

	checksum := util.PlanChecksum(plan)

	return checksum, plan, nil
}

func (r *MachineInventoryReconciler) newUnmanagedResetPlan(ctx context.Context) (string, []byte, error) {
	logger := ctrl.LoggerFrom(ctx)

	logger.Info("Creating new Unmanaged Reset plan secret")

	// This is the remote plan that should trigger the creation of a node reset marker with current timestamp
	timeStamp := time.Now().Format("2006-01-02 15:04:05")
	resetPlan := systemagent.Plan{
		Files: []systemagent.File{
			{
				Content:     base64.StdEncoding.EncodeToString([]byte(timeStamp)),
				Path:        LocalResetUnmanagedMarker,
				Permissions: "0600",
			},
		},
	}

	var buf bytes.Buffer
	if err := json.NewEncoder(&buf).Encode(resetPlan); err != nil {
		return "", nil, fmt.Errorf("failed to encode plan: %w", err)
	}

	plan := buf.Bytes()

	checksum := util.PlanChecksum(plan)

	return checksum, plan, nil
}

func (r *MachineInventoryReconciler) createPlanSecret(ctx context.Context, mInventory *elementalv1.MachineInventory) error {
	logger := ctrl.LoggerFrom(ctx)

	readyCondition := meta.FindStatusCondition(mInventory.Status.Conditions, elementalv1.ReadyCondition)
	if readyCondition != nil && readyCondition.Reason != elementalv1.PlanCreationFailureReason {
		logger.V(log.DebugDepth).Info("Skipping plan secret creation because ready condition is already set")
		return nil
	}

	planSecret := &corev1.Secret{
		ObjectMeta: metav1.ObjectMeta{
			Annotations: map[string]string{elementalv1.PlanTypeAnnotation: elementalv1.PlanTypeEmpty},
			Namespace:   mInventory.Namespace,
			Name:        mInventory.Name,
			OwnerReferences: []metav1.OwnerReference{
				{
					APIVersion: elementalv1.GroupVersion.String(),
					Kind:       "MachineInventory",
					Name:       mInventory.Name,
					UID:        mInventory.UID,
					Controller: ptr.To(true),
				},
			},
			Labels: map[string]string{
				elementalv1.ElementalManagedLabel: "true",
			},
		},
		Type:       elementalv1.PlanSecretType,
		StringData: map[string]string{"plan": "{}"},
	}

	if err := r.Create(ctx, planSecret); err != nil && !apierrors.IsAlreadyExists(err) {
		return fmt.Errorf("failed to create secret: %w", err)
	}

	mInventory.Status.Plan = &elementalv1.PlanStatus{
		PlanSecretRef: &corev1.ObjectReference{
			Namespace: planSecret.Namespace,
			Name:      planSecret.Name,
		},
		State: elementalv1.PlanState(""),
	}

	logger.Info("Plan secret created")
	meta.SetStatusCondition(&mInventory.Status.Conditions, metav1.Condition{
		Type:    elementalv1.ReadyCondition,
		Reason:  elementalv1.WaitingForPlanReason,
		Status:  metav1.ConditionFalse,
		Message: "waiting for plan to be applied",
	})

	return nil
}

func (r *MachineInventoryReconciler) updateInventoryWithPlanStatus(ctx context.Context, mInventory *elementalv1.MachineInventory) error {
	logger := ctrl.LoggerFrom(ctx)

	planSecret := &corev1.Secret{
		ObjectMeta: metav1.ObjectMeta{
			Namespace: mInventory.Namespace,
			Name:      mInventory.Name,
		},
	}

	logger.V(log.DebugDepth).Info("Attempting to set plan status")
	if err := r.Get(ctx, types.NamespacedName{
		Namespace: mInventory.Namespace,
		Name:      mInventory.Name,
	}, planSecret); err != nil {
		return fmt.Errorf("failed to get secret: %w", err)
	}

	appliedChecksum := string(planSecret.Data["applied-checksum"])
	failedChecksum := string(planSecret.Data["failed-checksum"])

	switch {
	case appliedChecksum != "":
		logger.Info("Plan successfully applied")
		mInventory.Status.Plan.State = elementalv1.PlanApplied
		mInventory.Status.Plan.Checksum = appliedChecksum
		meta.SetStatusCondition(&mInventory.Status.Conditions, metav1.Condition{
			Type:    elementalv1.ReadyCondition,
			Reason:  elementalv1.PlanSuccessfullyAppliedReason,
			Status:  metav1.ConditionTrue,
			Message: "plan successfully applied",
		})
		return nil
	case failedChecksum != "":
		logger.V(log.DebugDepth).Info("Plan failed to be applied")
		mInventory.Status.Plan.State = elementalv1.PlanFailed
		mInventory.Status.Plan.Checksum = failedChecksum
		return fmt.Errorf("failed to apply plan")
	default:
		logger.V(log.DebugDepth).Info("Waiting for plan to be applied")
		meta.SetStatusCondition(&mInventory.Status.Conditions, metav1.Condition{
			Type:    elementalv1.ReadyCondition,
			Reason:  elementalv1.WaitingForPlanReason,
			Status:  metav1.ConditionFalse,
			Message: "waiting for plan to be applied",
		})
		return nil
	}
}

// updateInventoryWithAdoptionStatus computes sanity checks on owner references to verify inventory owner is properly set.
// Returns true if a requeue to wait for owner setup is required, false if no requeue is needed.
func (r *MachineInventoryReconciler) updateInventoryWithAdoptionStatus(ctx context.Context, mInventory *elementalv1.MachineInventory) (bool, error) {
	logger := ctrl.LoggerFrom(ctx)

	owner := getSelectorOwner(mInventory)
	if owner == nil {
		logger.V(log.DebugDepth).Info("Waiting to be adopted")
		meta.SetStatusCondition(&mInventory.Status.Conditions, metav1.Condition{
			Type:    elementalv1.AdoptionReadyCondition,
			Reason:  elementalv1.WaitingToBeAdoptedReason,
			Status:  metav1.ConditionFalse,
			Message: "Waiting to be adopted",
		})
		return false, nil
	}

	adoptedCondition := meta.FindStatusCondition(mInventory.Status.Conditions, elementalv1.AdoptionReadyCondition)
	if adoptedCondition != nil && adoptedCondition.Status == metav1.ConditionTrue {
		logger.V(log.DebugDepth).Info("Inventory already adopted")
		return false, nil
	}

	miSelector := &elementalv1.MachineInventorySelector{}
	if err := r.Get(ctx, types.NamespacedName{
		Namespace: mInventory.Namespace,
		Name:      owner.Name,
	},
		miSelector,
	); err != nil {
		return false, fmt.Errorf("failed to get machine inventory selector: %w", err)
	}

	meta.SetStatusCondition(&mInventory.Status.Conditions, metav1.Condition{
		Type:    elementalv1.AdoptionReadyCondition,
		Reason:  elementalv1.ValidatingAdoptionReason,
		Status:  metav1.ConditionUnknown,
		Message: "Adoption being validated",
	})
	adoptedCondition = meta.FindStatusCondition(mInventory.Status.Conditions, elementalv1.AdoptionReadyCondition)

	deadLine := adoptedCondition.LastTransitionTime.Add(adoptionTimeout * time.Second)

	switch {
	case miSelector.Status.MachineInventoryRef == nil && time.Now().Before(deadLine):
		logger.V(log.DebugDepth).Info("Adoption being validated")
		meta.SetStatusCondition(&mInventory.Status.Conditions, metav1.Condition{
			Type:    elementalv1.AdoptionReadyCondition,
			Reason:  elementalv1.ValidatingAdoptionReason,
			Status:  metav1.ConditionUnknown,
			Message: "Adoption being validated",
		})
		return true, nil
	case miSelector.Status.MachineInventoryRef == nil:
		removeSelectorOwnerShip(mInventory)
		return false, fmt.Errorf("Adoption timeout, dropping selector ownership. Deadline was: %v ", deadLine)
	case miSelector.Status.MachineInventoryRef.Name != mInventory.Name:
		removeSelectorOwnerShip(mInventory)
		return false, fmt.Errorf("Ownership mismatch, dropping selector ownership")
	default:
		logger.Info("Successfully adopted")
		meta.SetStatusCondition(&mInventory.Status.Conditions, metav1.Condition{
			Type:    elementalv1.AdoptionReadyCondition,
			Reason:  elementalv1.SuccessfullyAdoptedReason,
			Status:  metav1.ConditionTrue,
			Message: "Successfully adopted",
		})
		return false, nil
	}
}

func (r *MachineInventoryReconciler) ignoreIncrementalStatusUpdate() predicate.Funcs {
	return predicate.Funcs{
		// Avoid reconciling if the event triggering the reconciliation is related to incremental status updates
		// for MachineInventory resources only
		UpdateFunc: func(e event.UpdateEvent) bool {
			logger := ctrl.LoggerFrom(context.Background())

			if oldMInventory, ok := e.ObjectOld.(*elementalv1.MachineInventory); ok {

				oldMInventory = oldMInventory.DeepCopy()
				newMInventory := e.ObjectNew.(*elementalv1.MachineInventory).DeepCopy()

				// Ignore all fields that might be updated on a status update
				oldMInventory.Status = elementalv1.MachineInventoryStatus{}
				newMInventory.Status = elementalv1.MachineInventoryStatus{}
				oldMInventory.ObjectMeta.ResourceVersion = ""
				newMInventory.ObjectMeta.ResourceVersion = ""
				oldMInventory.ManagedFields = []metav1.ManagedFieldsEntry{}
				newMInventory.ManagedFields = []metav1.ManagedFieldsEntry{}

				update := !cmp.Equal(oldMInventory, newMInventory)
				if !update {
					logger.V(log.DebugDepth).Info("Ignoring status update", "MInventory", oldMInventory.Name)
				}
				return update
			}
			// Return true in case it watches other types
			return true
		},
	}
}

func findSelectorOwner(machineInventory *elementalv1.MachineInventory) (int, *metav1.OwnerReference) {
	for idx, owner := range machineInventory.GetOwnerReferences() {
		if owner.APIVersion == elementalv1.GroupVersion.String() && owner.Kind == "MachineInventorySelector" {
			return idx, &owner
		}
	}
	return -1, nil
}

func getSelectorOwner(machineInventory *elementalv1.MachineInventory) *metav1.OwnerReference {
	_, owner := findSelectorOwner(machineInventory)
	return owner
}

func isAlreadyOwned(machineInventory *elementalv1.MachineInventory) bool {
	return getSelectorOwner(machineInventory) != nil
}

func removeSelectorOwnerShip(machineInventory *elementalv1.MachineInventory) {
	idx, _ := findSelectorOwner(machineInventory)
	if idx >= 0 {
		owners := machineInventory.GetOwnerReferences()
		owners[idx] = owners[len(owners)-1]
		machineInventory.OwnerReferences = owners[:len(owners)-1]
	}
}
