/*
SPDX-FileCopyrightText: Red Hat

SPDX-License-Identifier: Apache-2.0
*/

package utils

import (
	"context"
	"fmt"
	"log/slog"
	"time"

	"k8s.io/apimachinery/pkg/api/meta"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/client-go/util/retry"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"

	hwmgmtv1alpha1 "github.com/openshift-kni/oran-o2ims/api/hardwaremanagement/v1alpha1"
	ctlrutils "github.com/openshift-kni/oran-o2ims/internal/controllers/utils"
)

const (
	NodeAllocationRequestFinalizer = "clcm.openshift.io/nodeallocationrequest-finalizer"
)

var nodeAllocationRequestGVK schema.GroupVersionKind

func InitNodeAllocationRequestUtils(scheme *runtime.Scheme) error {
	nodeAllocationRequest := &hwmgmtv1alpha1.NodeAllocationRequest{}
	gvks, unversioned, err := scheme.ObjectKinds(nodeAllocationRequest)
	if err != nil {
		return fmt.Errorf("failed to query scheme to get GVK for NodeAllocationRequest CR: %w", err)
	}
	if unversioned || len(gvks) != 1 {
		return fmt.Errorf("expected a single versioned item in ObjectKinds response, got %d with unversioned=%t", len(gvks), unversioned)
	}

	nodeAllocationRequestGVK = gvks[0]

	return nil
}

func GetNodeAllocationRequest(ctx context.Context, client client.Reader, key client.ObjectKey, nodeAllocationRequest *hwmgmtv1alpha1.NodeAllocationRequest) error {
	if err := client.Get(ctx, key, nodeAllocationRequest); err != nil {
		return fmt.Errorf("failed to get NodeAllocationRequest: %w", err)
	}

	if nodeAllocationRequest.Kind == "" {
		// The non-caching query doesn't set the GVK for the CR, so do it now
		nodeAllocationRequest.SetGroupVersionKind(nodeAllocationRequestGVK)
	}

	return nil
}

func UpdateNodeAllocationRequestObservedGeneration(
	ctx context.Context,
	c client.Client,
	nodeAllocationRequest *hwmgmtv1alpha1.NodeAllocationRequest) error {

	// nolint: wrapcheck
	err := ctlrutils.RetryOnConflictOrRetriable(retry.DefaultRetry, func() error {
		newNodeAllocationRequest := &hwmgmtv1alpha1.NodeAllocationRequest{}
		if err := c.Get(ctx, client.ObjectKeyFromObject(nodeAllocationRequest), newNodeAllocationRequest); err != nil {
			return err
		}
		newNodeAllocationRequest.Status.ObservedGeneration = newNodeAllocationRequest.ObjectMeta.Generation
		if err := c.Status().Update(ctx, newNodeAllocationRequest); err != nil {
			return err
		}
		return nil
	})

	if err != nil {
		return fmt.Errorf("failed to update NodeAllocationRequest condition: %w", err)
	}

	return nil
}

// UpdateNodeAllocationRequestObservedStatus acknowledges all spec changes by updating
// both ObservedGeneration and ObservedConfigTransactionId in a single status update.
func UpdateNodeAllocationRequestObservedStatus(
	ctx context.Context,
	c client.Client,
	nodeAllocationRequest *hwmgmtv1alpha1.NodeAllocationRequest) error {

	// nolint: wrapcheck
	err := ctlrutils.RetryOnConflictOrRetriable(retry.DefaultRetry, func() error {
		newNodeAllocationRequest := &hwmgmtv1alpha1.NodeAllocationRequest{}
		if err := c.Get(ctx, client.ObjectKeyFromObject(nodeAllocationRequest), newNodeAllocationRequest); err != nil {
			return err
		}
		newNodeAllocationRequest.Status.ObservedGeneration = newNodeAllocationRequest.ObjectMeta.Generation
		newNodeAllocationRequest.Status.ObservedConfigTransactionId = newNodeAllocationRequest.Spec.ConfigTransactionId
		if err := c.Status().Update(ctx, newNodeAllocationRequest); err != nil {
			return err
		}
		return nil
	})

	if err != nil {
		return fmt.Errorf("failed to update NodeAllocationRequest observed generation and transaction id: %w", err)
	}

	return nil
}

func UpdateNodeAllocationRequestStatusCondition(
	ctx context.Context,
	c client.Client,
	nodeAllocationRequest *hwmgmtv1alpha1.NodeAllocationRequest,
	conditionType hwmgmtv1alpha1.ConditionType,
	conditionReason hwmgmtv1alpha1.ConditionReason,
	conditionStatus metav1.ConditionStatus,
	message string) error {

	SetStatusCondition(&nodeAllocationRequest.Status.Conditions,
		string(conditionType),
		string(conditionReason),
		conditionStatus,
		message)

	// nolint: wrapcheck
	err := ctlrutils.RetryOnConflictOrRetriable(retry.DefaultRetry, func() error {
		newNodeAllocationRequest := &hwmgmtv1alpha1.NodeAllocationRequest{}
		if err := c.Get(ctx, client.ObjectKeyFromObject(nodeAllocationRequest), newNodeAllocationRequest); err != nil {
			return err
		}

		SetStatusCondition(&newNodeAllocationRequest.Status.Conditions,
			string(conditionType),
			string(conditionReason),
			conditionStatus,
			message)

		// Update the observed config transaction id if the condition is Configured
		if conditionType == hwmgmtv1alpha1.Configured {
			newNodeAllocationRequest.Status.ObservedConfigTransactionId = nodeAllocationRequest.Spec.ConfigTransactionId
		}

		// Apply start time logic to the fresh object to avoid race conditions.
		// This ensures we work with the latest state from the API server.
		now := metav1.NewTime(time.Now())
		switch conditionType {
		case hwmgmtv1alpha1.Provisioned:
			if conditionStatus == metav1.ConditionFalse &&
				conditionReason != hwmgmtv1alpha1.TimedOut &&
				conditionReason != hwmgmtv1alpha1.Failed {

				// Set start time when provisioning operation starts
				if conditionReason == hwmgmtv1alpha1.InProgress &&
					newNodeAllocationRequest.Status.HardwareOperationStartTime == nil {
					// Set start time to track hardware operation timeout
					// Only set once when first transitioning to InProgress
					newNodeAllocationRequest.Status.HardwareOperationStartTime = &now
				}
			} else if conditionStatus == metav1.ConditionTrue ||
				conditionReason == hwmgmtv1alpha1.Completed ||
				conditionReason == hwmgmtv1alpha1.Failed ||
				conditionReason == hwmgmtv1alpha1.TimedOut {
				// Clear start time when provisioning operation completes, fails, or times out
				newNodeAllocationRequest.Status.HardwareOperationStartTime = nil
			}

		case hwmgmtv1alpha1.Configured:
			if conditionStatus == metav1.ConditionFalse &&
				conditionReason != hwmgmtv1alpha1.TimedOut &&
				conditionReason != hwmgmtv1alpha1.Failed {

				// Set/reset start time when configuration operation starts
				// Only set when nil (first time) or when transitioning from terminal state
				if conditionReason == hwmgmtv1alpha1.InProgress {
					// Check if we're transitioning FROM a terminal state
					existingCondition := meta.FindStatusCondition(newNodeAllocationRequest.Status.Conditions, string(hwmgmtv1alpha1.Configured))
					isRetryAfterTerminal := existingCondition != nil &&
						(existingCondition.Reason == string(hwmgmtv1alpha1.TimedOut) ||
							existingCondition.Reason == string(hwmgmtv1alpha1.Failed))

					// Set start time only if:
					// 1. Start time is nil (first time entering config state), OR
					// 2. Retrying after a terminal state (timeout/failure)
					if newNodeAllocationRequest.Status.HardwareOperationStartTime == nil || isRetryAfterTerminal {
						newNodeAllocationRequest.Status.HardwareOperationStartTime = &now
					}
				}
			} else if conditionStatus == metav1.ConditionTrue ||
				conditionReason == hwmgmtv1alpha1.ConfigApplied ||
				conditionReason == hwmgmtv1alpha1.Failed ||
				conditionReason == hwmgmtv1alpha1.TimedOut {
				// Clear start time when configuration operation completes, fails, or times out
				newNodeAllocationRequest.Status.HardwareOperationStartTime = nil
			}
		}

		if err := c.Status().Update(ctx, newNodeAllocationRequest); err != nil {
			return err
		}
		return nil
	})

	if err != nil {
		return fmt.Errorf("failed to update NodeAllocationRequest condition: %s, %w", nodeAllocationRequest.Name, err)
	}

	return nil
}

func UpdateNodeAllocationRequestProperties(
	ctx context.Context,
	c client.Client,
	logger *slog.Logger,
	nodeAllocationRequest *hwmgmtv1alpha1.NodeAllocationRequest) error {

	// nolint: wrapcheck
	err := ctlrutils.RetryOnConflictOrRetriable(retry.DefaultRetry, func() error {
		newNodeAllocationRequest := &hwmgmtv1alpha1.NodeAllocationRequest{}
		if err := c.Get(ctx, client.ObjectKeyFromObject(nodeAllocationRequest), newNodeAllocationRequest); err != nil {
			return err
		}

		// Merge node names: union of what the API server has with the in-memory list
		existing := make(map[string]struct{}, len(newNodeAllocationRequest.Status.Properties.NodeNames))
		for _, name := range newNodeAllocationRequest.Status.Properties.NodeNames {
			existing[name] = struct{}{}
		}
		for _, name := range nodeAllocationRequest.Status.Properties.NodeNames {
			if _, found := existing[name]; !found {
				newNodeAllocationRequest.Status.Properties.NodeNames = append(
					newNodeAllocationRequest.Status.Properties.NodeNames, name)
			}
		}

		if err := c.Status().Update(ctx, newNodeAllocationRequest); err != nil {
			return err
		}
		return nil
	})

	if err != nil {
		return fmt.Errorf("failed to update NodeAllocationRequest properties: %w", err)
	}

	logger.InfoContext(ctx, "Updated NodeAllocationRequest properties",
		slog.Int("nodeCount", len(nodeAllocationRequest.Status.Properties.NodeNames)))
	return nil
}

func NodeAllocationRequestAddFinalizer(
	ctx context.Context,
	c client.Client,
	nodeAllocationRequest *hwmgmtv1alpha1.NodeAllocationRequest,
) error {
	// nolint: wrapcheck
	err := ctlrutils.RetryOnConflictOrRetriable(retry.DefaultRetry, func() error {
		newNodeAllocationRequest := &hwmgmtv1alpha1.NodeAllocationRequest{}
		if err := c.Get(ctx, client.ObjectKeyFromObject(nodeAllocationRequest), newNodeAllocationRequest); err != nil {
			return err
		}
		controllerutil.AddFinalizer(newNodeAllocationRequest, NodeAllocationRequestFinalizer)
		if err := c.Update(ctx, newNodeAllocationRequest); err != nil {
			return err
		}
		return nil
	})
	if err != nil {
		return fmt.Errorf("failed to add finalizer to NodeAllocationRequest: %w", err)
	}
	return nil
}

func NodeAllocationRequestRemoveFinalizer(
	ctx context.Context,
	c client.Client,
	nodeAllocationRequest *hwmgmtv1alpha1.NodeAllocationRequest,
) error {
	// nolint: wrapcheck
	err := ctlrutils.RetryOnConflictOrRetriable(retry.DefaultRetry, func() error {
		newNodeAllocationRequest := &hwmgmtv1alpha1.NodeAllocationRequest{}
		if err := c.Get(ctx, client.ObjectKeyFromObject(nodeAllocationRequest), newNodeAllocationRequest); err != nil {
			return err
		}
		controllerutil.RemoveFinalizer(newNodeAllocationRequest, NodeAllocationRequestFinalizer)
		if err := c.Update(ctx, newNodeAllocationRequest); err != nil {
			return err
		}
		return nil
	})
	if err != nil {
		return fmt.Errorf("failed to remove finalizer from NodeAllocationRequest: %w", err)
	}
	return nil
}
