package tidbautoscaling

import (
	"context"
	"os"
	"strconv"
	"time"

	"github.com/go-logr/logr"
	"github.com/pingcap/errors"
	serverlessv1alpha1 "github.com/tidbcloud/aws-shared-provider/apis/serverless/v1alpha1"
	"github.com/tidbcloud/aws-shared-provider/pkg/tidbmanager"
	"github.com/tidbcloud/aws-shared-provider/pkg/types"
	"github.com/tidbcloud/aws-shared-provider/pkg/utils/query"
	infraclient "github.com/tidbcloud/tidb-management-service/api/serverless_infra/client"
	corev1 "k8s.io/api/core/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

const (
	tidbAutoscalingName = "tidb-autoscaling"

	checkInterval = 20 * time.Second

	DOWN = -1
	HOLD = 0
	UP   = 1
)

type scaleRecord struct {
	direction int
	timestamp int64
}

type TidbAutoscaling struct {
	logr.Logger

	tidbPoolNamespace string
	scaleRecordMap    map[string]scaleRecord

	ctx             context.Context
	client          client.Client
	infraClient     client.Client
	infraClusterCli infraclient.Client
	tManager        tidbmanager.TidbManager
	o11yQuerier     query.PromQuerier
}

func New(tidbPoolNamespace string, client, infraClient client.Client, infraClusterCli infraclient.Client,
	tManager tidbmanager.TidbManager) *TidbAutoscaling {
	return &TidbAutoscaling{
		Logger: ctrl.Log.WithName("tidbautoscaling").WithName(tidbPoolNamespace),

		client:            client,
		infraClient:       infraClient,
		infraClusterCli:   infraClusterCli,
		tManager:          tManager,
		tidbPoolNamespace: tidbPoolNamespace,
	}
}

func (ta *TidbAutoscaling) Start(ctx context.Context) error {
	ta.ctx = ctx
	ta.Info("start tidbautoscaling")

	queryURL := os.Getenv(types.O11yGlobalReadURL)
	if queryURL == "" {
		return errors.Errorf("env %s is empty", types.O11yGlobalReadURL)
	}
	ta.o11yQuerier = query.NewPromQueryer(nil, queryURL)

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

	for {
		select {
		case <-checkTicker.C:
			taConfig := &serverlessv1alpha1.TidbAutoscaling{}
			if err := ta.client.Get(ta.ctx, client.ObjectKey{
				Name:      tidbAutoscalingName,
				Namespace: ta.tidbPoolNamespace,
			}, taConfig); err != nil {
				if !apierrors.IsNotFound(err) {
					ta.Error(err, "Get TidbAutoscaling config failed")
				}
				continue
			}

			ta.do(taConfig)
		case <-ta.ctx.Done():
			err := ta.ctx.Err()
			ta.Error(err, "stop tidbautoscaling")
			return err
		}
	}
}

func (ta *TidbAutoscaling) do(taConfig *serverlessv1alpha1.TidbAutoscaling) {
	start := time.Now()
	ta.V(1).Info("start tidbautoscaling check cycle")
	defer func() {
		ta.V(1).Info("finish tidbautoscaling check cycle", "time", time.Since(start))
	}()

	if taConfig.Disable {
		ta.Info("tidbautoscaling is disabled")
		return
	}

	metrics, err := ta.query(taConfig.Metrics)
	if err != nil {
		ta.Error(err, "query metrics failed")
		return
	}

	disableScaleDown, err := ta.queryScaleDownCheck(taConfig.ScaleDownCheckMetrics)
	if err != nil {
		ta.Error(err, "query scale down check failed")
		return
	}

	tidbMaxReplicas, err := ta.getTidbMaxReplicas()
	if err != nil {
		ta.Error(err, "getTidbMaxReplicas failed")
		return
	}

	ta.scale(ta.convertToTokenMetricMap(metrics), taConfig, tidbMaxReplicas, disableScaleDown)
}

func (ta *TidbAutoscaling) getTidbMaxReplicas() (int, error) {
	serverlessPoolConfigCm := &corev1.ConfigMap{}
	if err := ta.client.Get(ta.ctx,
		client.ObjectKey{
			Name:      types.ServerlessPoolConfig,
			Namespace: ta.tidbPoolNamespace,
		}, serverlessPoolConfigCm); err != nil {
		return 0, err
	}
	if serverlessPoolConfigCm.Data[types.TidbMaxReplicasKey] == "" {
		return 0, errors.Errorf("key %s is empty", types.TidbMaxReplicasKey)
	}
	return strconv.Atoi(serverlessPoolConfigCm.Data[types.TidbMaxReplicasKey])
}
