/*
Copyright 2024.

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 clusterupgradeoperation

import (
	"context"
	"fmt"
	"time"

	"github.com/go-logr/logr"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/client-go/tools/record"
	"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"
	"sigs.k8s.io/controller-runtime/pkg/log"
	"sigs.k8s.io/controller-runtime/pkg/reconcile"

	o11yv1 "github.com/tidbcloud/observability/api/regionalserver/resourceworker/v1"
	"github.com/tidbcloud/observability/pkg/common/constants"
	"github.com/tidbcloud/observability/pkg/common/structs"
	"github.com/tidbcloud/observability/pkg/resourceworker/controllers/clusterupgradeoperation/providers"
)

var (
	requeue  = reconcile.Result{RequeueAfter: interval}
	interval = 10 * time.Second
)

const (
	normalEvent  = "Normal"
	warningEvent = "Warning"
)

// updateStatusWithRetry updates the status with conflict retry. On conflict (object modified),
// re-fetches the latest object, applies the status from op, and retries.
func (r *ClusterUpgradeOperationReconciler) updateStatusWithRetry(ctx context.Context, op *o11yv1.ClusterUpgradeOperation) error {
	statusToApply := op.Status.DeepCopy()
	return retry.RetryOnConflict(retry.DefaultRetry, func() error {
		latest := &o11yv1.ClusterUpgradeOperation{}
		if err := r.Get(ctx, client.ObjectKeyFromObject(op), latest); err != nil {
			return err
		}
		latest.Status = *statusToApply
		return r.Status().Update(ctx, latest)
	})
}

// ClusterUpgradeOperationReconciler reconciles a ClusterUpgradeOperation object
type ClusterUpgradeOperationReconciler struct {
	client.Client
	Scheme                *runtime.Scheme
	ServiceInfo           structs.ServiceInfo
	eventRecorder         record.EventRecorder
	cloudProviderRegistry *providers.CloudProviderRegistry
}

// NewClusterUpgradeOperationReconciler creates a new reconciler
func NewClusterUpgradeOperationReconciler(
	client client.Client,
	scheme *runtime.Scheme,
	serviceInfo structs.ServiceInfo,
	eventRecorder record.EventRecorder,
) (*ClusterUpgradeOperationReconciler, error) {
	registry, err := providers.NewCloudProviderRegistry(
		o11yv1.CloudProvider(constants.VendorProviderMap[constants.ProvidedVendor(serviceInfo.Vendor)]),
		serviceInfo.Region,
		client,
	)
	if err != nil {
		return nil, fmt.Errorf("failed to create cloud provider registry: %w", err)
	}
	return &ClusterUpgradeOperationReconciler{
		Client:                client,
		Scheme:                scheme,
		ServiceInfo:           serviceInfo,
		eventRecorder:         eventRecorder,
		cloudProviderRegistry: registry,
	}, nil
}

// SetupWithManager sets up the controller with the Manager
func (r *ClusterUpgradeOperationReconciler) SetupWithManager(mgr ctrl.Manager) error {
	return ctrl.NewControllerManagedBy(mgr).
		For(&o11yv1.ClusterUpgradeOperation{}).
		Complete(r)
}

//+kubebuilder:rbac:groups=observability.pingcap.com,resources=clusterupgradeoperations,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=observability.pingcap.com,resources=clusterupgradeoperations/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=observability.pingcap.com,resources=clusterupgradeoperations/finalizers,verbs=update

// Reconcile is the main reconciliation loop
func (r *ClusterUpgradeOperationReconciler) Reconcile(ctx context.Context, req ctrl.Request) (res ctrl.Result, _ error) {
	logger := log.FromContext(ctx)
	logger.Info("Reconciling ClusterUpgradeOperation")

	// Fetch the ClusterUpgradeOperation CR
	op := &o11yv1.ClusterUpgradeOperation{}
	if err := r.Get(ctx, req.NamespacedName, op); err != nil {
		if apierrors.IsNotFound(err) {
			// Resource was deleted, expected - no need to log as error
			return ctrl.Result{}, nil
		}
		LogAndEmitEvent(logger, r.eventRecorder, op, warningEvent, "Failed to get the ClusterUpgradeOperation CR", err)
		return ctrl.Result{}, err
	}

	// Handle deletion: always remove finalizer so the CR can be deleted.
	// If phase is Running, we allow cancel-by-delete; in-flight cloud operations may continue but are no longer tracked.
	if !op.DeletionTimestamp.IsZero() {
		if !controllerutil.ContainsFinalizer(op, o11yv1.ClusterUpgradeOperationFinalizer) {
			return ctrl.Result{}, nil
		}
		if err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
			latest := &o11yv1.ClusterUpgradeOperation{}
			if err := r.Get(ctx, client.ObjectKeyFromObject(op), latest); err != nil {
				return err
			}
			controllerutil.RemoveFinalizer(latest, o11yv1.ClusterUpgradeOperationFinalizer)
			return r.Update(ctx, latest)
		}); err != nil {
			logger.Error(err, "failed to remove the finalizer from ClusterUpgradeOperation")
			return ctrl.Result{}, err
		}
		logger.Info("removed finalizer for ClusterUpgradeOperation", "phase", op.Status.Phase)
		return ctrl.Result{}, nil
	}

	// Add finalizer if not present
	if !controllerutil.ContainsFinalizer(op, o11yv1.ClusterUpgradeOperationFinalizer) {
		if err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
			latest := &o11yv1.ClusterUpgradeOperation{}
			if err := r.Get(ctx, client.ObjectKeyFromObject(op), latest); err != nil {
				return err
			}
			controllerutil.AddFinalizer(latest, o11yv1.ClusterUpgradeOperationFinalizer)
			return r.Update(ctx, latest)
		}); err != nil {
			return ctrl.Result{}, err
		}
	}

	// Skip if paused
	if op.Spec.Paused {
		LogAndEmitEvent(logger, r.eventRecorder, op, normalEvent, "ClusterUpgradeOperation is paused, skip processing", nil)
		return ctrl.Result{RequeueAfter: interval}, nil
	}

	// Initialize phase if empty
	if op.Status.Phase == "" {
		op.Status.Phase = o11yv1.ClusterUpgradeOperationPhasePending
		op.Status.NextSteps = op.Spec.Steps
	}

	// Handle based on current phase
	switch op.Status.Phase {
	case o11yv1.ClusterUpgradeOperationPhasePending:
		return r.reconcilePending(ctx, op)
	case o11yv1.ClusterUpgradeOperationPhaseRunning:
		return r.reconcileRunning(ctx, op)
	case o11yv1.ClusterUpgradeOperationPhaseFailed, o11yv1.ClusterUpgradeOperationPhaseCompleted:
		return r.reconcileFinished(ctx, op)
	}

	return ctrl.Result{}, nil
}

// reconcilePending handles the Pending phase
func (r *ClusterUpgradeOperationReconciler) reconcilePending(ctx context.Context, op *o11yv1.ClusterUpgradeOperation) (ctrl.Result, error) {
	logger := log.FromContext(ctx)

	// Check if scheduled time has passed
	if time.Now().Before(op.Spec.Schedule.Time) {
		waitDuration := time.Until(op.Spec.Schedule.Time)
		LogAndEmitEvent(logger, r.eventRecorder, op, normalEvent, fmt.Sprintf("Waiting for scheduled time, %v remaining", waitDuration), nil)
		return ctrl.Result{RequeueAfter: waitDuration}, nil
	}

	// Transition to Running
	op.Status.Phase = o11yv1.ClusterUpgradeOperationPhaseRunning
	op.UpdateCurrentStep(time.Now())

	if err := r.updateStatusWithRetry(ctx, op); err != nil {
		return ctrl.Result{}, err
	}

	return requeue, nil
}

// reconcileRunning handles the Running phase
func (r *ClusterUpgradeOperationReconciler) reconcileRunning(ctx context.Context, op *o11yv1.ClusterUpgradeOperation) (ctrl.Result, error) {
	logger := log.FromContext(ctx)
	logger.Info("Processing running ClusterUpgradeOperation", "currentStep", op.Status.CurrentStep)

	// If no current step, get the next one
	if op.Status.CurrentStep == nil {
		op.UpdateCurrentStep(time.Now())
		if op.Status.Phase == o11yv1.ClusterUpgradeOperationPhaseCompleted {
			return r.reconcileFinished(ctx, op)
		}
	}

	// Process current step based on processor type
	switch op.Status.CurrentStep.Processor {
	case o11yv1.ProcessorUpgradeControlPlane:
		return r.processUpgradeControlPlane(ctx, op)
	case o11yv1.ProcessorUpgradeClusterAddons:
		return r.processUpgradeClusterAddons(ctx, op)
	case o11yv1.ProcessorUpgradeNodeGroup:
		return r.processUpgradeNodeGroup(ctx, op)
	default:
		op.FailOperation(fmt.Sprintf("Unknown processor: %s", op.Status.CurrentStep.Processor))
		if err := r.updateStatusWithRetry(ctx, op); err != nil {
			return ctrl.Result{}, err
		}
		return ctrl.Result{}, nil
	}

}

// reconcileFinished handles the Completed/Failed phases
func (r *ClusterUpgradeOperationReconciler) reconcileFinished(ctx context.Context, op *o11yv1.ClusterUpgradeOperation) (ctrl.Result, error) {
	logger := log.FromContext(ctx)

	// Handle TTL-based cleanup
	if op.Spec.TTLSeconds != nil && op.Status.EndTime != nil {
		deleteAfter := op.Status.EndTime.Add(op.Spec.TTLSeconds.Duration)
		if time.Now().After(deleteAfter) {
			logger.Info("delete ClusterUpgradeOperation due to TTL expiry")
			if err := r.Delete(ctx, op); err != nil {
				return ctrl.Result{}, err
			}
			return ctrl.Result{}, nil
		}
		return ctrl.Result{RequeueAfter: time.Until(deleteAfter)}, nil
	}

	LogAndEmitEvent(logger, r.eventRecorder, op, normalEvent, fmt.Sprintf("ClusterUpgradeOperation %s", op.Status.Phase), nil)
	return ctrl.Result{}, nil
}

// LogAndEmitEvent logs the event and emits it using the provided logger and eventRecorder
func LogAndEmitEvent(logger logr.Logger, eventRecorder record.EventRecorder, obj runtime.Object, eventType, msg string, err error) {
	if err != nil {
		logger.Error(err, msg)
		eventRecorder.Event(obj, eventType, "Error", fmt.Sprintf("%s: %v", msg, err))
	} else {
		logger.Info(msg)
		eventRecorder.Event(obj, eventType, "Info", msg)
	}
}
