package tidbscaler

import (
	"context"
	"strconv"
	"time"

	"github.com/go-logr/logr"
	"github.com/tidbcloud/aws-shared-provider/pkg/metrics"
	"github.com/tidbcloud/aws-shared-provider/pkg/scheme"
	"github.com/tidbcloud/aws-shared-provider/pkg/tidbmanager"
	"github.com/tidbcloud/aws-shared-provider/pkg/tidbpool"
	"github.com/tidbcloud/aws-shared-provider/pkg/types"
	"k8s.io/apimachinery/pkg/runtime/serializer"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

const (
	checkInterval       = 8 * time.Second
	maxCreatingDuration = 8 * time.Second
	maxScaleChannelSize = 3
)

type TidbScaler struct {
	logr.Logger

	tidbPoolNamespace string

	ctx     context.Context
	client  client.Client
	pool    tidbpool.Pool
	scaleCh chan struct{}

	revision string
	minSize  int
	maxSize  int

	poolSize int

	creating   int
	createTime *time.Time

	codecs   serializer.CodecFactory
	tManager tidbmanager.TidbManager
}

func New(client client.Client, tManager tidbmanager.TidbManager) *TidbScaler {
	pool := tManager.GetTidbPool()
	return &TidbScaler{
		Logger: ctrl.Log.WithName("tidbscaler").WithName(pool.Namespace()),

		tidbPoolNamespace: pool.Namespace(),

		client:  client,
		pool:    pool,
		scaleCh: make(chan struct{}, maxScaleChannelSize),

		codecs:   serializer.NewCodecFactory(scheme.Scheme),
		tManager: tManager,
	}
}

func (ts *TidbScaler) Start(ctx context.Context) error {
	ts.ctx = ctx
	ts.Info("start tidbscaler")

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

	for {
		select {
		case <-ts.ctx.Done():
			err := ts.ctx.Err()
			ts.Error(err, "stop tidbscaler")
			return err
		case <-ts.scaleCh:
			start := time.Now()
			ts.V(2).Info("start scale")
			ts.do()
			ts.V(2).Info("finish scale", "time", time.Since(start))
		case <-checkTicker.C:
			start := time.Now()
			ts.V(1).Info("start tidbscaler check cycle...")
			ts.do()
			ts.V(1).Info("finish tidbscaler cycle", "time", time.Since(start))
		}
	}
}

func (ts *TidbScaler) TryScale() {
	select {
	case ts.scaleCh <- struct{}{}:
	default:
		ts.V(1).Info("tidbscaler channel is full, skip scale")
	}
}

func (ts *TidbScaler) do() {
	pool := ts.pool.GetPool(ts.ctx)
	if pool == nil {
		return
	}

	abnormalList, err := ts.pool.GetAbnormalPodList(ts.ctx)
	if err != nil {
		ts.Error(err, "get abnormal tidb pod list")
	} else {
		metrics.AbnormalTiDBGauge.WithLabelValues(ts.pool.Namespace()).Set(float64(len(abnormalList)))
	}

	ts.printConfigChanged(pool)

	poolSize := ts.fetchPoolSize()
	freeCacheRetentionTarget := ts.maxSize
	if ts.minSize == 0 {
		freeCacheRetentionTarget = 0
	}
	switch {
	case poolSize < ts.minSize:
		ts.Info("we should expand tidb pool", "poolSize", poolSize, "minSize", ts.minSize, "creating", ts.getCreating())
		ts.UnlockIfNeed() // unlock tidb pool first, maybe
		if len(abnormalList) > types.MaxAbnormalTidbPods {
			ts.Info("abnormal tidb pods too more, skip expand",
				"count", len(abnormalList), "max", types.MaxAbnormalTidbPods)
			break
		}
		if step := ts.minSize - poolSize - ts.getCreating(); step > 0 {
			ts.alterCreating(ts.Expand(step))
			metrics.ActionCounter.WithLabelValues(
				"Expand", strconv.Itoa(ts.minSize), strconv.Itoa(ts.maxSize), ts.tidbPoolNamespace).Inc()
		} else {
			ts.alterCreating(step) // keep or reduce creating count
		}
	case poolSize > freeCacheRetentionTarget:
		ts.resetCreating()
		ts.Lock()
		ts.Reduce(poolSize - freeCacheRetentionTarget)
		metrics.ActionCounter.WithLabelValues(
			"Reduce", strconv.Itoa(ts.minSize), strconv.Itoa(ts.maxSize), ts.tidbPoolNamespace).Inc()
	default:
		ts.resetCreating()
		ts.UnlockIfNeed()
		metrics.ActionCounter.WithLabelValues(
			"Default", strconv.Itoa(ts.minSize), strconv.Itoa(ts.maxSize), ts.tidbPoolNamespace).Inc()
	}
}

func (ts *TidbScaler) printConfigChanged(pool *tidbpool.TidbPool) {
	if ts.revision == "" {
		ts.Info("init tidbscaler", "revision", pool.Revision, "minSize", pool.MinSize, "maxSize", pool.MaxSize)
	} else {
		if ts.revision != pool.Revision {
			ts.resetCreating()
			ts.Info("revision changed", "old", ts.revision, "new", pool.Revision)
		}
		if ts.minSize != pool.MinSize {
			ts.resetCreating()
			ts.Info("minSize changed", "old", ts.minSize, "new", pool.MinSize)
		}
		if ts.maxSize != pool.MaxSize {
			ts.resetCreating()
			ts.Info("maxSize changed", "old", ts.maxSize, "new", pool.MaxSize)
		}
	}
	ts.revision, ts.minSize, ts.maxSize = pool.Revision, pool.MinSize, pool.MaxSize
}

func (ts *TidbScaler) fetchPoolSize() int {
	poolSize := ts.tManager.FreeCacheSize()
	if ts.poolSize != poolSize {
		ts.V(2).Info("pool size changed", "pre size", ts.poolSize, "now size", poolSize)
		ts.poolSize = poolSize
	}

	return ts.poolSize
}

func (ts *TidbScaler) resetCreating() {
	ts.creating = 0
	ts.createTime = nil
}

// alterCreating will increase or decrease creating count and reset creating count if it is too long
func (ts *TidbScaler) alterCreating(delta int) {
	preCreating := ts.creating
	ts.creating += delta
	if ts.creating <= 0 {
		ts.resetCreating()
	} else {
		if preCreating <= 0 {
			now := time.Now()
			ts.createTime = &now
		} else {
			ts.resetIfCreatingTooLong()
		}
	}
}

func (ts *TidbScaler) getCreating() int {
	return ts.creating
}

func (ts *TidbScaler) resetIfCreatingTooLong() {
	if ts.createTime != nil && time.Since(*ts.createTime) > maxCreatingDuration {
		ts.resetCreating()
	}
}
