/*
SPDX-FileCopyrightText: Red Hat

SPDX-License-Identifier: Apache-2.0
*/

package controller

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

	metal3v1alpha1 "github.com/metal3-io/baremetal-operator/apis/metal3.io/v1alpha1"
	hwmgmtv1alpha1 "github.com/openshift-kni/oran-o2ims/api/hardwaremanagement/v1alpha1"
	ctlrutils "github.com/openshift-kni/oran-o2ims/internal/controllers/utils"
	"github.com/openshift-kni/oran-o2ims/internal/logging"
	"k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/runtime"

	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/log"

	hwmgrutils "github.com/openshift-kni/oran-o2ims/internal/hardwaremanager/utils"
)

// AllocatedNodeReconciler reconciles AllocatedNode objects for the hardware manager
type AllocatedNodeReconciler struct {
	ctrl.Manager
	client.Client
	NoncachedClient client.Reader
	Scheme          *runtime.Scheme
	Logger          *slog.Logger
	Namespace       string
}

// +kubebuilder:rbac:groups=clcm.openshift.io,resources=allocatednodes,verbs=get;create;list;watch;update;patch;delete
// +kubebuilder:rbac:groups=clcm.openshift.io,resources=allocatednodes/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=clcm.openshift.io,resources=allocatednodes/finalizers,verbs=update
func (r *AllocatedNodeReconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, err error) {
	_ = log.FromContext(ctx)
	startTime := time.Now()

	// Add standard reconciliation context
	ctx = ctlrutils.LogReconcileStart(ctx, r.Logger, req, "AllocatedNode")

	defer func() {
		duration := time.Since(startTime)
		if err != nil {
			r.Logger.ErrorContext(ctx, "Reconciliation failed",
				slog.Duration("duration", duration),
				slog.Any("error", err))
		} else {
			r.Logger.InfoContext(ctx, "Reconciliation completed",
				slog.Duration("duration", duration),
				slog.Bool("requeue", result.Requeue),
				slog.Duration("requeueAfter", result.RequeueAfter))
		}
	}()

	// Add logging context with the resource name
	ctx = logging.AppendCtx(ctx, slog.String("reconcileRequest", req.Name))

	allocatedNode, err := hwmgrutils.GetNode(ctx, r.Logger, r.NoncachedClient, req.Namespace, req.Name)
	if err != nil {
		if errors.IsNotFound(err) {
			r.Logger.InfoContext(ctx, "AllocatedNode not found, assuming deleted")
			return hwmgrutils.DoNotRequeue(), nil
		}
		ctlrutils.LogError(ctx, r.Logger, "Unable to fetch AllocatedNode", err)
		return hwmgrutils.RequeueWithShortInterval(), nil
	}

	// Add object-specific context
	ctx = ctlrutils.AddObjectContext(ctx, allocatedNode)
	ctx = logging.AppendCtx(ctx, slog.String("startingResourceVersion", allocatedNode.ResourceVersion))
	r.Logger.InfoContext(ctx, "Fetched AllocatedNode successfully")

	if allocatedNode.GetDeletionTimestamp() != nil {
		// Handle deletion
		r.Logger.InfoContext(ctx, "AllocatedNode is being deleted")
		if controllerutil.ContainsFinalizer(allocatedNode, hwmgrutils.AllocatedNodeFinalizer) {
			completed, deleteErr := r.handleAllocatedNodeDeletion(ctx, allocatedNode)
			if deleteErr != nil {
				return hwmgrutils.RequeueWithShortInterval(), fmt.Errorf("failed CleanupForDeletedNode: %w", deleteErr)
			}

			if !completed {
				r.Logger.InfoContext(ctx, "Node deletion handling in progress, requeueing")
				return hwmgrutils.RequeueWithShortInterval(), nil
			}

			if finalizerErr := hwmgrutils.AllocatedNodeRemoveFinalizer(ctx, r.NoncachedClient, r.Client, allocatedNode); finalizerErr != nil {
				r.Logger.InfoContext(ctx, "Failed to remove finalizer, requeueing", slog.Any("error", finalizerErr))
				return hwmgrutils.RequeueWithShortInterval(), nil
			}

			r.Logger.InfoContext(ctx, "Deletion handling complete, finalizer removed")
			return hwmgrutils.DoNotRequeue(), nil
		}

		r.Logger.InfoContext(ctx, "No finalizer, deletion handling complete")
		return hwmgrutils.DoNotRequeue(), nil
	}

	if !controllerutil.ContainsFinalizer(allocatedNode, hwmgrutils.AllocatedNodeFinalizer) {
		if finalizerErr := hwmgrutils.AllocatedNodeAddFinalizer(ctx, r.NoncachedClient, r.Client, allocatedNode); finalizerErr != nil {
			r.Logger.InfoContext(ctx, "Failed to add node finalizer, requeueing", slog.Any("error", finalizerErr))
			return hwmgrutils.RequeueWithShortInterval(), nil
		}
	}

	return hwmgrutils.DoNotRequeue(), nil
}

// SetupWithManager sets up the controller with the Manager.
func (r *AllocatedNodeReconciler) SetupWithManager(mgr ctrl.Manager) error {
	if err := ctrl.NewControllerManagedBy(mgr).
		For(&hwmgmtv1alpha1.AllocatedNode{}).
		Complete(r); err != nil {
		return fmt.Errorf("failed to create allocated node controller: %w", err)
	}

	return nil
}

// CleanupForDeletedNode
func (r *AllocatedNodeReconciler) handleAllocatedNodeDeletion(ctx context.Context, allocatednode *hwmgmtv1alpha1.AllocatedNode) (bool, error) {
	ctx = logging.AppendCtx(ctx, slog.String("node", allocatednode.Name))

	r.Logger.InfoContext(ctx, "handleAllocatedNodeDeletion")
	bmh, err := getBMHForNode(ctx, r.NoncachedClient, allocatednode)
	if err != nil {
		// If BMH is not found (e.g., manually deleted), allow the AllocatedNode deletion to proceed
		if errors.IsNotFound(err) {
			r.Logger.InfoContext(ctx, "BMH not found, assuming manually deleted — proceeding with AllocatedNode deletion",
				slog.String("bmh", allocatednode.Spec.HwMgrNodeId))
			return true, nil
		}
		return true, fmt.Errorf("failed to get BMH for node %s: %w", allocatednode.Name, err)
	}
	ctx = logging.AppendCtx(ctx, slog.String("bmh", bmh.Name))

	skipCleanup := allocatednode.Spec.SkipCleanup

	if !isBMHDeallocated(bmh) {
		if err = deallocateBMH(ctx, r.Client, r.Logger, bmh, skipCleanup); err != nil {
			return false, fmt.Errorf("failed to deallocate BMH: %w", err)
		}
		return false, nil
	}

	if !skipCleanup && isNodeProvisioningInProgress(allocatednode) {
		// Wait for BMH to transition to Available before powering off
		if bmh.Status.Provisioning.State != metal3v1alpha1.StateAvailable {
			r.Logger.InfoContext(ctx, "BMH not yet Available — waiting before powering off")
			return false, nil
		}
	}

	if bmh.Spec.Online && !skipCleanup {
		if err := patchBMHOnline(ctx, r.Client, bmh, false); err != nil {
			return false, fmt.Errorf("failed to set online=false for BMH %s: %w", bmh.Name, err)
		}
	}

	return true, clearBMHAnnotation(ctx, r.Client, r.Logger, bmh)
}
