package nodepool

import (
	"context"
	"fmt"
	"time"

	"github.com/go-logr/logr"
	corev1 "k8s.io/api/core/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/client-go/tools/record"
	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/predicate"

	"github.com/tidbcloud/infra-provider/regional-server/api/v1alpha1"
	cloudaws "github.com/tidbcloud/infra-provider/regional-server/pkg/cloud/aws"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/common/utils"
	"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/nodepool/task"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/deploy"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/k8s"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/namer"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/repository"
)

// +kubebuilder:rbac:groups=dedicated.regionalserver.tidbcloud.com,resources=nodepools,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=dedicated.regionalserver.tidbcloud.com,resources=nodepools/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=dedicated.regionalserver.tidbcloud.com,resources=nodepools/finalizers,verbs=update
// +kubebuilder:rbac:groups=eks.aws.upbound.io,resources=nodegroups,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=ack.alibabacloud.upbound.io,resources=kubernetesnodepools,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=containerservice.azure.upbound.io,resources=kubernetesclusternodepools,verbs=get;list;watch;create;update;patch;delete

// NodePoolReconciler reconciles a NodePool object.
type NodePoolReconciler struct {
	client.Client
	Log           logr.Logger
	Scheme        *runtime.Scheme
	Config        config.RootConfig
	EventRecorder record.EventRecorder

	nodePoolRW    repository.NodePoolReaderWriter
	dataPlaneR    repository.DataPlaneReader
	appBoxR       repository.APPBoxReader
	vpcR          repository.VPCReader
	awsCli        cloudaws.Client
	amiTypeGetter cloudaws.AMITypeGetter
}

func NewReconciler(
	client client.Client,
	log logr.Logger,
	scheme *runtime.Scheme,
	recorder record.EventRecorder,
	config config.RootConfig,
	awsCli cloudaws.Client,
) *NodePoolReconciler {
	return &NodePoolReconciler{
		Client:        client,
		Log:           log,
		Scheme:        scheme,
		EventRecorder: recorder,
		Config:        config,
		nodePoolRW:    repository.NewNodePoolReaderWriter(client),
		dataPlaneR:    repository.NewDataPlaneReaderWriter(client),
		appBoxR:       repository.NewAPPBoxReader(client),
		vpcR:          repository.NewVPCReader(client),
		awsCli:        awsCli,
		amiTypeGetter: cloudaws.NewAMITypeGetter(),
	}
}

// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the NodePool closer to the desired state.
func (r *NodePoolReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	logger := r.Log.WithValues("NodePool", req.NamespacedName)
	logger.Info("Reconciling RegionalServer NodePool")
	r.EventRecorder.Event(&v1alpha1.NodePool{}, corev1.EventTypeNormal, "ReconcileStart", "")

	start := time.Now()
	defer func() {
		logger.Info("Finish RegionalServer NodePool reconcile", "take", time.Since(start))
	}()

	nodePool := new(v1alpha1.NodePool)
	if err := r.nodePoolRW.Get(ctx, req.NamespacedName, nodePool); err != nil {
		if apierrors.IsNotFound(err) {
			logger.Info("NodePool resource not found")
			return ctrl.Result{}, nil
		}
		return ctrl.Result{}, fmt.Errorf("unable to get NodePool %s: %w", req.NamespacedName, err)
	}

	if utils.IsPaused(nodePool.GetObjectMeta()) {
		logger.Info("reconciliation is paused through annotation",
			"annotation", v1alpha1.AnnotationKeyReconciliationPaused)
		r.EventRecorder.Event(nodePool, corev1.EventTypeWarning, "ReconciliationPaused",
			"reconciliation is paused through the annotation "+v1alpha1.AnnotationKeyReconciliationPaused)
		return ctrl.Result{}, nil
	}

	var dataPlane *v1alpha1.DataPlane
	var appBox *v1alpha1.APPBox
	var vpc *v1alpha1.VPC
	if nodePool.DeletionTimestamp.IsZero() {
		var err error
		dataPlane, appBox, vpc, err = r.loadDependencies(ctx, nodePool)
		if err != nil {
			return ctrl.Result{}, err
		}
	}

	local := k8s.NewMutClientWithOwner(r.Client, nodePool.DeepCopy(), namer.NewNamer()).Client()
	c, err := (&task.Context{
		Local:            local,
		Logger:           logger,
		Config:           r.Config,
		NodePool:         nodePool,
		DataPlane:        dataPlane,
		APPBox:           appBox,
		VPC:              vpc,
		AWSClient:        r.awsCli,
		AWSAMITypeGetter: r.amiTypeGetter,
	}).Default()
	if err != nil {
		return ctrl.Result{}, fmt.Errorf("failed to create task context: %w", err)
	}

	tasks := GetNodePoolTasks(c)

	d := deploy.NewDeployer(tasks, nil, logger)

	var res ctrl.Result
	if nodePool.DeletionTimestamp.IsZero() {
		res, err = r.updateCreate(ctx, logger, c, d.Deploy(ctx))
	} else {
		if controllers.DeletionConfirmed(nodePool) {
			nodePool.Status.Phase = v1alpha1.NodePoolPhaseDeleting
			if err := r.nodePoolRW.UpdateStatus(ctx, nodePool); err != nil {
				return ctrl.Result{}, fmt.Errorf("failed to update nodepool status: %w", err)
			}
			res, err = r.updateDelete(ctx, logger, c, d.Destroy(ctx))
		} else {
			logger.Info("waiting for nodepool deletion to be confirmed")
			return ctrl.Result{}, fmt.Errorf("nodepool deletion is not confirmed")
		}
	}

	if err == nil {
		r.EventRecorder.Event(nodePool, corev1.EventTypeNormal, "ReconcileSuccess", "")
	} else {
		r.EventRecorder.Event(nodePool, corev1.EventTypeWarning, "ReconcileFail", err.Error())
	}

	return res, err
}

func (r *NodePoolReconciler) loadDependencies(ctx context.Context, nodePool *v1alpha1.NodePool) (*v1alpha1.DataPlane, *v1alpha1.APPBox, *v1alpha1.VPC, error) {
	if nodePool.Spec.DataPlane.Name == "" {
		return nil, nil, nil, fmt.Errorf("nodepool %s has empty dataplane reference", nodePool.Name)
	}

	dataPlane := new(v1alpha1.DataPlane)
	dataPlaneNamespace := nodePool.Spec.DataPlane.Namespace
	if dataPlaneNamespace == "" {
		dataPlaneNamespace = nodePool.Namespace
	}
	dataPlaneKey := types.NamespacedName{Namespace: dataPlaneNamespace, Name: nodePool.Spec.DataPlane.Name}
	if err := r.dataPlaneR.Get(ctx, dataPlaneKey, dataPlane); err != nil {
		return nil, nil, nil, fmt.Errorf("unable to get dataplane %s: %w", dataPlaneKey, err)
	}
	if dataPlane.Status.AppBox == nil {
		return nil, nil, nil, fmt.Errorf("dataplane %s has no appbox reference", dataPlane.Name)
	}

	appBox := new(v1alpha1.APPBox)
	appBoxKey := types.NamespacedName{Namespace: dataPlane.Status.AppBox.Namespace, Name: dataPlane.Status.AppBox.Name}
	if err := r.appBoxR.Get(ctx, appBoxKey, appBox); err != nil {
		return nil, nil, nil, fmt.Errorf("unable to get appbox %s: %w", appBoxKey, err)
	}
	vpc := new(v1alpha1.VPC)
	if dataPlane.Status.Vpc == nil {
		return nil, nil, nil, fmt.Errorf("dataplane %s has no vpc reference", dataPlane.Name)
	}
	vpcKey := types.NamespacedName{Namespace: dataPlane.Status.Vpc.Namespace, Name: dataPlane.Status.Vpc.Name}
	if err := r.vpcR.Get(ctx, vpcKey, vpc); err != nil {
		return nil, nil, nil, fmt.Errorf("unable to get vpc %s: %w", vpcKey, err)
	}

	return dataPlane, appBox, vpc, nil
}

func (r *NodePoolReconciler) updateCreate(ctx context.Context, logger logr.Logger, t *task.Context, results []deploy.Result) (ctrl.Result, error) {
	res, err, retriableErrs := deploy.MinRequeue(results)
	if err != nil {
		logger.Info("deploy create encounters error", "unretriable", err)
		t.NodePool.Status.Reason = err.Error()
	} else if retriableErrs != nil {
		logger.Info("deploy create encounters error", "retriable", retriableErrs)
		t.NodePool.Status.Reason = retriableErrs.Error()
	} else {
		t.NodePool.Status.Phase = v1alpha1.NodePoolPhaseActive
		t.NodePool.Status.Reason = ""
	}
	if t.NodePool.Status.ObservedGeneration != t.NodePool.Generation {
		t.NodePool.Status.ObservedGeneration = t.NodePool.Generation
	}

	if e := r.updateNodePool(ctx, logger, t.NodePool); e != nil {
		return res, fmt.Errorf("failed to update nodepool: %w", e)
	}
	return res, err
}

func (r *NodePoolReconciler) updateNodePool(ctx context.Context, logger logr.Logger, nodePool *v1alpha1.NodePool) error {
	nodePoolStatus := nodePool.Status.DeepCopy()
	if err := r.nodePoolRW.Update(ctx, nodePool); err != nil {
		logger.Error(err, "failed to update nodepool")
		return err
	}
	nodePool.Status = *nodePoolStatus
	if err := r.nodePoolRW.UpdateStatus(ctx, nodePool); err != nil && !apierrors.IsNotFound(err) {
		logger.Error(err, "failed to update nodepool status")
		return err
	}
	return nil
}

func (r *NodePoolReconciler) updateDelete(ctx context.Context, logger logr.Logger, t *task.Context, results []deploy.Result) (ctrl.Result, error) {
	res, err, retriableErr := deploy.MinRequeue(results)
	if err != nil {
		logger.Info("deploy delete encounters error", "unretriable", err)
		t.NodePool.Status.Reason = err.Error()
	} else if retriableErr != nil {
		logger.Info("deploy delete encounters error", "retriable", retriableErr)
		t.NodePool.Status.Reason = retriableErr.Error()
	} else {
		logger.Info("deploy delete successfully, remove finalizer")
		controllerutil.RemoveFinalizer(t.NodePool, v1alpha1.Finalizer)
	}

	if e := r.updateNodePool(ctx, logger, t.NodePool); e != nil {
		return res, fmt.Errorf("failed to update nodepool: %w", e)
	}
	return res, err
}

func (r *NodePoolReconciler) SetupWithManager(mgr ctrl.Manager, pds ...predicate.Predicate) error {
	return ctrl.NewControllerManagedBy(mgr).
		Named("nodepool").
		For(&v1alpha1.NodePool{}, builder.WithPredicates(pds...)).
		WithOptions(controller.Options{RateLimiter: k8s.RetryLimiter, MaxConcurrentReconciles: 5}).
		WithEventFilter(predicate.Or(
			predicate.GenerationChangedPredicate{},
			predicate.LabelChangedPredicate{},
			controllers.AnnotationChangedPredicate{},
		)).
		Complete(r)
}
