package tidbpdb

import (
	"context"
	"strconv"
	"time"

	"github.com/go-logr/logr"
	"github.com/tidbcloud/aws-shared-provider/pkg/metrics"
	"github.com/tidbcloud/aws-shared-provider/pkg/tidbmanager"
	"github.com/tidbcloud/aws-shared-provider/pkg/types"
	"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"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/util/intstr"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
)

const (
	checkInterval = 6 * time.Second

	tidbPDBName = "tidb-pdb"
)

var (
	tidbMatchLabels = map[string]string{
		labels.NameLabelKey:      labels.TidbPodNameLabelValue,
		labels.ComponentLabelKey: labels.TidbPodComponentLabelValue,
	}
)

type TidbPDB struct {
	logr.Logger

	tidbPoolNamespace string

	ctx      context.Context
	client   client.Client
	tManager tidbmanager.TidbManager
}

func New(tidbPoolNamespace string, client client.Client, tManager tidbmanager.TidbManager) *TidbPDB {
	return &TidbPDB{
		Logger: ctrl.Log.WithName("tidbpdb").WithName(tidbPoolNamespace),

		client:            client,
		tManager:          tManager,
		tidbPoolNamespace: tidbPoolNamespace,
	}
}

func (tp *TidbPDB) Start(ctx context.Context) error {
	tp.ctx = ctx
	tp.Info("start tidbpdb")

	checkTicker := time.NewTicker(checkInterval)
	defer checkTicker.Stop()

	for {
		select {
		case <-tp.ctx.Done():
			err := tp.ctx.Err()
			tp.Error(err, "stop tidbpdb")
			return err
		case <-checkTicker.C:
			tp.do()
		}
	}
}

func (tp *TidbPDB) do() {
	start := time.Now()
	tp.V(2).Info("start tidbpdb check cycle")
	defer func() {
		tp.V(2).Info("finish tidbpdb check cycle", "time", time.Since(start))
	}()

	freeCacheSize := tp.tManager.FreeCacheSize()
	tidbPoolMinSize, err := tp.getTidbPoolMinSize()
	if err != nil {
		tp.Error(err, "get tidbPoolMinSize")
		return
	}
	halfTidbPoolMinSize := tidbPoolMinSize / 2 //nolint:mnd // not need
	if halfTidbPoolMinSize < 0 {
		halfTidbPoolMinSize = 0
	}
	canEvictTidbPodSize := freeCacheSize - halfTidbPoolMinSize
	if canEvictTidbPodSize < 0 {
		canEvictTidbPodSize = 0
	}
	tidbPodList, err := tp.getTidbPodList()
	if err != nil {
		tp.Error(err, "get tidb pod list failed")
		return
	}
	tidbPodAvailableSize := 0
	for i := range tidbPodList {
		if kubeapi.IsPodReady(&tidbPodList[i]) {
			tidbPodAvailableSize++
		}
	}
	tp.V(2).Info("get some xxxSize vars",
		"tidbPodAvailableSize", tidbPodAvailableSize, "canEvictTidbPodSize", canEvictTidbPodSize,
		"freeCacheSize", freeCacheSize, "halfTidbPoolMinSize", halfTidbPoolMinSize)

	minAvailable := intstr.FromInt(tidbPodAvailableSize - canEvictTidbPodSize)
	pdb := &policyv1.PodDisruptionBudget{
		ObjectMeta: metav1.ObjectMeta{
			Name:      tidbPDBName,
			Namespace: tp.tidbPoolNamespace,
		},
		Spec: policyv1.PodDisruptionBudgetSpec{
			Selector: metav1.SetAsLabelSelector(tidbMatchLabels),
		},
	}
	op, err := controllerutil.CreateOrUpdate(tp.ctx, tp.client, pdb, func() error {
		pdb.Spec.MinAvailable = &minAvailable
		return nil
	})
	if err != nil {
		tp.Error(err, "CreateOrUpdate tidb-pdb failed")
		return
	}
	if op != controllerutil.OperationResultNone {
		metrics.MinAvailableGauge.WithLabelValues(tp.tidbPoolNamespace).Set(float64(minAvailable.IntValue()))
		metrics.CanEvictGauge.WithLabelValues(tp.tidbPoolNamespace).Set(float64(canEvictTidbPodSize))
		tp.Info("CreateOrUpdate tidb-pdb success", "minAvailable", minAvailable)
	}
}

func (tp *TidbPDB) getTidbPoolMinSize() (int, error) {
	serverlessPoolConfigCm := &corev1.ConfigMap{}
	if err := tp.client.Get(tp.ctx,
		client.ObjectKey{
			Name:      types.ServerlessPoolConfig,
			Namespace: tp.tidbPoolNamespace,
		}, serverlessPoolConfigCm); err != nil {
		return 0, err
	}
	return strconv.Atoi(serverlessPoolConfigCm.Data[types.MinPoolReplicasKey])
}

func (tp *TidbPDB) getTidbPodList() ([]corev1.Pod, error) {
	var podList corev1.PodList
	if err := tp.client.List(tp.ctx, &podList,
		client.InNamespace(tp.tidbPoolNamespace), client.MatchingLabels(tidbMatchLabels)); err != nil {
		return nil, err
	}
	return podList.Items, nil
}
