package upgradeoperation

import (
	"context"
	"fmt"
	"time"

	"github.com/go-logr/logr"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/client-go/tools/record"
	"k8s.io/client-go/util/workqueue"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/builder"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
	"sigs.k8s.io/controller-runtime/pkg/log"
	"sigs.k8s.io/controller-runtime/pkg/predicate"

	"github.com/tidbcloud/infra-provider/pkg/cloud/aws"
	"github.com/tidbcloud/infra-provider/pkg/cloud/gcp"
	"github.com/tidbcloud/infra-provider/pkg/consts"
	utiltime "github.com/tidbcloud/infra-provider/pkg/util/time"
	"github.com/tidbcloud/infra-provider/regional-server/api/v1alpha1"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/common/acscli"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/common/clustercli"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/config"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/controllers"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/controllers/upgradeoperation/common"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/controllers/upgradeoperation/eks"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/controllers/upgradeoperation/gke"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/resourcemanager/appbox"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/resourcemanager/upgradeoperation"
)

const (
	defaultBaseDelay = 5 * time.Second
	defaultMaxDelay  = 2 * time.Minute
)

type Reconciler struct {
	client        client.Client
	Log           logr.Logger
	eventRecorder record.EventRecorder
	appBoxRW      appbox.APPBoxReaderWriter
	uoRW          upgradeoperation.ReaderWriter
	clock         utiltime.Clock
	upgrader      common.Upgrader
}

func NewReconciler(
	client client.Client,
	log logr.Logger,
	recorder record.EventRecorder,
	provider string,
	cli clustercli.ClusterClient,
	gcpCli gcp.Client,
	awsCli aws.Client,
	manager *controllers.Manager,
	tmplConfig config.TemplatesConfig,
	appboxConfig config.APPBoxConfig,
	acsCli acscli.Interface,
) *Reconciler {
	var upgrader common.Upgrader
	switch provider {
	case consts.ProviderTypeEKS:
		upgrader = eks.NewUpgrader(client, cli, recorder, manager, tmplConfig, awsCli, appboxConfig, acsCli)
	case consts.ProviderTypeGKE:
		upgrader = gke.NewUpgrader(client, gcpCli, cli, recorder, appboxConfig, acsCli)
	}

	return &Reconciler{
		client:        client,
		upgrader:      upgrader,
		uoRW:          upgradeoperation.NewReaderWriter(client),
		appBoxRW:      appbox.NewAPPBoxReaderWriter(client),
		Log:           log,
		eventRecorder: recorder,
		clock:         utiltime.RealClock{},
	}
}

func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (res ctrl.Result, _ error) {
	logger := log.FromContext(ctx)
	logger.Info("Reconciling UpgradeOperation")
	defer func() { logger.Info("Finish reconcile", "Requeue", res.Requeue, "RequeueAfter", res.RequeueAfter) }()

	var uo v1alpha1.UpgradeOperation
	if err := r.uoRW.Get(ctx, req.NamespacedName, &uo); err != nil {
		if apierrors.IsNotFound(err) {
			logger.Info("UpgradeOperation is not found, may has been deleted")
		} else {
			common.LogAndEmitEvent(logger, r.eventRecorder, &uo, common.WarningEvent, "Failed to get the UpgradeOperation CR", err)
		}
		return ctrl.Result{}, nil
	}

	// Handle the deletion reconciliation loop
	if uo.GetDeletionTimestamp() != nil {
		if uo.Status.Phase == v1alpha1.UpgradeOperationPhaseRunning {
			// We need to wait for the UpgradeOperation to be completed or failed before deleting it
			logger.Info("wait for UpgradeOperation to complete/fail before deleting it")
			// Will go to `handleRunning`
		} else {
			controllerutil.RemoveFinalizer(&uo, v1alpha1.UpgradeOperationFinalizer)
			if err := r.uoRW.Update(ctx, &uo); err != nil {
				logger.Error(err, "failed to remove the finalizer from UpgradeOperation")
				return ctrl.Result{}, err
			}
			logger.Info("remove the finalizer from UpgradeOperation")
			return ctrl.Result{}, nil
		}
	}

	if uo.Spec.Paused {
		common.LogAndEmitEvent(logger, r.eventRecorder, &uo, common.NormalEvent, "UpgradeOperation is paused, will not process it", nil)
		return ctrl.Result{}, nil
	}

	switch uo.Status.Phase {
	case v1alpha1.UpgradeOperationPhaseNone:
		uo.Status.Phase = v1alpha1.UpgradeOperationPhasePending
		uo.Status.FinishedSteps = v1alpha1.NewSteps()
		uo.Status.NextSteps = v1alpha1.NewSteps()
		return ctrl.Result{}, r.uoRW.UpdateStatus(ctx, &uo)

	case v1alpha1.UpgradeOperationPhasePending:
		return r.handlePending(ctx, &uo)

	case v1alpha1.UpgradeOperationPhaseRunning:
		return r.handleRunning(ctx, &uo)

	case v1alpha1.UpgradeOperationPhaseCompleted, v1alpha1.UpgradeOperationPhaseFailed:
		return r.handleCompletedOrFailed(ctx, &uo)
	default:
		logger.Info("unknown phase, ignore it")
		return ctrl.Result{}, nil
	}
}

func (r *Reconciler) handlePending(ctx context.Context, uo *v1alpha1.UpgradeOperation) (ctrl.Result, error) {
	logger := log.FromContext(ctx)

	// We have already checked the `schedule` field in the webhook, so we can ignore the error here.
	schedule, _ := time.Parse(v1alpha1.UpgradeOperationScheduleLayout, uo.Spec.Schedule)
	now := r.clock.Now()
	if now.After(schedule) {
		uo.Status.NextSteps = uo.Spec.Steps
		uo.Status.Phase = v1alpha1.UpgradeOperationPhaseRunning
		uo.Status.Reason = "Begin to upgrade"
		common.LogAndEmitEvent(logger, r.eventRecorder, uo, common.NormalEvent, "The schedule is reached", nil)
	} else {
		if uo.Status.Reason == common.PendingReason {
			interval := schedule.Sub(now)
			common.LogAndEmitEvent(logger, r.eventRecorder, uo, common.NormalEvent, "Wait for the schedule", nil)
			return ctrl.Result{RequeueAfter: interval}, nil
		} else {
			uo.Status.Reason = common.PendingReason
		}
	}
	return ctrl.Result{}, r.uoRW.UpdateStatus(ctx, uo)
}

func (r *Reconciler) handleRunning(ctx context.Context, uo *v1alpha1.UpgradeOperation) (ctrl.Result, error) {
	logger := log.FromContext(ctx)

	appBox, err := r.getAppBox(ctx, uo)
	if err != nil {
		return ctrl.Result{}, fmt.Errorf("failed to get the related appbox: %w", err)
	}

	if uo.Status.CurrentStep == nil {
		uo.UpdateCurrentStep(r.clock.Now())
		if uo.Status.Phase == v1alpha1.UpgradeOperationPhaseCompleted {
			if err := common.ResumeReconcile(ctx, r.appBoxRW, appBox); err != nil {
				return ctrl.Result{}, fmt.Errorf("failed to unpause appbox: %w", err)
			}
			common.LogAndEmitEvent(logger, r.eventRecorder, uo, common.NormalEvent, "Unpaused the appbox", nil)
		}
		return ctrl.Result{}, r.uoRW.UpdateStatus(ctx, uo)
	}

	target := uo.Status.CurrentStep.Target

	// Check if there is a delay configured for this step
	if delayStr := uo.Status.CurrentStep.Options["delay"]; delayStr != "" {
		delay, err := time.ParseDuration(delayStr)
		if err != nil {
			common.LogAndEmitEvent(logger, r.eventRecorder, uo, common.WarningEvent, fmt.Sprintf("Invalid delay format %q: %v", delayStr, err), nil)
		} else if uo.Status.CurrentStep.BeginTime != nil {
			stepStartTime := uo.Status.CurrentStep.BeginTime.Add(delay)
			now := r.clock.Now()
			if now.Before(stepStartTime) {
				interval := stepStartTime.Sub(now)
				common.LogAndEmitEvent(logger, r.eventRecorder, uo, common.NormalEvent, fmt.Sprintf("Waiting for delay %s before executing step %s", delayStr, target), nil)
				return ctrl.Result{RequeueAfter: interval}, nil
			}
		}
	}

	// When upgrade the k8s control plane, if we directly modify the version in appbox's spec,
	// both the control plane and add-ons will be upgraded.
	// To avoid this, we will pause the appbox's reconcile.
	// Skip pause for addons (doesn't modify appBox spec) and o11y nodes (upgrade via ACS, independent of appBox).
	if target != v1alpha1.Addons && target != v1alpha1.O11yK8sNodes && target != v1alpha1.O11yTiDBNodes {
		updated, err := common.PauseReconcile(ctx, r.appBoxRW, appBox)
		if err != nil {
			return ctrl.Result{}, fmt.Errorf("failed to pause appbox's reconcile, err: %w", err)
		}
		if updated {
			common.LogAndEmitEvent(logger, r.eventRecorder, uo, common.NormalEvent, "Paused the appbox", nil)
		}
	}

	if res, err := common.HandlePreStepMaintenance(ctx, r.client, uo, logger); err != nil {
		return ctrl.Result{}, err
	} else if res.Requeue || res.RequeueAfter > 0 {
		return res, nil
	}

	switch target {
	case v1alpha1.ControlPlane:
		return r.upgrader.UpgradeControlPlane(ctx, uo, appBox)
	case v1alpha1.AdminNodes:
		return r.upgrader.UpgradeAdminNodes(ctx, uo, appBox)
	case v1alpha1.TiDBNodes, v1alpha1.DMNodes, v1alpha1.CDCNodes:
		return r.upgrader.UpgradeDataNodes(ctx, uo, appBox)
	case v1alpha1.EmptyNodePools:
		return r.upgrader.UpgradeEmptyNodePools(ctx, uo, appBox)
	case v1alpha1.Addons:
		return r.upgrader.UpgradeAddons(ctx, uo, appBox)
	case v1alpha1.O11yK8sNodes, v1alpha1.O11yTiDBNodes:
		return r.upgrader.UpgradeO11yNodes(ctx, uo, appBox)
	}

	logger.Info("should not reach here")
	return ctrl.Result{}, nil
}

func (r *Reconciler) handleCompletedOrFailed(ctx context.Context, uo *v1alpha1.UpgradeOperation) (ctrl.Result, error) {
	logger := log.FromContext(ctx)

	now := r.clock.Now()
	var timeToDelete time.Time
	if uo.Status.EndTime != nil {
		timeToDelete = uo.Status.EndTime.Add(uo.Spec.TTL.Duration)
	} else if uo.Status.BeginTime != nil {
		timeToDelete = uo.Status.BeginTime.Add(uo.Spec.TTL.Duration)
	} else {
		timeToDelete = uo.GetCreationTimestamp().Add(uo.Spec.TTL.Duration)
	}

	logger.WithValues("TTL", uo.Spec.TTL.Duration, "now", now, "timeToDelete", timeToDelete)

	if now.After(timeToDelete) {
		logger.Info("UpgradeOperation can be deleted")
		return ctrl.Result{}, r.uoRW.Delete(ctx, uo)
	}

	interval := timeToDelete.Sub(now)
	common.LogAndEmitEvent(logger, r.eventRecorder, uo, common.NormalEvent, "Wait for the UpgradeOperation to be deleted", nil)
	return ctrl.Result{RequeueAfter: interval}, nil
}

func (r *Reconciler) getAppBox(ctx context.Context, uo *v1alpha1.UpgradeOperation) (*v1alpha1.APPBox, error) {
	var appBox v1alpha1.APPBox
	if err := r.appBoxRW.Get(ctx, client.ObjectKey{Namespace: uo.Namespace, Name: uo.Spec.AppBoxName}, &appBox); err != nil {
		if apierrors.IsNotFound(err) {
			uo.Status.Phase = v1alpha1.UpgradeOperationPhaseFailed
			if err := uo.Fail(ctx, r.client, fmt.Sprintf("The appbox CR %s/%s is not found", uo.Namespace, uo.Spec.AppBoxName)); err != nil {
				return nil, fmt.Errorf("failed to update UpgradeOperation status, err: %w", err)
			}
		}
		return nil, fmt.Errorf("failed to get the appbox %s/%s, err: %w", uo.Namespace, uo.Spec.AppBoxName, err)
	}

	return &appBox, nil
}

func (r *Reconciler) SetupWithManager(mgr ctrl.Manager, pds ...predicate.Predicate) error {
	return ctrl.NewControllerManagedBy(mgr).
		For(&v1alpha1.UpgradeOperation{}, builder.WithPredicates(pds...)).
		WithOptions(controller.Options{
			MaxConcurrentReconciles: 1,
			RateLimiter:             workqueue.NewTypedItemExponentialFailureRateLimiter[ctrl.Request](defaultBaseDelay, defaultMaxDelay),
		}).
		Complete(r)
}
