package tidbpodevicter

import (
	"context"
	"fmt"
	"sort"
	"strconv"
	"time"

	"github.com/go-logr/logr"
	"github.com/pingcap/errors"
	"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"
	corev1 "k8s.io/api/core/v1"
	policyv1 "k8s.io/api/policy/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/utils/ptr"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/log"
)

const (
	forceEvictCount  = 1
	maxEvictDuration = 15 * time.Minute
)

func (r *Reconciler) evict(ctx context.Context, evictType string, evictThreshold float32, nodes map[string]float32) {
	shouldForceEvict := len(nodes) > 1
	for nodeName, nodeUsage := range nodes {
		logger := log.FromContext(ctx, "evictType", evictType, "evictThreshold", evictThreshold, "nodeName", nodeName)
		node := &corev1.Node{}
		if err := r.Client.Get(ctx, client.ObjectKey{Name: nodeName}, node); err != nil {
			logger.Error(err, "failed to get node")
			continue
		}
		nodegroup := node.Labels[labels.ServerlessNodeGroupLabelKey]
		logger = logger.WithValues("nodegroup", nodegroup)

		if node.DeletionTimestamp != nil {
			logger.Info("node is terminating, skip")
			continue
		}

		logger.Info("evicting", "nodeUsage", nodeUsage)
		metrics.TryEvictCounter.WithLabelValues(evictType, nodeName, nodegroup).Inc()

		evictDuration := getEvictDuration(node.Annotations[types.AnnoNodeEvictTimestampPrefix+evictType])
		evictCount := parseCount(node.Annotations[types.AnnoNodeEvictCountPeriodPrefix+evictType])
		if shouldForceEvict || (evictCount >= forceEvictCount && evictDuration < maxEvictDuration) {
			r.forceEvictTidbPods(logger, ctx, evictType, node)
		}
		r.evictTidbPods(logger, ctx, evictType, evictDuration, nodeUsage, evictThreshold, node)
	}
}

func (r *Reconciler) getPodDelta(ctx context.Context, evictType string, node *corev1.Node) ([]podDelta, error) {
	var (
		podInfoList []podDelta
		err         error
	)
	switch evictType {
	// the quick delta stage may more earlier, so we need to set longer interval
	case "cpu":
		podInfoList, err = r.getCpuDelta(ctx, node.Name, "3m")
	case "mem":
		podInfoList, err = r.getMemDelta(ctx, node.Name, "3m")
	default:
		err = errors.New("unknown evict type")
	}

	return podInfoList, err
}

func (r *Reconciler) getUsage(ctx context.Context, evictType string, node *corev1.Node) (map[string]float32, error) {
	var (
		podUsageMap map[string]float32
		err         error
	)
	switch evictType {
	case "cpu":
		podUsageMap, err = r.getCpuUsage(ctx, node.Name, "1m")
	case "mem":
		podUsageMap, err = r.getMemUsage(ctx, node.Name, "1m")
	default:
		err = errors.New("unknown evict type")
	}

	return podUsageMap, err
}

func calculatePodScoreAndSort(podDeltaList []podDelta, podUsageMap map[string]float32) []podScore {
	var positiveDeltaTotal, notPositiveDeltaTotal, positiveUsageTotal, notPositiveUsageTotal float32
	for _, pd := range podDeltaList {
		if pd.delta > 0 {
			positiveDeltaTotal += pd.delta
			positiveUsageTotal += podUsageMap[pd.namespace+"/"+pd.name]
		} else {
			notPositiveDeltaTotal += -pd.delta
			notPositiveUsageTotal += podUsageMap[pd.namespace+"/"+pd.name]
		}
	}

	var podScoreList []podScore
	for _, pd := range podDeltaList {
		podUsage := podUsageMap[pd.namespace+"/"+pd.name]
		ps := podScore{podDelta: pd, usage: podUsage}
		//nolint:mnd // 80 and 20 are constants
		if pd.delta > 0 {
			ps.score = int(pd.delta/positiveDeltaTotal*80 + podUsage/positiveUsageTotal*20)
		} else {
			if notPositiveDeltaTotal == 0 {
				ps.score = int(podUsage / notPositiveUsageTotal * 20)
			} else {
				ps.score = int(pd.delta/notPositiveDeltaTotal*80 + podUsage/notPositiveUsageTotal*20)
			}
		}
		podScoreList = append(podScoreList, ps)
	}

	sort.Slice(podScoreList, func(i, j int) bool {
		return podScoreList[i].score > podScoreList[j].score
	})
	return podScoreList
}

func (r *Reconciler) evictTidbPods(
	logger logr.Logger,
	ctx context.Context,
	evictType string,
	evictDuration time.Duration,
	nodeUsage, evictThreshold float32,
	node *corev1.Node,
) {
	podDeltaList, err := r.getPodDelta(ctx, evictType, node)
	if err != nil {
		logger.Error(err, "failed to getPodDelta")
		return
	}
	logger.V(2).Info("get podDeltaList", "podDeltaList", fmt.Sprintf("%v", podDeltaList))

	podUsageMap, err := r.getUsage(ctx, evictType, node)
	if err != nil {
		logger.Error(err, "failed to getUsage")
		return
	}
	logger.V(2).Info("get podUsageMap", "podUsageMap", podUsageMap)

	podScoreList := calculatePodScoreAndSort(podDeltaList, podUsageMap)
	logger.V(2).Info("get podScoreList", "podScoreList", fmt.Sprintf("%v", podScoreList))

	if len(podScoreList) == 1 {
		logger.Info("only 1 pod, skip evicting")
		return
	}

	var evictPods []string
	var podUsageTotal float32
	for _, ps := range podScoreList {
		logger.Info("[pod lifecycle] evicting tidb pod", "podScore", ps)
		pod := &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: ps.name, Namespace: ps.namespace}}
		if err = client.IgnoreNotFound(
			r.Client.Delete(ctx, pod, &client.DeleteOptions{GracePeriodSeconds: ptr.To[int64](80)}),
		); err != nil {
			logger.Error(err, "failed to delete pod", "podScore", ps)
			return
		}
		evictPods = append(evictPods, ps.name)
		podUsageTotal += ps.usage
		if nodeUsage-podUsageTotal < evictThreshold {
			break
		}
	}

	if err = kubeapi.UpdateNode(ctx, r.Client, func() *corev1.Node {
		if node.Annotations == nil {
			node.Annotations = map[string]string{}
		}
		node.Annotations[types.AnnoNodeEvictTimestampPrefix+evictType] = time.Now().Format(time.RFC3339)
		if evictDuration < maxEvictDuration {
			count := incCount(node.Annotations[types.AnnoNodeEvictCountPeriodPrefix+evictType])
			node.Annotations[types.AnnoNodeEvictCountPeriodPrefix+evictType] = count
		} else {
			node.Annotations[types.AnnoNodeEvictCountPeriodPrefix+evictType] = "1"
		}
		return node
	}); err != nil {
		logger.Error(err, "failed to update node for EvictTidbPods")
		return
	}
	r.Recorder.Eventf(node, corev1.EventTypeNormal, "EvictTidbPods",
		"evict tidb pods: %v, evictType=%v", evictPods, evictType)
	metrics.EvictCounter.WithLabelValues(evictType, node.Name, node.Labels[labels.ServerlessNodeGroupLabelKey]).Inc()
}

func (r *Reconciler) forceEvictTidbPods(logger logr.Logger, ctx context.Context, evictType string, node *corev1.Node) {
	if node.Annotations[types.AnnoNodeForceEvictTimestamp] == "" {
		if err := kubeapi.UpdateNode(ctx, r.Client, func() *corev1.Node {
			node.Annotations = utils.UpdateMap(node.Annotations, types.AnnoNodeForceEvictTimestamp,
				time.Now().Format(time.RFC3339))
			node.Spec.Unschedulable = true
			return node
		}); err != nil {
			logger.Error(err, "failed to update node AnnoNodeForceEvictTimestamp/Unschedulable")
			return
		}
	}

	var podEvicted bool
	for tidbPool, tManager := range r.TidbManagers {
		var poolPodEvicted bool
		for _, podName := range tManager.GetTidbFromFreeCache(node.Name, false) {
			logger.Info("[pod lifecycle] force evict tidb pod", "tidbPool", tidbPool, "podName", podName)
			pod := &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: podName, Namespace: tidbPool}}
			if err := client.IgnoreNotFound(r.Client.SubResource("eviction").Create(
				ctx, pod, &policyv1.Eviction{DeleteOptions: &metav1.DeleteOptions{GracePeriodSeconds: ptr.To[int64](0)}},
			)); err != nil {
				logger.Error(err, "failed to evict pod", "tidbPool", tidbPool, "podName", podName)
				if apierrors.IsTooManyRequests(err) {
					break
				}
				continue
			}
			poolPodEvicted, podEvicted = true, true
		}
		if poolPodEvicted {
			if tidbScaler := tManager.GetTidbScaler(); tidbScaler != nil {
				tidbScaler.TryScale()
			}
		}
	}
	if !podEvicted {
		return
	}

	if err := kubeapi.UpdateNode(ctx, r.Client, func() *corev1.Node {
		node.Annotations[types.AnnoNodeForceEvictTimestamp] = time.Now().Format(time.RFC3339) // reset timestamp
		return node
	}); err != nil {
		logger.Error(err, "failed to update node for ForceEvictTidbPods")
		return
	}
	r.Recorder.Eventf(node, corev1.EventTypeNormal, "ForceEvictTidbPods",
		"evict all free tidb pods, evictType=%v", evictType)
	metrics.ForceEvictCounter.WithLabelValues(evictType, node.Name, node.Labels[labels.ServerlessNodeGroupLabelKey]).Inc()
}

func parseCount(count string) int {
	if count == "" {
		return 0
	}

	c, err := strconv.Atoi(count)
	if err != nil {
		return 0
	}
	return c
}

func incCount(count string) string {
	if count == "" {
		return "1"
	}

	c, err := strconv.Atoi(count)
	if err != nil {
		return "1"
	}
	return strconv.Itoa(c + 1)
}

func getEvictDuration(evictTimestamp string) time.Duration {
	if evictTimestamp == "" {
		return 0
	}

	evictTime, err := time.Parse(time.RFC3339, evictTimestamp)
	if err != nil {
		return 0
	}
	return time.Since(evictTime)
}
