package limiter

import (
	"code.gitea.io/gitea/models"
	"code.gitea.io/gitea/modules/log"
	"code.gitea.io/gitea/modules/redis/redis_client"
	"code.gitea.io/gitea/modules/redis/redis_key"
	"code.gitea.io/gitea/services/task/period"
	"encoding/json"
	"errors"
	"fmt"
	"time"
)

type limiterRunner struct {
	limiters      []models.LimitConfig
	index         int
	userId        int64
	amount        int64
	limitCode     string
	limitType     models.LimitType
	rejectPolicy  models.LimiterRejectPolicy
	resultMap     map[int]limitResult
	minRealAmount int64
}

type limitResult struct {
	isLoss     bool
	planAmount int64
	realAmount int64
}

func newLimitResult(isLoss bool, planAmount int64, realAmount int64) limitResult {
	return limitResult{
		isLoss:     isLoss,
		planAmount: planAmount,
		realAmount: realAmount,
	}
}

func newLimiterRunner(limitCode string, limitType models.LimitType, userId, amount int64, policy models.LimiterRejectPolicy) *limiterRunner {
	return &limiterRunner{
		userId:       userId,
		amount:       amount,
		limitCode:    limitCode,
		limitType:    limitType,
		index:        0,
		rejectPolicy: policy,
		resultMap:    make(map[int]limitResult, 0),
	}
}

//Run run all limiters
//return real used amount(when choose the FillUp reject policy, amount may only be partially used)
func (l *limiterRunner) Run() error {
	if err := l.LoadLimiters(); err != nil {
		return err
	}
	l.minRealAmount = l.amount
	for l.index < len(l.limiters) {
		err := l.limit(l.limiters[l.index])
		if err != nil {
			log.Info("limiter check failed,%v", err)
			l.Rollback()
			return err
		}
		result := l.resultMap[l.index]
		if result.isLoss {
			//find the minimum real amount
			if l.minRealAmount > result.realAmount {
				l.minRealAmount = result.realAmount
			}
		}
		l.index += 1
	}

	//post process
	l.PostProcess()
	return nil
}

//Rollback rollback the usedNum  from limiters[0] to limiters[index]
func (l *limiterRunner) Rollback() error {
	for i := l.index - 1; i >= 0; i-- {
		l.rollback(l.limiters[i], l.resultMap[i])
	}
	return nil
}

func (l *limiterRunner) rollback(r models.LimitConfig, result limitResult) error {
	p, err := period.GetPeriod(r.RefreshRate)
	if err != nil {
		return err
	}
	redisKey := redis_key.LimitCount(l.userId, r.LimitCode, r.LimitType, r.Scope, p)
	redis_client.IncrBy(redisKey, -1*result.realAmount)
	return nil
}

//PostProcess process loss,if realAmount < planAmount
func (l *limiterRunner) PostProcess() error {
	for i := l.index - 1; i >= 0; i-- {
		l.postProcess(l.limiters[i], l.resultMap[i])
	}
	return nil
}

func (l *limiterRunner) postProcess(r models.LimitConfig, result limitResult) error {
	if result.realAmount == l.minRealAmount {
		return nil
	}
	p, err := period.GetPeriod(r.RefreshRate)
	if err != nil {
		return err
	}
	diff := result.realAmount - l.minRealAmount
	redisKey := redis_key.LimitCount(l.userId, r.LimitCode, r.LimitType, r.Scope, p)
	redis_client.IncrBy(redisKey, -1*diff)
	return nil
}

func (l *limiterRunner) limit(r models.LimitConfig) error {
	p, err := period.GetPeriod(r.RefreshRate)
	if err != nil {
		return err
	}
	redisKey := redis_key.LimitCount(l.userId, r.LimitCode, r.LimitType, r.Scope, p)
	usedNum, err := redis_client.IncrBy(redisKey, l.amount)
	if err != nil {
		return err
	}
	//if usedNum equals  amount,it is the first operation in period or redis cache deleted
	//count in database to distinguish the two cases
	if usedNum == l.amount {
		n, err := l.countInPeriod(r, p)
		if err != nil {
			return err
		}
		if n > 0 {
			//means redis cache deleted,incr the cache with real value
			usedNum, err = redis_client.IncrBy(redisKey, n)
		}
		if p != nil {
			redis_client.Expire(redisKey, p.LeftTime)
		} else {
			//add default expire time if no period set
			redis_client.Expire(redisKey, 24*time.Hour)
		}
	}
	if usedNum > r.LimitNum {
		if usedNum-r.LimitNum >= l.amount {
			redis_client.IncrBy(redisKey, -1*l.amount)
			return errors.New(fmt.Sprintf("over limit,congfigId=%d", r.ID))
		}
		switch l.rejectPolicy {
		case models.FillUp:
			exceed := usedNum - r.LimitNum
			realAmount := l.amount - exceed
			redis_client.IncrBy(redisKey, -1*exceed)
			l.resultMap[l.index] = newLimitResult(true, l.amount, realAmount)
			return nil
		case models.JustReject:
			redis_client.IncrBy(redisKey, -1*l.amount)
			return errors.New(fmt.Sprintf("over limit,congfigId=%d", r.ID))
		case models.PermittedOnce:
			l.resultMap[l.index] = newLimitResult(false, l.amount, l.amount)
			return nil
		}

	}
	l.resultMap[l.index] = newLimitResult(false, l.amount, l.amount)
	return nil
}

func (l *limiterRunner) LoadLimiters() error {
	limiters, err := GetLimiters(l.limitCode, l.limitType)
	if err != nil {
		return err
	}
	if limiters != nil {
		l.limiters = limiters
	}
	return nil
}

func (l *limiterRunner) countInPeriod(r models.LimitConfig, p *models.PeriodResult) (int64, error) {
	switch r.LimitType {
	case models.LimitTypeTask.Name():
		return models.CountTaskAccomplishLogInTaskPeriod(r.LimitCode, l.userId, p)
	case models.LimitTypeRewardPoint.Name():
		return models.SumRewardAmountInTaskPeriod(models.RewardTypePoint.Name(), r.LimitCode, l.userId, p)
	default:
		return 0, nil

	}
}

func CheckLimit(limitCode string, limitType models.LimitType, userId, amount int64, rejectPolicy models.LimiterRejectPolicy) (int64, error) {
	if rejectPolicy == "" {
		rejectPolicy = models.JustReject
	}
	r := newLimiterRunner(limitCode, limitType, userId, amount, rejectPolicy)
	err := r.Run()
	if err != nil {
		return 0, err
	}
	return r.minRealAmount, nil
}

func GetLimiters(limitCode string, limitType models.LimitType) ([]models.LimitConfig, error) {
	limiters, err := GetLimitersByLimitType(limitType)
	if err != nil {
		return nil, err
	}
	result := make([]models.LimitConfig, 0)
	for i, v := range limiters {
		if v.LimitCode == "" || v.LimitCode == limitCode {
			result = append(result, limiters[i])
		}
	}
	return result, nil
}

func GetLimitersByLimitType(limitType models.LimitType) ([]models.LimitConfig, error) {
	redisKey := redis_key.LimitConfig(limitType.Name())
	val, _ := redis_client.Get(redisKey)
	if val != "" {
		if val == redis_key.EMPTY_REDIS_VAL {
			return nil, nil
		}
		limiters := make([]models.LimitConfig, 0)
		json.Unmarshal([]byte(val), &limiters)
		return limiters, nil
	}
	limiters, err := models.GetLimitConfigByLimitType(limitType)
	if err != nil {
		if models.IsErrRecordNotExist(err) {
			redis_client.Setex(redisKey, redis_key.EMPTY_REDIS_VAL, 5*time.Second)
			return nil, nil
		}
		return nil, err
	}
	jsonStr, _ := json.Marshal(limiters)
	redis_client.Setex(redisKey, string(jsonStr), 30*24*time.Hour)

	return limiters, nil
}

func GetLimitersByRelatedIdWithDeleted(limitType models.LimitType) ([]models.LimitConfig, error) {
	limiters, err := models.GetLimitersByRelatedIdWithDeleted(limitType)
	if err != nil {
		if models.IsErrRecordNotExist(err) {
			return nil, nil
		}
		return nil, err
	}
	return limiters, nil
}
