package noderecover

import (
	"context"
	"errors"
	"fmt"
	"math"
	"os"
	"time"

	"github.com/tidbcloud/aws-shared-provider/pkg/manager"
	"github.com/tidbcloud/aws-shared-provider/pkg/metrics"
	"github.com/tidbcloud/aws-shared-provider/pkg/types"
	"github.com/tidbcloud/aws-shared-provider/pkg/utils"
	"github.com/tidbcloud/aws-shared-provider/pkg/utils/kubeapi"
	"github.com/tidbcloud/aws-shared-provider/pkg/utils/labels"
	"github.com/tidbcloud/aws-shared-provider/pkg/utils/query"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/client-go/tools/record"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller"
	"sigs.k8s.io/controller-runtime/pkg/log"
	"sigs.k8s.io/controller-runtime/pkg/predicate"
)

const (
	maxEvictDuration = 24 * time.Hour
	checkInterval    = 10 * time.Minute

	cpuIdleThreshold = 0.5
	memIdleThreshold = 0.5

	nodeRecoverCheckTimestamp = "noderecover.tidbcloud.com/check-timestamp"
	nodeRecoverCheckResult    = "noderecover.tidbcloud.com/check-result"

	MaxConcurrentReconciles = 3
)

var (
	ErrNoMetric = errors.New("no metric")
)

// Reconciler reconciles a force evicted Node object.
type Reconciler struct {
	Client   client.Client
	Recorder record.EventRecorder

	o11yQuerier query.PromQuerier
}

// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state.
func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	logger := log.FromContext(ctx)
	logger.V(2).Info("start reconcile noderecover")
	start := time.Now()
	defer func() {
		logger.V(2).Info("finish reconcile noderecover", "time", time.Since(start))
	}()

	node := &corev1.Node{}
	if err := r.Client.Get(ctx, req.NamespacedName, node); err != nil {
		return ctrl.Result{}, client.IgnoreNotFound(err)
	}

	logger = logger.WithValues("nodegroup", node.Labels[labels.ServerlessNodeGroupLabelKey])

	forceEvictTimestamp, err := time.Parse(time.RFC3339, node.Annotations[types.AnnoNodeForceEvictTimestamp])
	if err != nil {
		logger.Error(err, "parse AnnoNodeForceEvictTimestamp failed, skip")
		return ctrl.Result{}, nil
	}
	if time.Since(forceEvictTimestamp) < checkInterval {
		logger.V(2).Info("less than checkInterval since the last force evict, skip")
		return ctrl.Result{}, nil
	}

	if node.Annotations[nodeRecoverCheckTimestamp] != "" {
		checkTimestamp, err := time.Parse(time.RFC3339, node.Annotations[nodeRecoverCheckTimestamp])
		if err != nil {
			logger.Error(err, "parse nodeRecoverCheckTimestamp failed, skip")
			return ctrl.Result{}, nil
		}
		if time.Since(checkTimestamp) < checkInterval {
			logger.V(2).Info("less than checkInterval since the last check, skip")
			return ctrl.Result{}, nil
		}
	}

	evictDuration := time.Since(forceEvictTimestamp)
	if evictDuration > maxEvictDuration {
		logger.Info("more than maxEvictDuration since the last force evict, recover now")
		return r.Recover(ctx, node)
	}

	cpuIdle, err := r.getCpuIdle(ctx, node.Name, checkInterval.String())
	if err != nil {
		if errors.Is(err, ErrNoMetric) {
			logger.Info("no cpu idle metric, skip")
			return ctrl.Result{}, nil
		}
		logger.Error(err, "getCpuIdle failed")
		return ctrl.Result{}, err
	}
	memIdle, err := r.getMemIdle(ctx, node.Name, checkInterval.String())
	if err != nil {
		if errors.Is(err, ErrNoMetric) {
			logger.Info("no mem idle metric, skip")
			return ctrl.Result{}, nil
		}
		logger.Error(err, "getMemIdle failed")
		return ctrl.Result{}, err
	}
	logger.Info("get node idle", "cpuIdle", cpuIdle, "memIdle", memIdle)
	if cpuIdle > cpuIdleThreshold && memIdle > memIdleThreshold {
		logger.Info("the node is idle, recover now")
		return r.Recover(ctx, node)
	}

	if err := kubeapi.UpdateNode(ctx, r.Client, func() *corev1.Node {
		node.Annotations = utils.UpdateMap(node.Annotations, nodeRecoverCheckTimestamp, time.Now().Format(time.RFC3339))
		node.Annotations = utils.UpdateMap(node.Annotations, nodeRecoverCheckResult, fmt.Sprintf(
			"evictDuration %v, cpuIdle %v, memIdle %v",
			// round to the nearest number with 2 decimal places
			//nolint: gomnd // nolint
			evictDuration, math.Round(cpuIdle*100)/100, math.Round(memIdle*100)/100))
		return node
	}); err != nil {
		logger.Error(err, "update node recover check failed, skip")
	}

	metrics.CheckCounter.WithLabelValues(node.Name, node.Labels[labels.ServerlessNodeGroupLabelKey]).Inc()
	return ctrl.Result{}, nil
}

// SetupWithManager sets up the controller with the Manager.
func (r *Reconciler) SetupWithManager(mgr manager.Manager) error {
	queryURL := os.Getenv(types.O11yGlobalReadURL)
	if queryURL == "" {
		return fmt.Errorf("env %s is empty", types.O11yGlobalReadURL)
	}
	r.o11yQuerier = query.NewPromQueryer(nil, queryURL)
	r.Recorder = mgr.GetEventRecorderFor("NodeRecover")

	byAnnoFuncs := predicate.NewPredicateFuncs(func(object client.Object) bool {
		return object.GetAnnotations()[types.AnnoNodeForceEvictTimestamp] != ""
	})
	return ctrl.NewControllerManagedBy(mgr).Named("NodeRecover").For(&corev1.Node{}).WithEventFilter(byAnnoFuncs).
		WithOptions(controller.Options{MaxConcurrentReconciles: MaxConcurrentReconciles}).
		Complete(r)
}
