/*
SPDX-FileCopyrightText: Red Hat

SPDX-License-Identifier: Apache-2.0
*/

package controller

import (
	"context"
	"errors"
	"fmt"
	"log/slog"
	"strings"
	"sync"
	"time"

	corev1 "k8s.io/api/core/v1"
	k8serrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/client-go/util/retry"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"

	metal3v1alpha1 "github.com/metal3-io/baremetal-operator/apis/metal3.io/v1alpha1"

	hwmgmtv1alpha1 "github.com/openshift-kni/oran-o2ims/api/hardwaremanagement/v1alpha1"
	"github.com/openshift-kni/oran-o2ims/internal/constants"
	ctlrutils "github.com/openshift-kni/oran-o2ims/internal/controllers/utils"
	hwmgrutils "github.com/openshift-kni/oran-o2ims/internal/hardwaremanager/utils"
	"github.com/openshift-kni/oran-o2ims/internal/logging"
	typederrors "github.com/openshift-kni/oran-o2ims/internal/typed-errors"
)

// BMHAllocationStatus defines filtering options for FetchBMHList
type BMHAllocationStatus string

const ErrorRetryWindow = 5 * time.Minute

const (
	BmhRebootAnnotation                      = "reboot.metal3.io"
	BmhNetworkDataPrefx                      = "network-data"
	BiosUpdateNeededAnnotation               = "clcm.openshift.io/bios-update-needed"
	FirmwareUpdateNeededAnnotation           = "clcm.openshift.io/firmware-update-needed"
	BmhAllocatedLabel                        = "clcm.openshift.io/allocated"
	BmhDeallocationDoneAnnotation            = "clcm.openshift.io/deallocation-complete"
	BmhErrorTimestampAnnotation              = "clcm.openshift.io/bmh-error-timestamp"
	BmhHostMgmtAnnotation                    = "bmac.agent-install.openshift.io/allow-provisioned-host-management"
	BmhInfraEnvLabel                         = "infraenvs.agent-install.openshift.io"
	PreprovisioningImageDeprovisionFinalizer = "preprovisioningimage.agent-install.openshift.io/ai-deprovision"
	SiteConfigOwnedByLabel                   = "siteconfig.open-cluster-management.io/owned-by"
	UpdateReasonBIOSSettings                 = "bios-settings-update"
	UpdateReasonFirmware                     = "firmware-update"
	ValueTrue                                = "true"
	MetaTypeLabel                            = "label"
	MetaTypeAnnotation                       = "annotation"
	OpAdd                                    = "add"
	OpRemove                                 = "remove"
	BmhServicingErr                          = "BMH Servicing Error"
	OrigNetworkDataAnnotation                = "clcm.openshift.io/origNetworkData"
	IBIWarningAnnotation                     = "clcm.openshift.io/ibi-warning"
	IBIWarningMessage                        = "Warning - this node was used for IBI and has been deprovisioned. BMH deletion and IBI reinstall is required before it can be used in a new cluster"
)

// Struct definitions for the nodelist configmap
type bmhBmcInfo struct {
	Address         string `json:"address,omitempty"`
	CredentialsName string `json:"credentialsName,omitempty"`
}

type bmhNodeInfo struct {
	ResourcePoolID string                      `json:"poolID,omitempty"`
	BMC            *bmhBmcInfo                 `json:"bmc,omitempty"`
	Interfaces     []*hwmgmtv1alpha1.Interface `json:"interfaces,omitempty"`
}

func updateBMHMetaWithRetry(
	ctx context.Context,
	c client.Client,
	logger *slog.Logger,
	name types.NamespacedName,
	metaType string, // "label" or "annotation"
	key, value, operation string,
) error {
	ctx = logging.AppendCtx(ctx, slog.String("bmh", name.Name))
	// nolint: wrapcheck
	return retry.OnError(retry.DefaultRetry, k8serrors.IsConflict, func() error {
		// Fetch the latest version of the BMH
		var latestBMH metal3v1alpha1.BareMetalHost
		if err := c.Get(ctx, name, &latestBMH); err != nil {
			logger.ErrorContext(ctx, "Failed to fetch BMH for "+metaType+" update",
				slog.Any("error", err))
			return err
		}

		var targetMap map[string]string
		switch metaType {
		case MetaTypeLabel:
			if latestBMH.Labels == nil && operation == OpAdd {
				latestBMH.Labels = make(map[string]string)
			}
			targetMap = latestBMH.Labels
		case MetaTypeAnnotation:
			if latestBMH.Annotations == nil && operation == OpAdd {
				latestBMH.Annotations = make(map[string]string)
			}
			targetMap = latestBMH.Annotations
		default:
			return fmt.Errorf("unsupported meta type: %s", metaType)
		}

		if operation == OpRemove {
			if targetMap == nil {
				logger.InfoContext(ctx, fmt.Sprintf("BMH has no %ss, skipping remove operation", metaType))
				return nil
			}
			if _, exists := targetMap[key]; !exists {
				logger.InfoContext(ctx, fmt.Sprintf("%s not present, skipping remove operation", metaType),
					slog.String(metaType, key))
				return nil
			}
		}

		if operation == OpAdd {
			// For transient error timestamp, skip if annotation already exists to preserve original timestamp
			if key == BmhErrorTimestampAnnotation {
				if existingValue, exists := targetMap[key]; exists {
					logger.InfoContext(ctx, fmt.Sprintf("%s already present with value %s, skipping add operation", metaType, existingValue),
						slog.String(metaType, key))
					return nil
				}
			}
		}

		// Create a patch base
		patch := client.MergeFrom(latestBMH.DeepCopy())

		switch operation {
		case OpAdd:
			targetMap[key] = value
		case OpRemove:
			delete(targetMap, key)
		default:
			return fmt.Errorf("unsupported operation: %s", operation)
		}

		// Apply the patch
		if err := c.Patch(ctx, &latestBMH, patch); err != nil {
			logger.ErrorContext(ctx, "Failed to update BMH "+metaType,
				slog.String("operation", operation),
				slog.Any("error", err))
			return fmt.Errorf("failed to %s %s on BMH %s: %w", operation, metaType, name.Name, err)
		}

		logger.InfoContext(ctx, "Successfully updated BMH "+metaType,
			slog.String("operation", operation))
		return nil
	})
}

// FetchBMHList retrieves BareMetalHosts filtered by resource pool, allocation status, and resource selectors.
func fetchBMHList(
	ctx context.Context,
	c client.Reader,
	logger *slog.Logger,
	nodeGroupData hwmgmtv1alpha1.NodeGroupData) (metal3v1alpha1.BareMetalHostList, error) {

	var bmhList metal3v1alpha1.BareMetalHostList

	opts, err := ResourceSelectionPrimaryFilter(ctx, c, logger, nodeGroupData)
	if err != nil {
		return bmhList, fmt.Errorf("failed to create primary filter: %w", err)
	}

	// Fetch BMHs based on primary filters
	if err := c.List(ctx, &bmhList, opts...); err != nil {
		return bmhList, fmt.Errorf("failed to get BMH list: %w", err)
	}

	if len(bmhList.Items) == 0 {
		logger.WarnContext(ctx, "No BareMetalHosts found matching criteria",
			slog.String("resourcePoolName", nodeGroupData.ResourcePoolId))
		return bmhList, nil
	}

	bmhList, err = ResourceSelectionSecondaryFilter(ctx, c, logger, nodeGroupData, bmhList)
	if err != nil {
		return bmhList, fmt.Errorf("failed to apply secondary filter: %w", err)
	}

	return bmhList, nil
}

func buildInterfacesFromBMH(
	bmh *metal3v1alpha1.BareMetalHost) ([]*hwmgmtv1alpha1.Interface, error) {
	var interfaces []*hwmgmtv1alpha1.Interface

	if bmh.Status.HardwareDetails == nil {
		return nil, fmt.Errorf("bareMetalHost.status.hardwareDetails should not be nil")
	}

	for _, nic := range bmh.Status.HardwareDetails.NIC {
		label := ""

		if strings.EqualFold(nic.MAC, bmh.Spec.BootMACAddress) {
			label = constants.BootInterfaceLabel
		} else {
			// Interface labels with MACs use - instead of :
			hyphenatedMac := strings.ReplaceAll(nic.MAC, ":", "-")

			// Process interface labels
			for fullLabel, value := range bmh.Labels {
				match := REPatternInterfaceLabel.FindStringSubmatch(fullLabel)
				if len(match) != 2 {
					continue
				}

				if value == nic.Name || strings.EqualFold(hyphenatedMac, value) {
					// We found a matching label
					label = match[1]
					break
				}
			}
		}

		interfaces = append(interfaces, &hwmgmtv1alpha1.Interface{
			Name:       nic.Name,
			MACAddress: nic.MAC,
			Label:      label,
		})
	}

	return interfaces, nil
}

// setBootMACAddressFromLabel populates the bootMACAddress in the BMH spec
// from interface labels when bootMACAddress is not already set. When bootMACAddress
// is already set, it optionally validates that the boot interface label points to the
// same MAC if the label exists. The label is optional when bootMACAddress is already set.
// This allows pre-provisioned hardware to identify the boot interface via labels.
func setBootMACAddressFromLabel(
	ctx context.Context,
	c client.Client,
	logger *slog.Logger,
	bmh *metal3v1alpha1.BareMetalHost) error {
	ctx = logging.AppendCtx(ctx, slog.String("bmh", bmh.Name))

	// Verify hardware details are available
	if bmh.Status.HardwareDetails == nil {
		return fmt.Errorf("bareMetalHost.status.hardwareDetails should not be nil")
	}

	// If bootMACAddress is already set, the label is optional
	// Optionally validate it matches the label if the label exists
	if bmh.Spec.BootMACAddress != "" {

		// Check if the boot interface label exists on the BMH
		bootLabelValue, found := bmh.Labels[constants.BootInterfaceLabelFullKey]
		if !found {
			// Label not found, but bootMACAddress is already set - this is fine
			logger.InfoContext(ctx, "bootMACAddress already set, boot interface label not present (optional)",
				slog.String("mac", bmh.Spec.BootMACAddress))
			return nil
		}

		// Label exists - validate it matches the bootMACAddress
		// The label value could be either a NIC name or a hyphenated MAC address
		var targetMAC string
		for _, nic := range bmh.Status.HardwareDetails.NIC {
			hyphenatedMac := strings.ReplaceAll(nic.MAC, ":", "-")

			if bootLabelValue == nic.Name || strings.EqualFold(hyphenatedMac, bootLabelValue) {
				targetMAC = nic.MAC
				break
			}
		}

		if targetMAC == "" {
			return fmt.Errorf("no NIC found matching boot interface label value '%s' on BMH '%s'",
				bootLabelValue, bmh.Name)
		}

		if !strings.EqualFold(bmh.Spec.BootMACAddress, targetMAC) {
			return fmt.Errorf("bootMACAddress '%s' does not match boot interface label '%s' which points to MAC '%s' on BMH '%s'",
				bmh.Spec.BootMACAddress, constants.BootInterfaceLabelFullKey, targetMAC, bmh.Name)
		}

		logger.InfoContext(ctx, "Validated bootMACAddress matches boot interface label",
			slog.String("label", constants.BootInterfaceLabelFullKey),
			slog.String("mac", targetMAC))
		return nil
	}

	// bootMACAddress is NOT set - label is REQUIRED to determine the boot MAC
	// Look for the boot interface label on the BMH
	bootLabelValue, found := bmh.Labels[constants.BootInterfaceLabelFullKey]
	if !found {
		return fmt.Errorf("boot interface label '%s' not found on BMH '%s'", constants.BootInterfaceLabelFullKey, bmh.Name)
	}

	// The label value could be either a NIC name or a hyphenated MAC address
	// Find the NIC that matches this value
	var targetMAC string
	for _, nic := range bmh.Status.HardwareDetails.NIC {
		hyphenatedMac := strings.ReplaceAll(nic.MAC, ":", "-")

		if bootLabelValue == nic.Name || strings.EqualFold(hyphenatedMac, bootLabelValue) {
			targetMAC = nic.MAC
			break
		}
	}

	if targetMAC == "" {
		return fmt.Errorf("no NIC found matching boot interface label value '%s' on BMH '%s'",
			bootLabelValue, bmh.Name)
	}

	// Update the BMH spec with retry logic to handle concurrent modifications
	name := types.NamespacedName{Name: bmh.Name, Namespace: bmh.Namespace}
	// nolint: wrapcheck
	return retry.OnError(retry.DefaultRetry, k8serrors.IsConflict, func() error {
		// Fetch the latest version of the BMH
		var latestBMH metal3v1alpha1.BareMetalHost
		if err := c.Get(ctx, name, &latestBMH); err != nil {
			logger.ErrorContext(ctx, "Failed to fetch BMH for bootMACAddress update",
				slog.Any("error", err))
			return err
		}

		// Check if bootMACAddress was set by another process
		if latestBMH.Spec.BootMACAddress != "" {
			// Validate it matches what we expect
			if !strings.EqualFold(latestBMH.Spec.BootMACAddress, targetMAC) {
				return fmt.Errorf("bootMACAddress '%s' does not match boot interface label '%s' which points to MAC '%s' on BMH '%s'",
					latestBMH.Spec.BootMACAddress, constants.BootInterfaceLabelFullKey, targetMAC, latestBMH.Name)
			}
			logger.InfoContext(ctx, "bootMACAddress already set and validated",
				slog.String("mac", targetMAC))
			return nil
		}

		// Create a patch base
		patch := client.MergeFrom(latestBMH.DeepCopy())

		// Set the bootMACAddress
		latestBMH.Spec.BootMACAddress = targetMAC

		// Apply the patch
		if err := c.Patch(ctx, &latestBMH, patch); err != nil {
			logger.ErrorContext(ctx, "Failed to patch BMH bootMACAddress",
				slog.Any("error", err))
			return fmt.Errorf("failed to patch BMH bootMACAddress %s: %w", name.Name, err)
		}

		logger.InfoContext(ctx, "Successfully set bootMACAddress from interface label",
			slog.String("label", constants.BootInterfaceLabelFullKey),
			slog.String("labelValue", bootLabelValue),
			slog.String("mac", targetMAC))
		return nil
	})
}

func isBMHAllocated(bmh *metal3v1alpha1.BareMetalHost) bool {
	if currentValue, exists := bmh.Labels[BmhAllocatedLabel]; exists && currentValue == ValueTrue {
		return true
	}
	return false
}

func processHwProfileWithHandledError(
	ctx context.Context,
	c client.Client,
	noncachedClient client.Reader,
	logger *slog.Logger,
	namespace string,
	bmh *metal3v1alpha1.BareMetalHost,
	node *hwmgmtv1alpha1.AllocatedNode,
	profileName string,
	postInstall, validateOnly bool,
) (bool, error) {

	updateRequired, err := processHwProfile(ctx, c, logger, namespace, bmh, profileName, postInstall, validateOnly)
	contType := string(hwmgmtv1alpha1.Provisioned)
	if postInstall {
		contType = string(hwmgmtv1alpha1.Configured)
	}
	if err != nil {
		reason := hwmgmtv1alpha1.Failed
		if typederrors.IsInputError(err) {
			reason = hwmgmtv1alpha1.InvalidInput
		}
		if err := hwmgrutils.SetNodeConditionStatus(ctx, c, noncachedClient, node,
			contType, metav1.ConditionFalse, string(reason), err.Error()); err != nil {
			logger.ErrorContext(ctx, "failed to update node status", slog.String("node", node.Name), slog.Any("error", err))
		}
		return updateRequired, err
	}
	return updateRequired, nil
}

// processHwProfile processes a HardwareProfile and determines if BIOS/firmware updates are required.
// When validateOnly is true, it only checks if updates are needed — HFS/HFC are not modified.
// When validateOnly is false, it updates HFS/HFC, annotates the BMH with BiosUpdateNeeded and/or
// FirmwareUpdateNeeded to signal pending updates if updates are needed. For postInstall, it also
// creates or updates a HostUpdatePolicy to control the update timing.
//
// Returns (true, nil) if any update is required, (false, nil) if no updates needed.
func processHwProfile(ctx context.Context,
	c client.Client,
	logger *slog.Logger,
	namespace string,
	bmh *metal3v1alpha1.BareMetalHost, profileName string,
	postInstall, validateOnly bool) (bool, error) {
	logger.DebugContext(ctx, "Processing hardware profile for BMH",
		slog.String("hwProfile", profileName),
		slog.Bool("postInstall", postInstall),
		slog.Bool("validateOnly", validateOnly))

	// Clear any existing update annotations to ensure clean state
	if !validateOnly {
		if err := clearBMHUpdateAnnotations(ctx, c, logger, bmh); err != nil {
			return false, fmt.Errorf("failed to clear existing update annotations from BMH %s: %w", bmh.Name, err)
		}
	}

	// Skip processing if no hardware profile is specified
	if profileName == "" {
		logger.InfoContext(ctx, "No hardware profile specified, skipping HW profile processing")
		return false, nil
	}

	var err error
	name := types.NamespacedName{
		Name:      profileName,
		Namespace: namespace,
	}

	hwProfile := &hwmgmtv1alpha1.HardwareProfile{}
	if err := c.Get(ctx, name, hwProfile); err != nil {
		return false, fmt.Errorf("unable to find HardwareProfile CR (%s): %w", profileName, err)
	}

	// Check if BIOS update is required
	biosUpdateRequired := false
	if hwProfile.Spec.Bios.Attributes != nil {
		logger.DebugContext(ctx, "Checking if BIOS update is required")
		biosUpdateRequired, err = IsBiosUpdateRequired(ctx, c, logger, bmh, hwProfile.Spec.Bios, validateOnly)
		if err != nil {
			return false, err
		}
		logger.DebugContext(ctx, "BIOS update check complete",
			slog.Bool("biosUpdateRequired", biosUpdateRequired))
	}

	// Check if firmware update is required
	logger.DebugContext(ctx, "Checking if firmware update is required")
	firmwareUpdateRequired, err := IsFirmwareUpdateRequired(ctx, c, logger, bmh, hwProfile.Spec, validateOnly)
	if err != nil {
		return false, err
	}
	logger.DebugContext(ctx, "Firmware update check complete",
		slog.Bool("firmwareUpdateRequired", firmwareUpdateRequired))

	// If nothing is required, return early
	if !biosUpdateRequired && !firmwareUpdateRequired {
		logger.DebugContext(ctx, "No BIOS or firmware updates required")
		return false, nil
	}

	if validateOnly {
		// Return without updating any resources
		return true, nil
	}

	if postInstall {
		if err = createOrUpdateHostUpdatePolicy(ctx, c, logger, bmh, firmwareUpdateRequired, biosUpdateRequired); err != nil {
			return true, fmt.Errorf("failed create or update  HostUpdatePolicy%s/%s: %w", bmh.Namespace, bmh.Name, err)
		}
	}

	bmhName := types.NamespacedName{Name: bmh.Name, Namespace: bmh.Namespace}
	// If bios update is required, annotate BMH
	if biosUpdateRequired {
		logger.DebugContext(ctx, "BIOS update required, setting update-needed annotation",
			slog.String("annotation", BiosUpdateNeededAnnotation))
		if err := updateBMHMetaWithRetry(ctx, c, logger, bmhName, MetaTypeAnnotation, BiosUpdateNeededAnnotation, ValueTrue, OpAdd); err != nil {
			return true, fmt.Errorf("failed to annotate BMH %s/%s: %w", bmh.Namespace, bmh.Name, err)
		}
		logger.DebugContext(ctx, "BIOS update-needed annotation set successfully")
	}

	// if firmware update is required, annotate BMH
	if firmwareUpdateRequired {
		logger.DebugContext(ctx, "Firmware update required, setting update-needed annotation",
			slog.String("annotation", FirmwareUpdateNeededAnnotation))
		if err := updateBMHMetaWithRetry(ctx, c, logger, bmhName, MetaTypeAnnotation, FirmwareUpdateNeededAnnotation, ValueTrue, OpAdd); err != nil {
			return true, fmt.Errorf("failed to annotate BMH %s/%s: %w", bmh.Namespace, bmh.Name, err)
		}
		logger.DebugContext(ctx, "Firmware update-needed annotation set successfully")
	}

	return true, nil
}

func checkBMHStatus(ctx context.Context, logger *slog.Logger, bmh *metal3v1alpha1.BareMetalHost, state metal3v1alpha1.ProvisioningState) bool {
	// Check if the BMH is in  desired state
	if bmh.Status.Provisioning.State == state {
		logger.InfoContext(ctx, "BMH is now in desired state", slog.String("state", string(state)))
		return true
	}
	return false
}

// aannotateNodeConfigInProgress sets an annotation on the corresponding Node object to indicate configuration is in progress.
func annotateNodeConfigInProgress(ctx context.Context,
	c client.Client,
	logger *slog.Logger,
	namespace, nodeName, reason string) error {
	// Fetch the Node object
	node := &hwmgmtv1alpha1.AllocatedNode{}
	if err := c.Get(ctx, types.NamespacedName{Name: nodeName, Namespace: namespace}, node); err != nil {
		return fmt.Errorf("unable to get Node object (%s): %w", nodeName, err)
	}

	// Set annotation to indicate configuration is in progress
	if node.Annotations == nil {
		node.Annotations = make(map[string]string)
	}

	setConfigAnnotation(node, reason)

	// Update the Node object
	if err := c.Update(ctx, node); err != nil {
		logger.InfoContext(ctx, "Failed to annotate node for BIOS configuration", slog.String("node", nodeName))
		return fmt.Errorf("failed to update node %s: %w", nodeName, err)
	}
	logger.InfoContext(ctx, "Annotated node with BIOS config", slog.String("node", nodeName))
	return nil
}

func handleTransitionNodes(ctx context.Context,
	c client.Client,
	noncachedClient client.Reader,
	logger *slog.Logger,
	namespace string,
	nodelist *hwmgmtv1alpha1.AllocatedNodeList, postInstall bool) (ctrl.Result, error) {

	for _, node := range nodelist.Items {
		res, err := handleTransitionNode(ctx, c, noncachedClient, logger, namespace, &node, postInstall, nil)
		if err != nil || res.Requeue || res.RequeueAfter > 0 {
			return res, err
		}
	}

	return ctrl.Result{}, nil
}

// handleTransitionNode processes Day1/Day2 transition logic for a given AllocatedNode.
// It is responsible for:
// - adding reboot annotation (postInstall only) once the corresponding CRs report valid changes
// - waiting for BMH to enter expected transitional state (Preparing/Servicing)
// - removing update-needed annotations and setting node config-in-progress annotation
func handleTransitionNode(ctx context.Context,
	c client.Client,
	noncachedClient client.Reader,
	logger *slog.Logger,
	namespace string,
	node *hwmgmtv1alpha1.AllocatedNode,
	postInstall bool,
	nodeOps NodeOps,
) (ctrl.Result, error) {
	ctx = logging.AppendCtx(ctx, slog.String("node", node.Name))
	bmh, err := getBMHForNode(ctx, noncachedClient, node)
	if err != nil {
		return ctrl.Result{}, fmt.Errorf("failed to get BMH for node %s: %w", node.Name, err)
	}
	ctx = logging.AppendCtx(ctx, slog.String("bmh", bmh.Name))

	if bmh.Annotations == nil {
		bmh.Annotations = make(map[string]string)
	}

	if postInstall {
		if err := evaluateCRForReboot(ctx, c, logger, bmh); err != nil {
			return ctrl.Result{}, err
		}
	}

	updateCases := []struct {
		AnnotationKey string
		Reason        string
		LogLabel      string
	}{
		{BiosUpdateNeededAnnotation, UpdateReasonBIOSSettings, "BIOS settings"},
		{FirmwareUpdateNeededAnnotation, UpdateReasonFirmware, "firmware"},
	}

	// Process each update case for the current BMH.
	// Since BMO can process both BIOS and firmware updates in a single cycle,
	// we process all annotations that exist so they can all be removed when
	// the BMH enters the transitional state.
	for _, uc := range updateCases {
		if _, exists := bmh.Annotations[uc.AnnotationKey]; !exists {
			continue
		}
		res, err := processBMHUpdateCase(ctx, c, noncachedClient, logger, namespace, node, bmh, uc, postInstall, nodeOps)
		if err != nil || res.Requeue || res.RequeueAfter > 0 {
			// Propagate either requeue or error
			return res, err
		}
		// Continue to process remaining annotations in the same cycle
	}

	return ctrl.Result{}, nil
}

func addRebootAnnotation(ctx context.Context, c client.Client, logger *slog.Logger, bmh *metal3v1alpha1.BareMetalHost) error {
	bmhName := types.NamespacedName{Name: bmh.Name, Namespace: bmh.Namespace}
	logger.DebugContext(ctx, "Adding reboot annotation to BMH",
		slog.String("annotation", BmhRebootAnnotation))
	if err := updateBMHMetaWithRetry(ctx, c, logger, bmhName, "annotation", BmhRebootAnnotation, "", OpAdd); err != nil {
		return fmt.Errorf("failed to add %s to BMH %+v: %w", BmhRebootAnnotation, bmhName, err)
	}
	logger.InfoContext(ctx, "Added reboot annotation to trigger firmware/BIOS update")
	return nil
}

// evaluateCRForReboot evaluates the BareMetalHost for pending BIOS and/or
// firmware updates and adds the reboot annotation to the BMH to trigger node
// reboot when required.
func evaluateCRForReboot(ctx context.Context, c client.Client, logger *slog.Logger, bmh *metal3v1alpha1.BareMetalHost) error {
	// Check if both annotations are present
	hasBiosAnnotation := bmh.Annotations[BiosUpdateNeededAnnotation] != ""
	hasFirmwareAnnotation := bmh.Annotations[FirmwareUpdateNeededAnnotation] != ""

	logger.DebugContext(ctx, "Evaluating BMH for reboot",
		slog.Bool("hasBiosAnnotation", hasBiosAnnotation),
		slog.Bool("hasFirmwareAnnotation", hasFirmwareAnnotation))

	if !hasBiosAnnotation && !hasFirmwareAnnotation {
		logger.DebugContext(ctx, "No update-needed annotations present, skipping reboot evaluation")
		return nil
	}

	var biosChange, firmwareChange bool
	var err error

	// If both annotations are present, require both checks to pass
	if hasBiosAnnotation && hasFirmwareAnnotation {
		logger.DebugContext(ctx, "Both BIOS and firmware annotations present, checking both")

		biosChange, err = isFirmwareSettingsChangeDetectedAndValid(ctx, c, bmh)
		if err != nil {
			return fmt.Errorf("failed to evaluate FirmwareSettings status: %w", err)
		}

		firmwareChange, err = isHostFirmwareComponentsChangeDetectedAndValid(ctx, c, bmh)
		if err != nil {
			return fmt.Errorf("failed to evaluate HostFirmwareComponents status: %w", err)
		}

		logger.DebugContext(ctx, "Both annotations check results",
			slog.Bool("biosChange", biosChange),
			slog.Bool("firmwareChange", firmwareChange))

		if biosChange && firmwareChange {
			logger.DebugContext(ctx, "Both BIOS and firmware changes detected, adding reboot annotation")
			return addRebootAnnotation(ctx, c, logger, bmh)
		}
		logger.DebugContext(ctx, "Not all changes detected yet, skipping reboot")
		return nil
	}

	// If only BIOS annotation is present
	if hasBiosAnnotation {
		logger.DebugContext(ctx, "Only BIOS annotation present, checking BIOS change")

		biosChange, err = isFirmwareSettingsChangeDetectedAndValid(ctx, c, bmh)
		if err != nil {
			return fmt.Errorf("failed to evaluate FirmwareSettings status: %w", err)
		}

		logger.DebugContext(ctx, "BIOS-only check result",
			slog.Bool("biosChange", biosChange))

		if biosChange {
			logger.DebugContext(ctx, "BIOS change detected, adding reboot annotation")
			return addRebootAnnotation(ctx, c, logger, bmh)
		}
		logger.DebugContext(ctx, "BIOS change not detected yet, skipping reboot")
	}

	// If only firmware annotation is present
	if hasFirmwareAnnotation {
		logger.DebugContext(ctx, "Only firmware annotation present, checking firmware change")

		firmwareChange, err = isHostFirmwareComponentsChangeDetectedAndValid(ctx, c, bmh)
		if err != nil {
			return fmt.Errorf("failed to evaluate HostFirmwareComponents status: %w", err)
		}

		logger.DebugContext(ctx, "Firmware-only check result",
			slog.Bool("firmwareChange", firmwareChange))

		if firmwareChange {
			logger.DebugContext(ctx, "Firmware change detected, adding reboot annotation")
			return addRebootAnnotation(ctx, c, logger, bmh)
		}
		logger.DebugContext(ctx, "Firmware change not detected yet, skipping reboot")
	}

	return nil
}

// processBMHUpdateCase handles the update transition for a given BMH and update case.
//
// This function orchestrates the transition from "update-needed" to "config-in-progress" state:
//  1. Handles BMH error states (transient errors are tolerated with timeout, permanent errors fail the node)
//  2. Waits for BMH to enter the expected transition state:
//     - Day1: waits for Preparing state
//     - Day2(postInstall): waits for Servicing state
//  3. When the BMH is in the expected state, removes the update-needed annotations from BMH and
//     adds the config-in-progress annotation to the AllocatedNode to mark the update as in progress.
func processBMHUpdateCase(ctx context.Context,
	c client.Client,
	noncachedClient client.Reader,
	logger *slog.Logger,
	namespace string,
	node *hwmgmtv1alpha1.AllocatedNode, bmh *metal3v1alpha1.BareMetalHost,
	uc struct {
		AnnotationKey string
		Reason        string
		LogLabel      string
	}, postInstall bool,
	nodeOps NodeOps,
) (ctrl.Result, error) {
	if bmh.Status.OperationalStatus == metal3v1alpha1.OperationalStatusError {
		tolerate, err := tolerateAndAnnotateTransientBMHError(ctx, c, logger, bmh)
		if err != nil || tolerate {
			return hwmgrutils.RequeueWithShortInterval(), nil
		}

		message := fmt.Sprintf("BMH in error state: %s", bmh.Status.ErrorType)
		logger.WarnContext(ctx, message)

		// Uncordon the node on day2 failure so it returns to schedulable
		if postInstall {
			if err := nodeOps.UncordonNode(ctx, node.Status.Hostname); err != nil {
				return ctrl.Result{}, fmt.Errorf(
					"failed to uncordon node (%s) after BMH error: %w",
					node.Status.Hostname, err)
			}
		}

		// Clear config-in-progress annotation before setting node to failed
		if err := clearConfigAnnotationWithPatch(ctx, c, node); err != nil {
			logger.ErrorContext(ctx, "Failed to clear config annotation",
				slog.Any("error", err))
			return ctrl.Result{}, err
		}

		// Clear BMH update annotations to ensure clean state for retry
		if err := clearBMHUpdateAnnotations(ctx, c, logger, bmh); err != nil {
			logger.WarnContext(ctx, "Failed to clear BMH update annotations after error",
				slog.Any("error", err))
			return hwmgrutils.RequeueWithShortInterval(), nil
		}

		// Clear BMH error annotation to allow future retry attempts
		if err := clearTransientBMHErrorAnnotation(ctx, c, logger, bmh); err != nil {
			logger.WarnContext(ctx, "failed to clear BMH error annotation for future retries",
				slog.Any("error", err))
			return hwmgrutils.RequeueWithShortInterval(), nil
		}

		// Set node to failed status after cleanup completes
		// This ensures NAR can be retried after user intervention
		condType := hwmgmtv1alpha1.Provisioned
		if postInstall {
			condType = hwmgmtv1alpha1.Configured
		}
		if err := hwmgrutils.SetNodeFailedStatus(ctx, c, logger, node, string(condType), message); err != nil {
			if k8serrors.IsConflict(err) {
				logger.WarnContext(ctx, "conflict setting node status; will retry", slog.Any("error", err))
				return hwmgrutils.RequeueWithShortInterval(), nil
			}
			// everything else here is unexpected
			return ctrl.Result{}, fmt.Errorf("failed to set node failed status for %s: %w", node.Name, err)
		}

		return hwmgrutils.RequeueWithMediumInterval(), nil
	}

	// clear transient error annotation if BMH recovered
	if _, hasAnnotation := bmh.Annotations[BmhErrorTimestampAnnotation]; hasAnnotation {
		if err := clearTransientBMHErrorAnnotation(ctx, c, logger, bmh); err != nil {
			logger.WarnContext(ctx, "failed to clean up transient error annotation", slog.Any("error", err))
			return hwmgrutils.RequeueWithShortInterval(), nil
		}
	}

	// Check whether the current state of the BMH meets the transition condition.
	if postInstall {
		if bmh.Status.OperationalStatus != metal3v1alpha1.OperationalStatusServicing {
			// Update condition message to indicate waiting for BMH to enter Servicing
			if err := hwmgrutils.SetNodeConfigUpdateRequested(ctx, c, noncachedClient, logger, node,
				node.Spec.HwProfile, string(hwmgmtv1alpha1.NodeWaitingBMHServicing)); err != nil {
				return ctrl.Result{}, fmt.Errorf("failed to update node condition for waiting servicing: %w", err)
			}
			logger.InfoContext(ctx,
				"BMH not in 'Servicing' state yet, requeuing",
				slog.String("expected", string(metal3v1alpha1.OperationalStatusServicing)),
				slog.String("current", string(bmh.Status.OperationalStatus)))
			return hwmgrutils.RequeueWithShortInterval(), nil
		}
		logger.InfoContext(ctx,
			fmt.Sprintf("BMH transitioned to 'Servicing' state for %s update", uc.LogLabel))
	} else {
		if bmh.Status.Provisioning.State != metal3v1alpha1.StatePreparing {
			logger.InfoContext(ctx,
				"BMH not in 'Preparing' state yet, requeuing",
				slog.String("expected", string(metal3v1alpha1.StatePreparing)),
				slog.String("current", string(bmh.Status.Provisioning.State)))
			return hwmgrutils.RequeueWithShortInterval(), nil
		}
		logger.InfoContext(ctx,
			fmt.Sprintf("BMH transitioned to 'Preparing' state for %s update", uc.LogLabel))
	}

	// Remove the update-needed annotation from the BMH.
	bmhName := types.NamespacedName{Name: bmh.Name, Namespace: bmh.Namespace}
	if err := updateBMHMetaWithRetry(ctx, c, logger, bmhName, MetaTypeAnnotation, uc.AnnotationKey, "", OpRemove); err != nil {
		logger.WarnContext(ctx, "failed to remove annotation",
			slog.String("annotation", uc.AnnotationKey), slog.Any("error", err))
		return hwmgrutils.RequeueWithShortInterval(), nil
	}

	// Only add the in-progress annotation if the node is not already annotated.
	if getConfigAnnotation(node) == "" {
		if err := annotateNodeConfigInProgress(ctx, c, logger, namespace, node.Name, uc.Reason); err != nil {
			logger.WarnContext(ctx,
				fmt.Sprintf("Failed to annotate %s update in progress", uc.LogLabel),
				slog.Any("error", err))
			return hwmgrutils.RequeueWithShortInterval(), nil
		}
		logger.InfoContext(ctx,
			fmt.Sprintf("BMH %s update initiated", uc.LogLabel))
		// Requeue to let the next reconcile handle the in-progress node
		return hwmgrutils.RequeueWithShortInterval(), nil
	} else {
		logger.InfoContext(ctx,
			"Skipping annotation; another config already in progress",
			slog.String("skipped", uc.Reason))
	}

	return ctrl.Result{}, nil
}

// handleBMHCompletion handles the completion check for all AllocatedNodes that are allocated
// to the NodeAllocationRequest. It sets the Provisioned condition to Completed when complete
// and returns true if any node is still in progress.
func handleBMHCompletion(ctx context.Context,
	c client.Client,
	noncachedClient client.Reader,
	logger *slog.Logger,
	namespace string,
	nodelist *hwmgmtv1alpha1.AllocatedNodeList) (bool, error) {

	logger.DebugContext(ctx, "Checking for nodes with config in progress")

	// Find all nodes that are in InProgress state or have no condition
	nodes := findNodesInProgress(nodelist)
	if len(nodes) == 0 {
		return false, nil // no node is in config progress
	}

	logger.DebugContext(ctx, "Handling nodes for completion", slog.Int("count", len(nodes)))
	var (
		wg          sync.WaitGroup
		mu          sync.Mutex
		anyUpdating bool
		errs        []error
	)

	// Process each node in parallel for better performance with large clusters (e.g. 3 masters + 50+ workers).
	for _, node := range nodes {
		wg.Add(1)
		go func(node *hwmgmtv1alpha1.AllocatedNode) {
			defer wg.Done()

			nodeCtx := logging.AppendCtx(ctx, slog.String("node", node.Name))
			updating, err := handleSingleNodeCompletion(nodeCtx, c, noncachedClient, logger, namespace, node)

			mu.Lock()
			defer mu.Unlock()

			if updating {
				anyUpdating = true
			}
			if err != nil {
				logger.ErrorContext(nodeCtx, "failed to handle single node completion",
					slog.Any("error", err))
				errs = append(errs, fmt.Errorf("node %s, error: %w", node.Name, err))
			}
		}(node)
	}

	wg.Wait()

	if len(errs) > 0 {
		aggErr := fmt.Errorf("failed to handle BMH completion: %w", errors.Join(errs...))
		return anyUpdating, aggErr
	}

	return anyUpdating, nil
}

// handleSingleNodeCompletion handles the completion check for a single AllocatedNode.
// Returns true if the node is still updating, false if the node is complete.
func handleSingleNodeCompletion(ctx context.Context,
	c client.Client,
	noncachedClient client.Reader,
	logger *slog.Logger,
	namespace string,
	node *hwmgmtv1alpha1.AllocatedNode) (bool, error) {
	ctx = logging.AppendCtx(ctx, slog.String("node", node.Name))

	// Get BMH associated with the node
	bmh, err := getBMHForNode(ctx, noncachedClient, node)
	if err != nil {
		return true, fmt.Errorf("failed to get BMH for node %s: %w", node.Name, err)
	}
	ctx = logging.AppendCtx(ctx, slog.String("bmh", bmh.Name))

	// Check if BMH has transitioned to "Available"
	// If BMH is not available yet, update is still ongoing
	if !checkBMHStatus(ctx, logger, bmh, metal3v1alpha1.StateAvailable) {
		// BMH entered an error state
		if bmh.Status.OperationalStatus == metal3v1alpha1.OperationalStatusError {
			tolerate, err := tolerateAndAnnotateTransientBMHError(ctx, c, logger, bmh)
			if err != nil || tolerate {
				return true, err
			}
			errMessage := fmt.Errorf("bmh %s/%s in an error state %s", bmh.Namespace, bmh.Name, bmh.Status.Provisioning.State)
			if err := hwmgrutils.SetNodeConditionStatus(ctx, c, noncachedClient, node,
				string(hwmgmtv1alpha1.Provisioned), metav1.ConditionFalse,
				string(hwmgmtv1alpha1.Failed), errMessage.Error()); err != nil {
				logger.ErrorContext(ctx, "failed to set node condition status",
					slog.Any("error", err))
			}
			return false, errMessage
		}
		// if BMH is not in error state, clean up transient annotation if it exists
		if _, hasAnnotation := bmh.Annotations[BmhErrorTimestampAnnotation]; hasAnnotation {
			if err := clearTransientBMHErrorAnnotation(ctx, c, logger, bmh); err != nil {
				logger.WarnContext(ctx, "failed to clean up transient error annotation", slog.Any("error", err))
			}
		}
		return true, nil // still waiting for available
	}

	// BMH is now available - clear any stale transient error annotation
	if _, hasAnnotation := bmh.Annotations[BmhErrorTimestampAnnotation]; hasAnnotation {
		if err := clearTransientBMHErrorAnnotation(ctx, c, logger, bmh); err != nil {
			logger.WarnContext(ctx, "failed to clean up transient error annotation on BMH available transition",
				slog.Any("error", err))
		}
	}

	// Validate node configuration (firmware versions and BIOS settings) before finalizing
	configValid, err := validateNodeConfiguration(ctx, c, noncachedClient, logger, bmh, namespace, node.Spec.HwProfile)
	if err != nil {
		return true, err
	}
	if !configValid {
		return true, nil // Continue polling
	}

	// Apply post-config updates and finalize the process
	if requeue, err := applyPostConfigUpdates(ctx, c, noncachedClient, node); err != nil {
		return true, fmt.Errorf("failed to apply post config update on node %s: %w", node.Name, err)
	} else if requeue > DoNotRequeue {
		return true, nil
	}

	return false, nil // update is now complete
}

func checkForPendingUpdate(ctx context.Context,
	c client.Client,
	noncachedClient client.Reader,
	logger *slog.Logger,
	namespace string,
	nodeAllocationRequest *hwmgmtv1alpha1.NodeAllocationRequest) (ctrl.Result, bool, error) {
	// check if there are any pending work
	nodelist, err := hwmgrutils.GetChildNodes(ctx, logger, c, nodeAllocationRequest)
	if err != nil {
		return ctrl.Result{}, false, fmt.Errorf("failed to get child nodes for Node Pool %s: %w", nodeAllocationRequest.Name, err)
	}

	// Process BMHs transitioning to "Preparing"
	res, err := handleTransitionNodes(ctx, c, noncachedClient, logger, namespace, nodelist, false)
	if err != nil {
		return ctrl.Result{}, false, err
	}

	if res.Requeue || res.RequeueAfter > 0 {
		logger.InfoContext(ctx, "Skipping handleBMHCompletion as update is in progress")
		return res, true, nil
	}

	// Check if configuration is completed
	updating, err := handleBMHCompletion(ctx, c, noncachedClient, logger, namespace, nodelist)
	return ctrl.Result{}, updating, err
}

func getBMHForNode(ctx context.Context, c client.Reader, node *hwmgmtv1alpha1.AllocatedNode) (*metal3v1alpha1.BareMetalHost, error) {
	bmhName := node.Spec.HwMgrNodeId
	bmhNamespace := node.Spec.HwMgrNodeNs
	name := types.NamespacedName{Name: bmhName, Namespace: bmhNamespace}

	var bmh metal3v1alpha1.BareMetalHost
	if err := c.Get(ctx, name, &bmh); err != nil {
		return nil, fmt.Errorf("unable to find BMH (%v): %w", name, err)
	}

	return &bmh, nil
}

func removeInfraEnvLabelFromPreprovisioningImage(ctx context.Context, c client.Client, logger *slog.Logger, name types.NamespacedName) error {
	// nolint: wrapcheck
	return retry.OnError(retry.DefaultRetry, k8serrors.IsConflict, func() error {
		image := &metal3v1alpha1.PreprovisioningImage{}
		if err := c.Get(ctx, name, image); err != nil {
			logger.ErrorContext(ctx, "Failed to get PreprovisioningImage",
				slog.Any("error", err))
			return err
		}

		patched := image.DeepCopy()
		delete(patched.Labels, BmhInfraEnvLabel)
		// Remove the AI deprovision finalizer if present, as it blocks BMH deletion
		controllerutil.RemoveFinalizer(patched, PreprovisioningImageDeprovisionFinalizer)

		// Patch changes
		patch := client.MergeFrom(image)
		if err := c.Patch(ctx, patched, patch); err != nil {
			logger.ErrorContext(ctx, "Failed to patch PreprovisioningImage",
				slog.Any("error", err))
			return fmt.Errorf("failed to patch PreprovisioningImage %s: %w", name.String(), err)
		}

		logger.InfoContext(ctx, "Successfully cleaned up PreprovisioningImage")
		return nil
	})
}

// removeInfraEnvLabel removes InfraEnvLabel from BMH and the corresponding PreprovisioningImage resource.
func removeInfraEnvLabel(ctx context.Context, c client.Client, logger *slog.Logger, name types.NamespacedName) error {
	// Remove BmhInfraEnvLabel from BMH
	err := updateBMHMetaWithRetry(ctx, c, logger, name, MetaTypeLabel, BmhInfraEnvLabel, "", OpRemove)
	if err != nil {
		return fmt.Errorf("failed to remove %s label from BMH %v: %w", BmhInfraEnvLabel, name, err)
	}

	// Remove BmhInfraEnvLabel from preprovisioningImage
	err = removeInfraEnvLabelFromPreprovisioningImage(ctx, c, logger, name)
	if err != nil {
		return fmt.Errorf("failed to remove %s label from PreprovisioningImage %v: %w", BmhInfraEnvLabel, name, err)
	}
	return nil
}

// finalizeBMHDeallocation deallocates a BareMetalHost that is no longer associated with a cluster deployment.
func finalizeBMHDeallocation(ctx context.Context, c client.Client, logger *slog.Logger, bmh *metal3v1alpha1.BareMetalHost, skipCleanup bool) error {
	ctx = logging.AppendCtx(ctx, slog.String("bmh", bmh.Name))
	name := types.NamespacedName{Name: bmh.Name, Namespace: bmh.Namespace}
	// nolint: wrapcheck
	return retry.OnError(retry.DefaultRetry, k8serrors.IsConflict, func() error {
		// Fetch the latest version of the BMH
		var current metal3v1alpha1.BareMetalHost
		if err := c.Get(ctx, name, &current); err != nil {
			logger.ErrorContext(ctx, "Failed to get BMH",
				slog.Any("error", err))
			return err
		}

		patched := current.DeepCopy()

		// Remove allocation-related labels
		for _, key := range []string{SiteConfigOwnedByLabel, BmhAllocatedLabel, ctlrutils.AllocatedNodeLabel} {
			delete(patched.Labels, key)
		}

		// Remove configuration-related annotations
		for _, key := range []string{BiosUpdateNeededAnnotation, FirmwareUpdateNeededAnnotation} {
			delete(patched.Annotations, key)
		}

		// Initialize annotations map if it's nil
		if patched.Annotations == nil {
			patched.Annotations = make(map[string]string)
		}
		patched.Annotations[BmhDeallocationDoneAnnotation] = ValueTrue

		if !skipCleanup {
			// Clear CustomDeploy entirely
			patched.Spec.CustomDeploy = nil
			// Restore PreprovisioningNetworkDataName from the saved annotation if it
			// differs from the current value. During IBI provisioning, the field gets
			// overwritten with a secret that is deleted during deprovisioning.
			if origValue, exists := patched.Annotations[OrigNetworkDataAnnotation]; exists {
				if patched.Spec.PreprovisioningNetworkDataName != origValue {
					logger.InfoContext(ctx, "Restoring PreprovisioningNetworkDataName from annotation",
						slog.String("current", patched.Spec.PreprovisioningNetworkDataName),
						slog.String("original", origValue))
					patched.Spec.PreprovisioningNetworkDataName = origValue
				}
			} else if patched.Spec.PreprovisioningNetworkDataName == "" {
				// Fallback for upgrade scenario: if the annotation doesn't exist (BMH was
				// allocated by an older operator version that cleared the field), check if
				// a Secret with the legacy naming convention exists and restore it.
				// TODO: Remove this fallback once upgrade-breaking changes are introduced.
				expectedSecretName := BmhNetworkDataPrefx + "-" + bmh.Name
				secret := &corev1.Secret{}
				if err := c.Get(ctx, types.NamespacedName{
					Name: expectedSecretName, Namespace: bmh.Namespace,
				}, secret); err != nil {
					if !k8serrors.IsNotFound(err) {
						return fmt.Errorf("failed to get Secret %s/%s: %w", bmh.Namespace, expectedSecretName, err)
					}
				} else {
					patched.Spec.PreprovisioningNetworkDataName = expectedSecretName
					logger.InfoContext(ctx, "Restored PreprovisioningNetworkDataName from legacy secret",
						slog.String("secretName", expectedSecretName))
				}
			}
			// Clear image reference
			patched.Spec.Image = nil
		}

		// Always clear the origNetworkData annotation at the deallocation boundary
		// to prevent stale values leaking into the next allocation cycle.
		delete(patched.Annotations, OrigNetworkDataAnnotation)

		if !skipCleanup && (current.Status.Provisioning.State == metal3v1alpha1.StateProvisioned ||
			current.Status.Provisioning.State == metal3v1alpha1.StateExternallyProvisioned) {
			// Wipe partition tables using automated cleaning
			patched.Spec.AutomatedCleaningMode = metal3v1alpha1.CleaningModeMetadata
			// Power off the host
			patched.Spec.Online = false
		}
		if !skipCleanup && current.Spec.ExternallyProvisioned {
			patched.Annotations[IBIWarningAnnotation] = IBIWarningMessage
		}

		// Patch changes
		patch := client.MergeFrom(&current)
		if err := c.Patch(ctx, patched, patch); err != nil {
			logger.ErrorContext(ctx, "Failed to patch BMH",
				slog.Any("error", err))
			return fmt.Errorf("failed to patch BMH %s: %w", name.String(), err)
		}

		logger.InfoContext(ctx, "Successfully deallocated BMH")
		return nil
	})
}

// saveOrigNetworkData saves the current PreprovisioningNetworkDataName value in an annotation
// on the BMH so it can be restored during deprovisioning. IBI provisioning overwrites this
// field with a secret that gets deleted when the cluster is deprovisioned.
func saveOrigNetworkData(ctx context.Context, c client.Client, logger *slog.Logger, bmh *metal3v1alpha1.BareMetalHost) error {
	// Skip if the annotation already exists (re-entrant safety)
	if _, exists := bmh.Annotations[OrigNetworkDataAnnotation]; exists {
		return nil
	}

	bmhName := types.NamespacedName{Name: bmh.Name, Namespace: bmh.Namespace}
	value := bmh.Spec.PreprovisioningNetworkDataName

	logger.InfoContext(ctx, "Saving original PreprovisioningNetworkDataName",
		slog.String("value", value))

	return updateBMHMetaWithRetry(ctx, c, logger, bmhName, MetaTypeAnnotation,
		OrigNetworkDataAnnotation, value, OpAdd)
}

// deallocateBMH deallocates a BareMetalHost that is no longer associated with a cluster deployment.
func deallocateBMH(ctx context.Context, c client.Client, logger *slog.Logger, bmh *metal3v1alpha1.BareMetalHost, skipCleanup bool) error {
	name := types.NamespacedName{Name: bmh.Name, Namespace: bmh.Namespace}

	// Remove InfraEnvLabel: ensure the assisted-service is no longer managing PreprovisioningImage
	if err := removeInfraEnvLabel(ctx, c, logger, name); err != nil {
		return fmt.Errorf("unable to removeInfraEnvLabel: %w", err)
	}
	// Clean up BMH
	if err := finalizeBMHDeallocation(ctx, c, logger, bmh, skipCleanup); err != nil {
		return fmt.Errorf("unable to finalizeBMHDeallocation: %w", err)
	}

	return nil
}

// markBMHAllocated sets the "allocated" label to "true" on a BareMetalHost.
func markBMHAllocated(ctx context.Context, c client.Client, logger *slog.Logger, bmh *metal3v1alpha1.BareMetalHost) error {
	// Check if the BMH is already allocated to avoid unnecessary patching
	if isBMHAllocated(bmh) {
		logger.InfoContext(ctx, "BMH is already allocated, skipping update")
		return nil // No change needed
	}
	name := types.NamespacedName{Name: bmh.Name, Namespace: bmh.Namespace}
	return updateBMHMetaWithRetry(ctx, c, logger, name, MetaTypeLabel, BmhAllocatedLabel, ValueTrue, OpAdd)
}

// allowHostManagement sets bmac.agent-install.openshift.io/allow-provisioned-host-management annotation on a BareMetalHost.
func allowHostManagement(ctx context.Context, c client.Client, logger *slog.Logger, bmh *metal3v1alpha1.BareMetalHost) error {
	if val, exists := bmh.Annotations[BmhHostMgmtAnnotation]; exists && val == "" {
		return nil
	}
	name := types.NamespacedName{Name: bmh.Name, Namespace: bmh.Namespace}
	return updateBMHMetaWithRetry(ctx, c, logger, name, MetaTypeAnnotation, BmhHostMgmtAnnotation, "", OpAdd)
}

func isBMHDeallocated(bmh *metal3v1alpha1.BareMetalHost) bool {
	return bmh.Annotations != nil && bmh.Annotations[BmhDeallocationDoneAnnotation] == ValueTrue
}

// clearBMHAnnotation clears both BmhDeallocationDoneAnnotation and BmhErrorTimestampAnnotation from a BareMetalHost in a single API call
func clearBMHAnnotation(ctx context.Context, c client.Client, logger *slog.Logger, bmh *metal3v1alpha1.BareMetalHost) error {
	name := types.NamespacedName{Namespace: bmh.Namespace, Name: bmh.Name}

	// nolint: wrapcheck
	return retry.OnError(retry.DefaultRetry, k8serrors.IsConflict, func() error {
		// Fetch the latest version of the BMH
		var latestBMH metal3v1alpha1.BareMetalHost
		if err := c.Get(ctx, name, &latestBMH); err != nil {
			logger.ErrorContext(ctx, "Failed to fetch BMH for annotation cleanup",
				slog.Any("error", err))
			return err
		}

		// Check if we need to do anything
		needsUpdate := false
		if latestBMH.Annotations != nil {
			if _, exists := latestBMH.Annotations[BmhDeallocationDoneAnnotation]; exists {
				needsUpdate = true
			}
			if _, exists := latestBMH.Annotations[BmhErrorTimestampAnnotation]; exists {
				needsUpdate = true
			}
		}

		// If no annotations to clear, skip the patch
		if !needsUpdate {
			logger.InfoContext(ctx, "No BMH annotations to clear, skipping update")
			return nil
		}

		// Create a patch base
		patch := client.MergeFrom(latestBMH.DeepCopy())

		// Remove both annotations in memory
		if latestBMH.Annotations != nil {
			delete(latestBMH.Annotations, BmhDeallocationDoneAnnotation)
			delete(latestBMH.Annotations, BmhErrorTimestampAnnotation)
		}

		// Apply the patch with both changes in a single API call
		if err := c.Patch(ctx, &latestBMH, patch); err != nil {
			logger.ErrorContext(ctx, "Failed to clear BMH annotations",
				slog.Any("error", err))
			return fmt.Errorf("failed to clear BMH annotations on BMH %s: %w", name.Name, err)
		}

		logger.InfoContext(ctx, "Successfully cleared BMH annotations")
		return nil
	})
}

func patchBMHOnline(ctx context.Context, c client.Client, bmh *metal3v1alpha1.BareMetalHost, online bool) error {
	name := types.NamespacedName{Namespace: bmh.Namespace, Name: bmh.Name}
	// nolint: wrapcheck
	return retry.OnError(retry.DefaultRetry, k8serrors.IsConflict, func() error {
		var fresh metal3v1alpha1.BareMetalHost
		if err := c.Get(ctx, name, &fresh); err != nil {
			return err
		}
		patched := fresh.DeepCopy()
		patched.Spec.Online = online

		return c.Patch(ctx, patched, client.MergeFrom(&fresh))
	})
}

func markBMHTransitenError(ctx context.Context, c client.Client, logger *slog.Logger, bmh *metal3v1alpha1.BareMetalHost) error {
	if bmh.Annotations == nil {
		bmh.Annotations = make(map[string]string)
	}
	if _, exists := bmh.Annotations[BmhErrorTimestampAnnotation]; exists {
		logger.InfoContext(ctx, "BMH already has transient error timestamp annotation, skipping",
			slog.String("timestamp", bmh.Annotations[BmhErrorTimestampAnnotation]))
		return nil // Already marked
	}
	bmhName := types.NamespacedName{Name: bmh.Name, Namespace: bmh.Namespace}
	logger.InfoContext(ctx, "Adding transient error timestamp annotation to BMH")
	return updateBMHMetaWithRetry(ctx, c, logger, bmhName, MetaTypeAnnotation,
		BmhErrorTimestampAnnotation,
		time.Now().Format(time.RFC3339), OpAdd)
}

func clearTransientBMHErrorAnnotation(ctx context.Context, c client.Client, logger *slog.Logger, bmh *metal3v1alpha1.BareMetalHost) error {
	bmhName := types.NamespacedName{Name: bmh.Name, Namespace: bmh.Namespace}
	return updateBMHMetaWithRetry(ctx, c, logger, bmhName, MetaTypeAnnotation,
		BmhErrorTimestampAnnotation,
		"", OpRemove)
}

func isTransientBMHError(bmh *metal3v1alpha1.BareMetalHost) (bool, error) {
	if bmh.Status.OperationalStatus != metal3v1alpha1.OperationalStatusError {
		return false, nil
	}

	tsStr, ok := bmh.Annotations[BmhErrorTimestampAnnotation]
	if !ok {
		// First time seeing the error, should be treated as transient error
		return true, nil
	}

	ts, err := time.Parse(time.RFC3339, tsStr)
	if err != nil {
		return false, fmt.Errorf("invalid BMH error timestamp format: %w", err)
	}

	// Return true if still within retry window
	elapsed := time.Since(ts)
	withinWindow := elapsed < ErrorRetryWindow
	return withinWindow, nil
}

func tolerateAndAnnotateTransientBMHError(
	ctx context.Context,
	c client.Client,
	logger *slog.Logger,
	bmh *metal3v1alpha1.BareMetalHost,
) (bool, error) {
	tolerate, err := isTransientBMHError(bmh)
	if err != nil {
		message := "error checking transient BMH error"
		logger.WarnContext(ctx, message, slog.Any("error", err))
		return false, fmt.Errorf("%s: %w", message, err)
	}

	if tolerate {
		tsStr, hasAnnotation := bmh.Annotations[BmhErrorTimestampAnnotation]
		if err := markBMHTransitenError(ctx, c, logger, bmh); err != nil {
			message := "failed to annotate transient BMH error"
			logger.WarnContext(ctx, message, slog.Any("error", err))
			return false, fmt.Errorf("%s: %w", message, err)
		}
		logger.InfoContext(ctx, "BMH in transient error — tolerating and skipping failure",
			slog.Bool("hasTimestamp", hasAnnotation),
			slog.String("timestamp", tsStr))
		return true, nil
	}

	return false, nil
}

// clearBMHUpdateAnnotations removes BIOS and firmware update annotations from a BareMetalHost
func clearBMHUpdateAnnotations(ctx context.Context, c client.Client, logger *slog.Logger, bmh *metal3v1alpha1.BareMetalHost) error {
	bmhName := types.NamespacedName{Name: bmh.Name, Namespace: bmh.Namespace}
	var errs []error

	hasBios := false
	hasFirmware := false

	// Remove BIOS update annotation if it exists
	if _, exists := bmh.Annotations[BiosUpdateNeededAnnotation]; exists {
		hasBios = true
		logger.DebugContext(ctx, "Clearing BIOS update-needed annotation",
			slog.String("annotation", BiosUpdateNeededAnnotation))
		if err := updateBMHMetaWithRetry(ctx, c, logger, bmhName, MetaTypeAnnotation, BiosUpdateNeededAnnotation, "", OpRemove); err != nil {
			logger.WarnContext(ctx, "Failed to remove BIOS update annotation",
				slog.Any("error", err))
			errs = append(errs, fmt.Errorf("bios annotation removal failed: %w", err))
		} else {
			logger.DebugContext(ctx, "BIOS update-needed annotation cleared successfully")
		}
	}

	// Remove firmware update annotation if it exists
	if _, exists := bmh.Annotations[FirmwareUpdateNeededAnnotation]; exists {
		hasFirmware = true
		logger.DebugContext(ctx, "Clearing firmware update-needed annotation",
			slog.String("annotation", FirmwareUpdateNeededAnnotation))
		if err := updateBMHMetaWithRetry(ctx, c, logger, bmhName, MetaTypeAnnotation, FirmwareUpdateNeededAnnotation, "", OpRemove); err != nil {
			logger.WarnContext(ctx, "Failed to remove firmware update annotation",
				slog.Any("error", err))
			errs = append(errs, fmt.Errorf("firmware annotation removal failed: %w", err))
		} else {
			logger.DebugContext(ctx, "Firmware update-needed annotation cleared successfully")
		}
	}

	if !hasBios && !hasFirmware {
		logger.DebugContext(ctx, "No update-needed annotations to clear")
	}

	// Return combined errors if any occurred
	if len(errs) > 0 {
		return fmt.Errorf("failed to remove some BMH update annotations from BMH %s: %v", bmh.Name, errs)
	}

	return nil
}

// deleteDataImageIfExists deletes the DataImage CR matching the given BMH name/namespace,
// if it exists. The IBI operator creates a DataImage during installation but never cleans
// it up. If left around, BMO will spuriously re-mount the virtual media during BMH status
// transitions (e.g. day-2 firmware updates). This causes stuck finalizers during
// deprovisioning.
func deleteDataImageIfExists(ctx context.Context, c client.Client, logger *slog.Logger, bmhName, bmhNamespace string) error {
	dataImage := &metal3v1alpha1.DataImage{
		ObjectMeta: metav1.ObjectMeta{
			Name:      bmhName,
			Namespace: bmhNamespace,
		},
	}

	if err := c.Delete(ctx, dataImage); err != nil {
		if k8serrors.IsNotFound(err) {
			return nil
		}
		return fmt.Errorf("failed to delete DataImage %s/%s: %w", bmhNamespace, bmhName, err)
	}

	logger.InfoContext(ctx, "Deleted DataImage to prevent spurious re-mount after firmware updates",
		slog.String("dataimage", bmhName), slog.String("namespace", bmhNamespace))

	return nil
}
