package spend_limit

import (
	"errors"
	"fmt"
	"sort"

	"github.com/google/uuid"
	"github.com/pingcap/dbaas/billing2/billing_errors"
	"github.com/pingcap/dbaas/billing2/config"
	"github.com/pingcap/dbaas/billing2/constant"
	"github.com/pingcap/dbaas/billing2/db"
	"github.com/pingcap/dbaas/billing2/db/entity"
	"github.com/pingcap/dbaas/billing2/forms"
	"github.com/pingcap/dbaas/billing2/handler/centralhandler"
	"github.com/pingcap/dbaas/billing2/handler/eventbushandler"
	"github.com/pingcap/dbaas/billing2/model/costexplorer"
	"github.com/pingcap/dbaas/billing2/utils/errorutils"
	"github.com/pingcap/dbaas/billing2/utils/labels"
	"github.com/pingcap/dbaas/billing2/utils/timeutils"
	"github.com/pingcap/dbaas/billing2/utils/uncutils"
	"github.com/pingcap/log"
	"github.com/shopspring/decimal"
	"go.uber.org/zap"
	"gorm.io/gorm"
	"gorm.io/gorm/clause"
)

type SpendLimitModel struct{}

func (*SpendLimitModel) validateInput(setting *forms.SpendLimitSetting) (err error) {
	if setting.Interval != entity.SpendLimitInterval_Monthly {
		return billing_errors.NewInvalidInputError(errors.New("interval only supports monthly "))
	}
	if setting.RangeType != entity.SpendLimitRangeType_Tenant && setting.RangeType != entity.SpendLimitRangeType_Cluster && setting.RangeType != entity.SpendLimitRangeType_Project && setting.RangeType != entity.SpendLimitRangeType_Customer {
		return billing_errors.NewInvalidInputError(errors.New("range type only supports tenant / cluster / project / customer"))
	}
	if setting.CostType != entity.SpendLimitCostType_Credits && setting.CostType != entity.SpendLimitCostType_Invoice && setting.CostType != entity.SpendLimitCostType_Total {
		return billing_errors.NewInvalidInputError(errors.New("cost type only supports credits / invoice / total"))
	}
	if setting.Limit.LessThanOrEqual(decimal.Zero) {
		return billing_errors.NewInvalidInputError(errors.New("spend limit should be positive"))
	}
	return
}

func (m *SpendLimitModel) SpendingLimitToBudget(spendingLimit forms.SpendLimitSetting, withMeta bool) (*forms.Budget, error) {
	if spendingLimit.BizType == entity.SpendLimitBizType_ServerlessCluster {
		return m.ServerlessSpendingLimitToBudget(&spendingLimit, withMeta)
	}
	info, err := m.SpendLimitSettingWithCost(&spendingLimit)
	if err != nil {
		return nil, err
	}
	thresholds, err := m.ListTenantThresholds(info.SpendLimitSettingID, spendingLimit.BizType)
	if err != nil {
		return nil, err
	}
	ret := &forms.Budget{SpendLimitInfo: *info, Thresholds: thresholds}
	return ret, nil
}

func (m *SpendLimitModel) SpendingLimitToBudgetView(spendingLimit forms.SpendLimitSetting) (*forms.Budget, error) {
	return m.SpendingLimitToBudget(spendingLimit, true)
}

func (m *SpendLimitModel) checkBizUniqueKey(bizUniqueKey *string, ignoreIDs ...uint) (err error) {
	if bizUniqueKey != nil {
		query := entity.NewSpendLimitSettingQuerySet(db.GetDB()).BizUniqueKeyEq(*bizUniqueKey)
		if len(ignoreIDs) != 0 {
			query = query.IDNotIn(ignoreIDs...)
		}
		cnt, err := query.Count()
		if err != nil {
			return err
		}
		if cnt != 0 {
			return billing_errors.NewInvalidInputError(errors.New("duplicated BizUniqueKey"))
		}
	}
	return
}

func (m *SpendLimitModel) CreateSpendLimitSetting(tenantID uint, setting *forms.SpendLimitSetting) (res *forms.SpendLimitSetting, err error) {
	if err = m.validateInput(setting); err != nil {
		return
	}
	setting.TenantID = tenantID
	setting.BizUniqueKey = m.genBizUniqueKey(setting)
	if err = m.checkBizUniqueKey(setting.BizUniqueKey); err != nil {
		return nil, err
	}
	var spendLimitSetting *entity.SpendLimitSetting
	err = db.GetDB().Transaction(func(tx *gorm.DB) error {
		if err := m.bizProcessOnCreating(tx, setting); err != nil {
			return err
		}
		spendLimitSetting = &entity.SpendLimitSetting{
			Name:         setting.Name,
			TenantID:     tenantID,
			Interval:     setting.Interval,
			CostType:     setting.CostType,
			RangeType:    setting.RangeType,
			RangeID:      setting.RangeID,
			Limit:        setting.Limit,
			Status:       entity.SpendLimitStatus_Fit,
			BizType:      setting.BizType,
			BizData:      setting.BizData,
			BizUniqueKey: setting.BizUniqueKey,
		}
		return spendLimitSetting.Create(tx)
	})
	if err != nil {
		log.Error("failed to create spend limit setting", zap.Error(err), zap.Uint("tenantID", tenantID))
		return nil, err
	}
	log.Info("success create new spend limit setting", zap.Uint("tenantID", tenantID))
	res = spendLimitSettingFromEntity(spendLimitSetting)
	err = m.bizProcessAfterCreating(spendLimitSettingFromEntity(spendLimitSetting))
	if err != nil {
		log.Error("failed process on bizProcessAfterCreating", zap.Error(err))
	}
	err = m.CheckSpendLimit(tenantID, res.SpendLimitSettingID)
	if err != nil {
		log.Error("failed to check spend limit on create", zap.Error(err), zap.Uint("tenantID", tenantID), zap.Uint("spendLimitSettingID", res.SpendLimitSettingID))
	}
	return m.GetSpendLimitSetting(tenantID, res.SpendLimitSettingID)
}

func (m *SpendLimitModel) UpdateSpendLimitSettings(tenantID uint, spendLimitSettingID uint, setting *forms.SpendLimitSetting) (res *forms.SpendLimitSetting, err error) {
	if err = m.validateInput(setting); err != nil {
		return
	}
	err = db.GetDB().Transaction(func(tx *gorm.DB) error {
		spendLimitSettingEntity := &entity.SpendLimitSetting{}
		err = errorutils.WrapSQLError(entity.NewSpendLimitSettingQuerySet(tx.Clauses(clause.Locking{Strength: "UPDATE"})).IDEq(spendLimitSettingID).One(spendLimitSettingEntity), fmt.Sprintf("Spend Limit Setting %d", spendLimitSettingID))
		if err != nil {
			return err
		}
		origin := spendLimitSettingFromEntity(spendLimitSettingEntity)
		setting.TenantID = tenantID
		setting.SpendLimitSettingID = spendLimitSettingID
		setting.BizUniqueKey = m.genBizUniqueKey(setting)
		if spendLimitSettingEntity.BizUniqueKey != origin.BizUniqueKey {
			err = errors.New("biz unique key changed")
			log.Error(err.Error(), zap.Error(err), zap.String("origin", *origin.BizUniqueKey), zap.String("new", *spendLimitSettingEntity.BizUniqueKey))
			return err
		}
		if err = m.checkBizUniqueKey(setting.BizUniqueKey, spendLimitSettingID); err != nil {
			return err
		}
		if err := m.bizProcessOnUpdating(tx, setting, origin); err != nil {
			return err
		}
		updater := entity.NewSpendLimitSettingQuerySet(tx).IDEq(spendLimitSettingID).TenantIDEq(tenantID).GetUpdater()
		err = updater.SetInterval(setting.Interval).SetCostType(setting.CostType).SetName(setting.Name).
			SetRangeType(setting.RangeType).SetRangeID(setting.RangeID).SetLimit(setting.Limit).
			SetBizData(setting.BizData).Update()
		if err != nil {
			return billing_errors.NewSystemError(err)
		}
		return nil
	})
	if err != nil {
		log.Error("failed to update spend limit setting", zap.Error(err), zap.Uint("tenantID", tenantID), zap.Uint("spendLimitSettingID", spendLimitSettingID))
		return
	}
	log.Info("success to update spend limit setting", zap.Uint("tenantID", tenantID), zap.Uint("spendLimitSettingID", spendLimitSettingID))
	err = m.bizProcessAfterUpdating(setting)
	if err != nil {
		log.Error("failed process on bizProcessAfterUpdating", zap.Error(err))
	}
	err = m.CheckSpendLimit(tenantID, spendLimitSettingID)
	if err != nil {
		log.Error("failed to check spend limit on update", zap.Error(err), zap.Uint("tenantID", tenantID), zap.Uint("spendLimitSettingID", spendLimitSettingID))
	}
	return m.GetSpendLimitSetting(tenantID, spendLimitSettingID)
}

func spendLimitSettingFromEntity(entity *entity.SpendLimitSetting) *forms.SpendLimitSetting {
	return &forms.SpendLimitSetting{
		SpendLimitSettingID: entity.ID,
		Name:                entity.Name,
		TenantID:            entity.TenantID,
		Interval:            entity.Interval,
		CostType:            entity.CostType,
		RangeType:           entity.RangeType,
		RangeID:             entity.RangeID,
		RangeIDStr:          entity.RangeID,
		Limit:               entity.Limit,
		Status:              entity.Status,
		BizType:             entity.BizType,
		BizData:             entity.BizData,
		BizUniqueKey:        entity.BizUniqueKey,
		WaterMark:           entity.WaterMark,
		CreatedAt:           entity.CreatedAt,

		HasServerlessFreeCredits: entity.HasServerlessFreeCredits,
	}
}

func thresholdFromEntity(ent entity.SpendLimitAlertThreshold) *forms.SpendLimitThreshold {
	return &forms.SpendLimitThreshold{
		ThresholdID:  ent.ID,
		TenantID:     ent.TenantID,
		SpendLimitID: ent.SpendingLimitID,
		BizType:      ent.BizType,
		Proportion:   ent.Proportion,
	}
}

func (m *SpendLimitModel) GetSpendLimitSetting(tenantID uint, spendLimitSettingID uint) (ret *forms.SpendLimitSetting, err error) {
	spendLimitSettingEntity := &entity.SpendLimitSetting{}
	err = errorutils.WrapSQLError(entity.NewSpendLimitSettingQuerySet(db.GetDB()).IDEq(spendLimitSettingID).TenantIDEq(tenantID).One(spendLimitSettingEntity), fmt.Sprintf("Spend Limit Setting %d", spendLimitSettingID))
	if err != nil {
		return nil, err
	}
	ret, err = m.bizProcessOnGetting(db.GetDB(), spendLimitSettingFromEntity(spendLimitSettingEntity))
	if err != nil {
		return nil, err
	}
	if ret.Status == entity.SpendLimitStatus_Recovered {
		sli, err := m.SpendLimitSettingWithCost(ret)
		if err != nil {
			return nil, err
		}
		err = m.checkSpendLimitStatus(sli)
		if err != nil {
			return nil, err
		}
		return m.GetSpendLimitSetting(tenantID, spendLimitSettingID)
	}
	return ret, err
}

func (m *SpendLimitModel) DeleteSpendLimitSetting(tenantID uint, spendLimitSettingID uint) (err error) {
	info, err := m.GetSpendLimitInfo(tenantID, spendLimitSettingID)
	if err != nil {
		return
	}
	err = db.GetDB().Transaction(func(tx *gorm.DB) (err error) {
		var ent entity.SpendLimitSetting
		err = entity.NewSpendLimitSettingQuerySet(tx.Clauses(clause.Locking{Strength: "UPDATE"})).IDEq(spendLimitSettingID).One(&ent)
		status := entity.SpendLimitStatus_Deprecated
		if err != nil {
			if errors.Is(err, billing_errors.ObjectNotExistsError) {
				log.Info("spending limit has been deleted or is not existed", zap.Uint("SpendLimitSettingID", info.SpendLimitSettingID))
				return nil
			}
			return
		} else if ent.Status == status {
			log.Info("Ignore deleted spend limit", zap.Uint("SpendLimitSettingID", info.SpendLimitSettingID))
			return
		}
		query := entity.NewSpendLimitSettingQuerySet(tx).IDEq(spendLimitSettingID).TenantIDEq(tenantID)
		err = query.GetUpdater().SetStatus(status).SetBizUniqueKey(nil).Update()
		if err != nil {
			return
		}
		info.Status = status
		err = m.sendSpendLimitStatusChangeEvent(tx, info)
		if err != nil {
			return
		}
		err = m.bizProcessOnDeleting(tx, &info.SpendLimitSetting)
		if err != nil {
			return err
		}
		err = query.Delete()
		return
	})
	if err != nil {
		log.Error("failed to delete spend limit setting", zap.Error(err), zap.Uint("tenantID", tenantID), zap.Uint("spendLimitSettingID", spendLimitSettingID))
		return
	}
	log.Info("success to delete spend limit setting", zap.Uint("tenantID", tenantID), zap.Uint("spendLimitSettingID", spendLimitSettingID))
	return
}

func (m *SpendLimitModel) SpendLimitSettingWithCost(setting *forms.SpendLimitSetting) (ret *forms.SpendLimitInfo, err error) {
	if setting.BizType == entity.SpendLimitBizType_ServerlessCluster {
		return m.ServerlessSpendLimitSettingWithCost(setting, true)
	}
	costExplorerModel := &costexplorer.CostExplorerModel{}

	now := timeutils.GetNow()

	var costSummary *costexplorer.SpendLimitCostSummary
	switch setting.RangeType {
	//don't need billingConfigID
	case entity.SpendLimitRangeType_Tenant:
		costSummary, err = costExplorerModel.GetMonthlySpendLimitCost(setting.TenantID, 0, now.Year(), int(now.Month()))
	case entity.SpendLimitRangeType_Project:
		costSummary, err = costExplorerModel.GetProjectMonthlySpendLimitCost(setting.TenantID, 0, now.Year(), int(now.Month()), setting.RangeID)
	case entity.SpendLimitRangeType_Cluster:
		costSummary, err = costExplorerModel.GetClusterMonthlySpendLimitCost(setting.TenantID, 0, now.Year(), int(now.Month()), setting.RangeID)
	case entity.SpendLimitRangeType_Customer:
		costSummary, err = costExplorerModel.GetCustomerMonthlySpendLimitCost(setting.TenantID, 0, now.Year(), int(now.Month()), setting.RangeID)
	default:
		err = billing_errors.NewSystemError(errors.New("unknown rangeType"))
	}
	if err != nil {
		return nil, err
	}
	costs := decimal.Zero
	switch setting.CostType {
	case entity.SpendLimitCostType_Total:
		costs = costSummary.TotalRawPrice
		if setting.BizType == entity.SpendLimitBizType_Normal {
			bizData, err := m.UnmarshallNormalBizData(setting.BizData)
			if err != nil {
				return nil, err
			}
			if bizData.ApplyDiscount && bizData.ApplyCredits {
				costs = costSummary.InvoiceCostPrice
			} else if bizData.ApplyDiscount {
				costs = costSummary.TotalCostPrice
			} else if bizData.ApplyCredits {
				costs = costSummary.TotalRawPrice.Sub(costSummary.CreditsCostPrice)
			} else {
				costs = costSummary.TotalRawPrice
			}
		}
	case entity.SpendLimitCostType_Invoice:
		costs = costSummary.InvoiceCostPrice
	case entity.SpendLimitCostType_Credits:
		return nil, billing_errors.NewSystemError(errors.New("invalid spend limit cost type"))
	}

	return &forms.SpendLimitInfo{
		SpendLimitSetting: *setting,
		Costs:             costs,
		StartDate:         timeutils.GetMonthDate(now),
	}, nil
}

func (m *SpendLimitModel) GetSpendLimitInfo(tenantID uint, spendLimitSettingID uint) (*forms.SpendLimitInfo, error) {
	setting, err := m.GetSpendLimitSetting(tenantID, spendLimitSettingID)
	if err != nil {
		return nil, err
	}
	return m.SpendLimitSettingWithCost(setting)
}

func (m *SpendLimitModel) checkSpendLimitStatus(info *forms.SpendLimitInfo) (err error) {
	var status string
	if info.Costs.Cmp(info.Limit) >= 0 {
		status = entity.SpendLimitStatus_Excessive
	} else {
		status = entity.SpendLimitStatus_Fit
	}
	if info.Status == status {
		return
	}
	log.Info("spend limit status changed",
		zap.Uint("spendLimitSettingID", info.SpendLimitSettingID), zap.Uint("tenantID", info.TenantID),
		zap.String("RangeType", info.RangeType), zap.Uint("RangeID", info.RangeID), zap.String("OldStatus", info.Status),
		zap.String("limit", info.Limit.String()), zap.String("TargetStatus", status),
	)
	info.Status = status
	return db.GetDB().Transaction(func(tx *gorm.DB) error {
		err = entity.NewSpendLimitSettingQuerySet(tx).IDEq(info.SpendLimitSettingID).GetUpdater().SetStatus(status).Update()
		if err != nil {
			return err
		}
		return m.sendSpendLimitStatusChangeEvent(tx, info)
	})
}

func (m *SpendLimitModel) sendSpendLimitStatusChangeEvent(tx *gorm.DB, info *forms.SpendLimitInfo) (err error) {
	return eventbushandler.Get().PublishEventWithTransaction(
		constant.SpendLimitEventType,
		&eventbushandler.Event{
			EventID:   uuid.NewString(),
			EventType: constant.SpendLimitEventType,
			EventData: info,
		},
		tx,
	)
}

func (m *SpendLimitModel) sendSpendLimitProportionChangeEvent(tx *gorm.DB, info *forms.SpendLimitThresholdAlert) (err error) {
	return eventbushandler.Get().PublishEventWithTransaction(
		constant.SpendLimitThresholdEventType,
		&eventbushandler.Event{
			EventID:   uuid.NewString(),
			EventType: constant.SpendLimitThresholdEventType,
			EventData: info,
		},
		tx,
	)
}

func (m *SpendLimitModel) sendSpendLimitReachedNotification(info *forms.SpendLimitThresholdAlert) {
	notificationEvent := ""
	payload := map[string]string{
		"actualCost":   info.Costs.String(),
		"budgetAmount": info.Limit.String(),
		"threshold":    info.Threshold.Mul(decimal.NewFromInt(100)).String(),
		"orgId":        fmt.Sprintf("%d", info.TenantID),
	}
	// org budget
	if info.RangeType == entity.SpendLimitRangeType_Tenant {
		notificationEvent = uncutils.ReachedOrgBudgetThreshold
		// query tenant info
		tenant, err := centralhandler.GetCentralHandler().GetTenant(info.TenantID)
		if err != nil {
			log.Error("fail to send in-app notification for spend limit reached, get tenant info error", zap.Uint("tenantID", info.TenantID), zap.Uint("tenantID", info.RangeID), zap.Error(err))
			return
		}
		payload["orgName"] = tenant.Name
		payload["budgetName"] = info.Name
	}
	// project budget
	if info.RangeType == entity.SpendLimitRangeType_Project {
		notificationEvent = uncutils.ReachedProjectBudgetThreshold
		projectName, err := labels.GetProjectName(info.RangeID)
		if err != nil {
			log.Error("fail to send in-app notification for spend limit reached, get project name error", zap.Uint("tenantID", info.TenantID), zap.Uint("projectID", info.RangeID), zap.Error(err))
			return
		}
		payload["projectName"] = projectName
		payload["projectId"] = fmt.Sprintf("%d", info.RangeID)
		payload["budgetName"] = info.Name
	}
	// cluster budget
	if info.RangeType == entity.SpendLimitRangeType_Cluster {
		notificationEvent = uncutils.ReachedClusterBudgetThreshold
		cluster, err := labels.GetClusterWithCache(info.TenantID, uint(info.RangeID))
		if err != nil {
			log.Error("fail to send in-app notification for spend limit reached, get cluster info error", zap.Uint("tenantID", info.TenantID), zap.Uint("clusterID", info.RangeID), zap.Error(err))
			return
		}
		payload["clusterName"] = cluster.Name
		payload["projectId"] = fmt.Sprintf("%d", cluster.ProjectID)
		payload["budgetName"] = "Budget for " + cluster.Name
	}
	uncutils.SendInAppNotification(info.TenantID, notificationEvent, payload)
}

func (m *SpendLimitModel) checkSpendLimitProportion(info *forms.Budget) (err error) {
	if info.Limit.LessThanOrEqual(decimal.Zero) {
		log.Info("limit is zero, checking threshold stop", zap.Uint("SpendLimitSettingID", info.SpendLimitSettingID))
		return
	}
	if info.Costs.LessThanOrEqual(decimal.Zero) && info.WaterMark.LessThanOrEqual(decimal.Zero) {
		log.Info("bizCost is zero and same as previous checking, checking threshold stop", zap.Uint("SpendLimitSettingID", info.SpendLimitSettingID))
		return
	}
	err = db.GetDB().Transaction(func(tx *gorm.DB) error {
		for i := len(info.Thresholds) - 1; i >= 0; i-- {
			t := info.Thresholds[i]
			threshold := t.Proportion.Mul(info.Limit)
			if threshold.LessThanOrEqual(info.Costs) && (info.WaterMark.LessThan(threshold) || info.WaterMark.GreaterThan(info.Costs)) {
				log.Info(fmt.Sprintf("The threshold %s is reached and the alarm condition is met", t.Proportion.String()), zap.String("threshold", threshold.String()),
					zap.Uint("SpendLimitSettingID", info.SpendLimitSettingID), zap.String("bizCost", info.Costs.String()))
				alert := &forms.SpendLimitThresholdAlert{SpendLimitInfo: info.SpendLimitInfo, Threshold: t.Proportion}
				alert.WaterMark = info.Costs
				err = m.sendSpendLimitProportionChangeEvent(tx, alert)
				if err != nil {
					return err
				}
				m.sendSpendLimitReachedNotification(alert)
				break
			}
		}
		log.Info("Update Spend Limit watermark", zap.Uint("SpendLimitSettingID", info.SpendLimitSettingID), zap.String("bizCost", info.Costs.String()))
		err = entity.NewSpendLimitSettingQuerySet(tx).IDEq(info.SpendLimitSettingID).GetUpdater().SetWaterMark(info.Costs).Update()
		return err
	})
	return err
}

func (m *SpendLimitModel) CheckSpendLimit(tenantID uint, spendLimitSettingID uint) (err error) {
	info, err := m.GetSpendLimitInfo(tenantID, spendLimitSettingID)
	if err != nil {
		return
	}
	if info.Status == entity.SpendLimitStatus_Closing {
		log.Info("skip check on closing", zap.Uint("tenantID", tenantID), zap.Uint("spendLimitSettingID", spendLimitSettingID))
		return nil
	}
	err = m.checkSpendLimitStatus(info)
	if err != nil {
		return
	}
	budget, err := m.SpendingLimitToBudget(info.SpendLimitSetting, false)
	if err != nil {
		if errors.Is(err, billing_errors.ObjectNotExistsError) {
			return nil
		}
		return err
	}
	err = m.checkSpendLimitProportion(budget)
	if err != nil {
		return
	}
	return
}

func (m *SpendLimitModel) ListAllSpendLimitSettings(ignoreBizProc bool) ([]forms.SpendLimitSetting, error) {
	return m.ListSpendLimitSettings(0, &forms.SpendLimitFilter{IgnoreBizProc: ignoreBizProc}, nil)
}

func (m *SpendLimitModel) ListTenantSpendLimitSettings(tenantID uint, ignoreBizProc bool) ([]forms.SpendLimitSetting, error) {
	return m.ListSpendLimitSettings(tenantID, &forms.SpendLimitFilter{IgnoreBizProc: ignoreBizProc}, nil)
}

func (m *SpendLimitModel) ListTenantSpendLimitSettingsAfterID(tenantID uint, ignoreBizProc bool, afterID uint, limit int) (ret []forms.SpendLimitSetting, err error) {
	entities := []entity.SpendLimitSetting{}
	query := entity.NewSpendLimitSettingQuerySet(db.GetDB()).TenantIDEq(tenantID).OrderAscByID()
	if afterID != 0 {
		query = query.IDGt(afterID)
	}
	if limit > 0 {
		query = query.Limit(limit)
	}
	err = query.All(&entities)
	if err != nil {
		return nil, err
	}
	ret = make([]forms.SpendLimitSetting, 0, len(entities))
	for _, spendLimitSettingEntity := range entities {
		sl := spendLimitSettingFromEntity(&spendLimitSettingEntity)
		ret = append(ret, *sl)
	}
	if !ignoreBizProc {
		ret, err = m.bizProcessOnListing(db.GetDB(), ret)
		if err != nil {
			return nil, err
		}
	}
	return ret, nil
}

func (m *SpendLimitModel) ListSpendLimitSettings(tenantID uint, filterQuery *forms.SpendLimitFilter, pageQuery *forms.PageQuery) (ret []forms.SpendLimitSetting, err error) {
	entities := []entity.SpendLimitSetting{}
	query := entity.NewSpendLimitSettingQuerySet(db.GetDB()).OrderDescByCreatedAt()
	if tenantID != 0 {
		query = query.TenantIDEq(tenantID)
	}
	if filterQuery != nil {
		if filterQuery.RangeType != "" {
			query = query.RangeTypeEq(filterQuery.RangeType)
			if filterQuery.RangeID != 0 {
				query = query.RangeIDEq(filterQuery.RangeID)
			}
		}
		if filterQuery.BizType != "" {
			query = query.BizTypeEq(filterQuery.BizType)
		}
		if filterQuery.HasServerlessFreeCredits != nil {
			query = query.HasServerlessFreeCreditsEq(*filterQuery.HasServerlessFreeCredits)
		}
	}
	if pageQuery != nil {
		query = query.Limit(pageQuery.PageSize).Offset((pageQuery.Page - 1) * pageQuery.PageSize)
	}
	err = query.All(&entities)
	if err != nil {
		return nil, err
	}
	for _, spendLimitSettingEntity := range entities {
		sl := spendLimitSettingFromEntity(&spendLimitSettingEntity)
		ret = append(ret, *sl)
	}
	if filterQuery == nil || !filterQuery.IgnoreBizProc {
		ret, err = m.bizProcessOnListing(db.GetDB(), ret)
	}
	for i := 0; i != len(ret); i++ {
		if ret[i].Status == entity.SpendLimitStatus_Recovered {
			// ensure that all recovered spending limits are with correct status
			// it may be make the api slower, but accuracy over latency
			err = m.CheckSpendLimit(tenantID, ret[i].SpendLimitSettingID)
			if err != nil {
				return nil, err
			}
			flushedSpendingLimit, err := m.GetSpendLimitSetting(tenantID, ret[i].SpendLimitSettingID)
			if err != nil {
				return nil, err
			}
			ret[i] = *flushedSpendingLimit
		}
	}
	return
}

func (m *SpendLimitModel) genBizUniqueKey(settings *forms.SpendLimitSetting) (ret *string) {
	switch settings.BizType {
	case entity.SpendLimitBizType_ServerlessCluster:
		return m.genServerlessBizUniqueKey(settings)
	}
	return
}

func (m *SpendLimitModel) bizProcessOnCreating(tx *gorm.DB, settings *forms.SpendLimitSetting) (err error) {
	switch settings.BizType {
	case entity.SpendLimitBizType_ServerlessCluster:
		err = m.serverlessProcessOnCreating(tx, settings)
	case entity.SpendLimitBizType_Normal:
		err = m.normalProcessOnCreating(tx, settings)
	}
	return
}

func (m *SpendLimitModel) bizProcessAfterCreating(settings *forms.SpendLimitSetting) (err error) {
	switch settings.BizType {
	case entity.SpendLimitBizType_ServerlessCluster:
		err = m.serverlessProcessAfterCreating(settings)
	case entity.SpendLimitBizType_Normal:
		err = m.normalProcessAfterCreating(settings)
	}
	return
}

func (m *SpendLimitModel) bizProcessOnUpdating(tx *gorm.DB, settings *forms.SpendLimitSetting, origin *forms.SpendLimitSetting) (err error) {
	switch settings.BizType {
	case entity.SpendLimitBizType_ServerlessCluster:
		err = m.serverlessProcessOnUpdating(tx, settings, origin)
	case entity.SpendLimitBizType_Normal:
		err = m.normalProcessOnUpdating(tx, settings, origin)
	}
	return
}

func (m *SpendLimitModel) bizProcessAfterUpdating(settings *forms.SpendLimitSetting) (err error) {
	switch settings.BizType {
	case entity.SpendLimitBizType_ServerlessCluster:
		err = m.serverlessProcessAfterUpdating(settings)
	}
	return
}

func (m *SpendLimitModel) bizProcessOnListing(tx *gorm.DB, settingsList []forms.SpendLimitSetting) (ret []forms.SpendLimitSetting, err error) {
	ret = make([]forms.SpendLimitSetting, len(settingsList))
	copy(ret, settingsList)
	bizSpendMap := make(map[string][]*forms.SpendLimitSetting)
	for i := 0; i != len(ret); i++ {
		sl := &ret[i]
		if _, ok := bizSpendMap[sl.BizType]; !ok {
			bizSpendMap[sl.BizType] = []*forms.SpendLimitSetting{sl}
		} else {
			bizSpendMap[sl.BizType] = append(bizSpendMap[sl.BizType], sl)
		}
	}
	for bizType, list := range bizSpendMap {
		switch bizType {
		case entity.SpendLimitBizType_ServerlessCluster:
			err = m.serverlessProcessOnListing(tx, list)
		case entity.SpendLimitBizType_Normal:
		}
		if err != nil {
			return nil, err
		}
	}
	return ret, nil
}

func (m *SpendLimitModel) bizProcessOnGetting(tx *gorm.DB, settings *forms.SpendLimitSetting) (ret *forms.SpendLimitSetting, err error) {
	switch settings.BizType {
	case entity.SpendLimitBizType_ServerlessCluster:
		err = m.serverlessProcessOnGetting(tx, settings)
	case entity.SpendLimitBizType_Normal:
	}
	if err != nil {
		return nil, err
	}
	ret = settings
	return
}

func (m *SpendLimitModel) bizProcessOnDeleting(tx *gorm.DB, settings *forms.SpendLimitSetting) (err error) {
	switch settings.BizType {
	case entity.SpendLimitBizType_ServerlessCluster:
		err = m.serverlessProcessOnDeleting(tx, settings)
	case entity.SpendLimitBizType_Normal:
	}
	if err != nil {
		return err
	}
	return
}

func (m *SpendLimitModel) ListTenantThresholds(spendLimitID uint, bizType string) ([]*forms.SpendLimitThreshold, error) {
	var entities []entity.SpendLimitAlertThreshold
	switch bizType {
	case entity.SpendLimitBizType_Normal:
		err := entity.NewSpendLimitAlertThresholdQuerySet(db.GetDB()).BizTypeEq(bizType).SpendingLimitIDEq(spendLimitID).All(&entities)
		if err != nil {
			return nil, err
		}
	case entity.SpendLimitBizType_ServerlessCluster:
		err := entity.NewSpendLimitAlertThresholdQuerySet(db.GetDB()).BizTypeEq(bizType).All(&entities)
		if err != nil {
			return nil, err
		}
	}
	sort.Slice(entities, func(i, j int) bool {
		return entities[i].Proportion.LessThan(entities[j].Proportion)
	})
	ret := make([]*forms.SpendLimitThreshold, len(entities))
	for i, ent := range entities {
		ret[i] = thresholdFromEntity(ent)
	}
	return ret, nil
}

func (m *SpendLimitModel) GetTenantSpendingLimitThreshold(tenantID, spendLimitID, thresholdID uint) (*forms.SpendLimitThreshold, error) {
	var ent entity.SpendLimitAlertThreshold
	err := entity.NewSpendLimitAlertThresholdQuerySet(db.GetDB()).SpendingLimitIDEq(spendLimitID).TenantIDEq(tenantID).IDEq(thresholdID).One(&ent)
	if err != nil {
		return nil, err
	}
	return thresholdFromEntity(ent), nil
}

func (m *SpendLimitModel) CreateTenantSpendingLimitThreshold(tenantID, spendLimitID uint, threshold *forms.SpendLimitThreshold) (*forms.SpendLimitThreshold, error) {
	spendLimit, err := m.GetSpendLimitSetting(tenantID, spendLimitID)
	if err != nil {
		return nil, err
	}
	if spendLimit.BizType == entity.SpendLimitBizType_ServerlessCluster {
		err = errors.New("forbidden")
		return nil, billing_errors.NewInvalidInputError(err)
	}
	ent := entity.SpendLimitAlertThreshold{
		SpendingLimitID: spendLimitID,
		Proportion:      threshold.Proportion.Round(config.Get().GetInt32(config.BudgetThresholdPrecision)),
		BizType:         spendLimit.BizType,
		TenantID:        tenantID,
	}
	err = ent.Create(db.GetDB())
	if err != nil {
		return nil, err
	}
	return thresholdFromEntity(ent), nil
}

func (m *SpendLimitModel) UpdateTenantSpendingLimitThreshold(tenantID, spendLimitID, thresholdID uint, threshold *forms.SpendLimitThreshold) (*forms.SpendLimitThreshold, error) {
	spendLimit, err := m.GetSpendLimitSetting(tenantID, spendLimitID)
	if err != nil {
		return nil, err
	}
	if spendLimit.BizType == entity.SpendLimitBizType_ServerlessCluster {
		err = errors.New("forbidden")
		return nil, billing_errors.NewInvalidInputError(err)
	}
	var ent entity.SpendLimitAlertThreshold
	err = entity.NewSpendLimitAlertThresholdQuerySet(db.GetDB()).SpendingLimitIDEq(spendLimitID).TenantIDEq(tenantID).IDEq(thresholdID).One(&ent)
	if err != nil {
		return nil, err
	}
	ent.UpdatedAt = timeutils.GetNow()
	ent.Proportion = threshold.Proportion.Round(config.Get().GetInt32(config.BudgetThresholdPrecision))
	err = ent.Update(db.GetDB(), "updated_at", "proportion")
	if err != nil {
		return nil, err
	}
	return thresholdFromEntity(ent), nil
}

func (m *SpendLimitModel) DeleteTenantThreshold(tenantID, spendLimitID, thresholdID uint) error {
	spendLimit, err := m.GetSpendLimitSetting(tenantID, spendLimitID)
	if err != nil {
		return err
	}
	if spendLimit.BizType == entity.SpendLimitBizType_ServerlessCluster {
		err = errors.New("forbidden")
		return billing_errors.NewInvalidInputError(err)
	}
	log.Info("start to delete threshold", zap.Uint("tenantID", tenantID), zap.Uint("spendLimitID", spendLimitID), zap.Uint("thresholdID", thresholdID))
	err = db.GetDB().Transaction(func(tx *gorm.DB) error {
		cnt, err := entity.NewSpendLimitAlertThresholdQuerySet(tx).SpendingLimitIDEq(spendLimitID).TenantIDEq(tenantID).Count()
		// todo may add update lock
		if err != nil {
			log.Error("failed to retrieve tenant threshold", zap.Uint("tenantID", tenantID), zap.Uint("spendLimitID", spendLimitID))
			return err
		}
		if cnt == 1 {
			err := errors.New("can not delete last threshold")
			return billing_errors.NewInvalidInputError(err)
		}
		err = entity.NewSpendLimitAlertThresholdQuerySet(db.GetDB()).SpendingLimitIDEq(spendLimitID).TenantIDEq(tenantID).IDEq(thresholdID).Delete()
		if err != nil {
			log.Error("failed to delete tenant threshold", zap.Uint("tenantID", tenantID), zap.Uint("spendLimitID", spendLimitID))
			return err
		}
		return nil
	})
	if err != nil {
		log.Info("failed to delete threshold", zap.Error(err), zap.Uint("tenantID", tenantID), zap.Uint("spendLimitID", spendLimitID), zap.Uint("thresholdID", thresholdID))
		return err
	}
	log.Info("success to delete threshold", zap.Uint("tenantID", tenantID), zap.Uint("spendLimitID", spendLimitID), zap.Uint("thresholdID", thresholdID))
	return err
}

func (m *SpendLimitModel) CreateThresholds(tenantID, spendingLimitID uint, thresholds []*forms.SpendLimitThreshold) error {
	if len(thresholds) > config.Get().GetInt(config.MaxBudgetThresholds) || len(thresholds) < 1 {
		return billing_errors.NewInvalidInputError(errors.New("invalidated threshold length"))
	}
	settings, err := m.GetSpendLimitSetting(tenantID, spendingLimitID)
	if err != nil {
		return err
	}
	if settings.BizType == entity.SpendLimitBizType_ServerlessCluster {
		err = errors.New("forbidden")
		return billing_errors.NewInvalidInputError(err)
	}
	tMap := make(map[string]*forms.SpendLimitThreshold)
	for i := 0; i != len(thresholds); i++ {
		t := thresholds[i]
		key := t.Proportion.StringFixed(config.Get().GetInt32(config.BudgetThresholdPrecision))
		tMap[key] = t
	}
	return db.GetDB().Transaction(func(tx *gorm.DB) error {
		for _, t := range tMap {
			ent := entity.SpendLimitAlertThreshold{
				TenantID:        tenantID,
				BizType:         settings.BizType,
				Proportion:      t.Proportion.Round(config.Get().GetInt32(config.BudgetThresholdPrecision)),
				SpendingLimitID: spendingLimitID,
			}
			err := ent.Create(tx)
			if err != nil {
				return err
			}
			log.Info("success to create threshold", zap.Any("threshold", ent))
		}
		return nil
	})
}

func (m *SpendLimitModel) UpdateThresholds(tenantID, spendingLimitID uint, thresholds []*forms.SpendLimitThreshold) error {
	if len(thresholds) > config.Get().GetInt(config.MaxBudgetThresholds) || len(thresholds) < 1 {
		return billing_errors.NewInvalidInputError(errors.New("invalidated threshold length"))
	}
	settings, err := m.GetSpendLimitSetting(tenantID, spendingLimitID)
	if err != nil {
		return err
	}
	if settings.BizType == entity.SpendLimitBizType_ServerlessCluster {
		err = errors.New("forbidden")
		return billing_errors.NewInvalidInputError(err)
	}
	oriThresholds, err := m.ListTenantThresholds(spendingLimitID, settings.BizType)
	if err != nil {
		return err
	}
	type mark struct {
		Threshold    *forms.SpendLimitThreshold
		OriginalMark bool
		InputMark    bool
	}
	markMap := make(map[string]*mark)
	for i := 0; i != len(oriThresholds); i++ {
		t := oriThresholds[i]
		key := t.Proportion.StringFixed(config.Get().GetInt32(config.BudgetThresholdPrecision))
		markMap[key] = &mark{
			Threshold:    t,
			OriginalMark: true,
		}
	}
	for i := 0; i != len(thresholds); i++ {
		t := thresholds[i]
		key := t.Proportion.StringFixed(config.Get().GetInt32(config.BudgetThresholdPrecision))
		if _, ok := markMap[key]; !ok {
			markMap[key] = &mark{Threshold: t}
		}
		markMap[key].InputMark = true
	}
	var deletingIDList []uint
	var newEntities []entity.SpendLimitAlertThreshold
	for _, mm := range markMap {
		if mm.OriginalMark && mm.InputMark {
			continue
		}
		if mm.OriginalMark {
			deletingIDList = append(deletingIDList, mm.Threshold.ThresholdID)
		}
		if mm.InputMark {
			newEntities = append(newEntities, entity.SpendLimitAlertThreshold{
				TenantID:        tenantID,
				BizType:         settings.BizType,
				Proportion:      mm.Threshold.Proportion.Round(config.Get().GetInt32(config.BudgetThresholdPrecision)),
				SpendingLimitID: spendingLimitID,
			})
		}
	}
	if len(deletingIDList) == 0 && len(newEntities) == 0 {
		log.Info("no change for update thresholds", zap.Uint("tenantID", tenantID), zap.Uint("spendingLimitID", spendingLimitID))
		return nil
	}
	return db.GetDB().Transaction(func(tx *gorm.DB) error {
		if len(deletingIDList) != 0 {
			log.Info("try to delete threshold", zap.Uints("deletingIDList", deletingIDList), zap.Uint("tenantID", tenantID), zap.Uint("spendingLimitID", spendingLimitID))
			err := entity.NewSpendLimitAlertThresholdQuerySet(tx).IDIn(deletingIDList...).SpendingLimitIDEq(spendingLimitID).BizTypeEq(settings.BizType).Delete()
			if err != nil {
				return err
			}
		}
		log.Info("try to create thresholds", zap.Int("length", len(newEntities)), zap.Uint("tenantID", tenantID), zap.Uint("spendingLimitID", spendingLimitID))
		for _, ent := range newEntities {
			err = ent.Create(tx)
			if err != nil {
				return err
			}
			log.Info("success to create threshold", zap.Any("threshold", ent))
		}
		return nil
	})
}

func (m *SpendLimitModel) HandleSpendLimitEmail(event *eventbushandler.Event) (err error) {
	info := event.EventData.(*forms.SpendLimitThresholdAlert)
	emailID := fmt.Sprintf(constant.SpendLimitEmailKeyPattern, info.SpendLimitSettingID, event.EventID)
	err = nil
	switch info.BizType {
	case entity.SpendLimitBizType_ServerlessCluster:
		err := m.SendServerlessSpendLimitEmail(emailID, info)
		if err != nil {
			log.Error("failed to send serverless spend limit email", zap.Uint("tenantID", info.TenantID), zap.Uint("rangeID", info.RangeID), zap.Error(err))
		}
		return err
	case entity.SpendLimitBizType_Normal:
		err := m.SendNormalSpendingLimitEmail(emailID, info)
		if err != nil {
			log.Error("failed to send serverless spend limit email", zap.Uint("tenantID", info.TenantID), zap.Uint("rangeID", info.RangeID), zap.Error(err))
		}
		return err
	default:
		log.Warn("default spend limit send email is not configured")
	}

	return
}

func (m *SpendLimitModel) RegisterSpendLimitHandler() (err error) {
	return eventbushandler.Get().RegisterEventHandleFunc(
		constant.SpendLimitThresholdEventType,
		constant.SpendLimitGroup,
		forms.SpendLimitThresholdAlert{},
		m.HandleSpendLimitEmail,
	)
}
