package ratecontroller

import (
	"context"
	"fmt"
	"time"

	"github.com/pingcap/errors"
	"github.com/pingcap/log"
	"github.com/tidbcloud/core-portal/internal/central/pkg/ratecontroller/types"
	"go.uber.org/zap"
)

type rateController struct {
	store types.Store
}

type rateControllerWithScene struct {
	rateController
	scene *types.Scene
}

func NewRateController(store types.Store) *rateController {
	return &rateController{
		store: store,
	}
}

func (r *rateController) WithScene(scene *types.Scene) rateControllerWithScene {
	return rateControllerWithScene{
		rateController: *r,
		scene:          scene,
	}
}

func (r *rateControllerWithScene) Access(ctx context.Context, accessor, target string, now time.Time) {
	if err := r.store.AddEntry(ctx, r.scene, accessor, target, now); err != nil {
		log.Warn("add access log failed",
			zap.String("component", "rateController"),
			zap.String("method", "Access"),
			zap.String("reason", err.Error()))
	}

	// purge every access.
	if err := r.purge(ctx, accessor, now); err != nil {
		log.Warn("purge failed",
			zap.String("component", "rateController"),
			zap.String("method", "Access"),
			zap.String("reason", err.Error()))
	}
}

func (r *rateControllerWithScene) purge(ctx context.Context, accessor string, now time.Time) error {
	// clear expired access log if TTL is set
	if r.scene.TTL != 0 {
		expiredFrom := now.Add(-r.scene.TTL)
		if err := r.store.PurgeExpired(ctx, r.scene, accessor, expiredFrom); err != nil {
			return errors.Annotate(err, "purge expired failed")
		}
	}

	// clear overflowed access log if capacity is set
	if r.scene.Capacity != 0 {
		if err := r.store.PurgeOverflowed(ctx, r.scene, accessor); err != nil {
			return errors.Annotate(err, "purge overflowed failed")
		}
	}

	return nil
}

func (r *rateControllerWithScene) Allow(ctx context.Context, accessor, target string, now time.Time, rate *types.Rate) (allow bool) {
	return r.AllowAll(ctx, accessor, target, now, rate)
}

// AllowAll determine whether the `accessor` now could do new request on the scene
// empty target mean don't care target
func (r *rateControllerWithScene) AllowAll(ctx context.Context, accessor, target string, now time.Time, rateList ...*types.Rate) (allow bool) {
	for _, rate := range rateList {
		r.validateRate(rate)
	}

	for _, rate := range rateList {
		startTime := now.Add(-rate.Period)
		count, err := r.store.Count(ctx, r.scene, accessor, target, startTime, now)
		// any failed, let it pass through
		if err != nil {
			log.Warn("calc count failed",
				zap.String("component", "rateController"),
				zap.String("method", "Allow"),
				zap.String("reason", err.Error()))
			continue
		}

		if count >= int64(rate.Limit) {
			return false
		}
	}
	return true
}

func (r *rateControllerWithScene) validateRate(rate *types.Rate) {
	MustBeTrue(r.scene.Capacity == 0 || rate.Limit <= r.scene.Capacity, "limit(%d) is too larger than capacity", rate.Limit)
	MustBeTrue(r.scene.TTL == 0 || rate.Period <= r.scene.TTL, "rate.period(%s) is too larger than TTL", rate.Period)
}

func MustBeTrue(flag bool, format string, args ...interface{}) {
	if !flag {
		panic(fmt.Sprintf(format, args...))
	}
}

func MustBeSuccess(err error) {
	if err != nil {
		panic(err.Error())
	}
}
