package spend_limit

import (
	"encoding/json"
	"errors"
	"fmt"
	"slices"
	"strconv"
	"time"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/service/sqs"
	"github.com/pingcap/dbaas/billing2/handler/awshandler"

	"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/cachehandler"
	"github.com/pingcap/dbaas/billing2/handler/centralhandler"
	"github.com/pingcap/dbaas/billing2/handler/infrahandler"
	"github.com/pingcap/dbaas/billing2/model/billing/tiered_billing"
	"github.com/pingcap/dbaas/billing2/model/marketing"
	"github.com/pingcap/dbaas/billing2/model/marketing/unit_filter"
	"github.com/pingcap/dbaas/billing2/utils/labels"
	"github.com/pingcap/dbaas/billing2/utils/log"
	"github.com/pingcap/dbaas/billing2/utils/timeutils"
	"github.com/shopspring/decimal"
	"go.uber.org/zap"
	"gorm.io/gorm"
	"gorm.io/gorm/clause"
)

// Serverless Adaptor for Spend Limit
// Billing recieved the spend limit req from serverless,
// which contain the free credits billing should recharge cyclically.
// So if the free credits is positive, billing will create recharge plan for recharge cyclically.
// On updating, if the free credits changed, billing will update/create/delete the recharge plan.

var rechargePlanModel = &marketing.RechargeCreditsModel{}
var tieredBillingModel = &tiered_billing.TieredBillingModel{}

func (m *SpendLimitModel) genServerlessBizUniqueKey(settings *forms.SpendLimitSetting) (ret *string) {
	s := fmt.Sprintf("%s-%d-%d", entity.SpendLimitBizType_ServerlessCluster, settings.TenantID, settings.RangeID)
	return &s
}

func (m *SpendLimitModel) serverlessProcessOnListing(tx *gorm.DB, settingsList []*forms.SpendLimitSetting) (err error) {
	bizDataList := make([]*forms.ServerlessBizData, len(settingsList))
	var tenantPlanIDList []uint
	tenantPlanPosMap := make(map[uint]int)
	for i := 0; i != len(settingsList); i++ {
		settings := settingsList[i]
		bizDataList[i], err = m.UnmarshallServerlessBizData(settings.BizData)
		if err != nil {
			return err
		}
		bizDataList[i].FreeCredits = decimal.Zero           // default
		settingsList[i].Limit = bizDataList[i].InvoiceLimit // default
		if bizDataList[i].TenantRechargePlanID != 0 {
			tenantPlanIDList = append(tenantPlanIDList, bizDataList[i].TenantRechargePlanID)
			tenantPlanPosMap[bizDataList[i].TenantRechargePlanID] = i
		}
	}
	now := timeutils.GetNow()
	var records []entity.RechargeCreditsPlanRecord

	firstDayOfThisMonth := timeutils.GetMonthDate(now)
	if len(tenantPlanIDList) != 0 {
		firstDayOfLastMonth := firstDayOfThisMonth.AddDate(0, -1, 0)
		err = entity.NewRechargeCreditsPlanRecordQuerySet(tx.Preload(clause.Associations).Preload("CreditsPackage.BindedPackage")).TenantPlanIDIn(tenantPlanIDList...).
			CreatedAtGt(firstDayOfLastMonth).All(&records)
	}
	// Here will retrieve all records created at after first day of last month because of records for this month is in end of last month
	// Most of the records of the last month will be created at the end of the last two months, so there will not be a lot of last month records fetched here (only new)
	// next month recharge record may be retrieved here
	// following code will filter this will filter both types of records
	if err != nil {
		return err
	}
	firstDayOfNextMonth := firstDayOfThisMonth.AddDate(0, 1, 0)
	precision := config.Get().GetInt32(config.BillingCostsPrecisionDigits)
	for _, rec := range records {
		credits := rec.CreditsPackage
		if credits.StartDate.Before(firstDayOfThisMonth) || credits.ExpiredDate.After(firstDayOfNextMonth) {
			continue
		}
		pos := tenantPlanPosMap[rec.TenantPlanID]
		bizData := bizDataList[pos]
		if credits.BindedPackageID == nil {
			bizData.RemainingCredits = credits.TotalCredits
		} else {
			bizData.RemainingCredits = decimal.Max(credits.BindedPackage.RemainingCredits, decimal.Zero).Shift(precision).Ceil().Shift(-precision)
		}
		if rec.TenantPlan.ClosedAt == nil {
			bizData.FreeCredits = rec.MainPlan.Value
		} else {
			bizData.FreeCredits = decimal.Zero
		}
		bizData.ActiveTotalCredits = credits.TotalCredits

		settingsList[pos].BizData, err = m.MarshallServerlessBizData(bizData)
		if err != nil {
			return err
		}
		settingsList[pos].Limit = bizData.ActiveTotalCredits.Add(bizData.InvoiceLimit)
	}
	return nil
}

func (m *SpendLimitModel) serverlessProcessOnGetting(tx *gorm.DB, settings *forms.SpendLimitSetting) (err error) {
	return m.serverlessProcessOnListing(tx, []*forms.SpendLimitSetting{settings})
}

func (m *SpendLimitModel) serverlessCheck(settings *forms.SpendLimitSetting, isUpdate bool) (err error) {
	if settings.RangeType != entity.SpendLimitRangeType_Cluster {
		return billing_errors.NewInvalidInputError(errors.New(fmt.Sprintf("invalid range_type: %s", settings.RangeType)))
	}
	if settings.Interval != entity.SpendLimitInterval_Monthly {
		return billing_errors.NewInvalidInputError(errors.New(fmt.Sprintf("invalid interval: %s", settings.Interval)))
	}
	bizData, err := m.UnmarshallServerlessBizData(settings.BizData)
	if err != nil {
		return billing_errors.NewInvalidInputError(err)
	}
	if bizData.FreeCredits.LessThan(decimal.Zero) || bizData.InvoiceLimit.LessThan(decimal.Zero) {
		err = errors.New("BizData.FreeCredits or BizData.InvoiceLimit should not be negative")
		return billing_errors.NewInvalidInputError(err)
	}
	if !isUpdate {
		if bizData.RechargePlanID == 0 && bizData.FreeCredits.Add(bizData.InvoiceLimit).LessThanOrEqual(decimal.Zero) {
			// todo free credits check should be remove recently
			err = errors.New("BizData.FreeCredits Add BizData.InvoiceLimit should not be negative")
			return billing_errors.NewInvalidInputError(err)
		}
	}
	return
}

func (m *SpendLimitModel) MarshallServerlessBizData(bizData *forms.ServerlessBizData) (ret string, err error) {
	marshalledBizData, err := json.Marshal(bizData)
	if err != nil {
		return "", err
	}
	return string(marshalledBizData), err
}

func (m *SpendLimitModel) UnmarshallServerlessBizData(raw string) (ret *forms.ServerlessBizData, err error) {
	var bizData forms.ServerlessBizData
	err = json.Unmarshal([]byte(raw), &bizData) // this error checked before
	if err != nil {
		return
	}
	precision := config.Get().GetInt32(config.BillingCostsPrecisionDigits)
	bizData.FreeCredits = bizData.FreeCredits.Truncate(precision)
	bizData.InvoiceLimit = bizData.InvoiceLimit.Truncate(precision)
	return &bizData, nil
}
func (m *SpendLimitModel) serverlessSpendingLimitName(clusterName string) string {
	return fmt.Sprintf("Budget for %s", clusterName)
}

func (m *SpendLimitModel) serverlessProcessOnCreating(tx *gorm.DB, settings *forms.SpendLimitSetting) error {
	var err error
	if err = m.serverlessCheck(settings, false); err != nil {
		log.Error("failed to check spend limit with serverless_cluster", zap.Error(err), zap.Uint("clusterID", settings.RangeID), zap.Uint("tenantID", settings.TenantID))
		return err
	}
	bizData, _ := m.UnmarshallServerlessBizData(settings.BizData) // check error before
	if bizData.RechargePlanID != 0 || bizData.FreeCredits.GreaterThan(decimal.Zero) {
		err = m.createServerlessRechargePlan(tx, bizData, settings)
		if err != nil {
			return err
		}
	}
	err = m.createServerlessTieredBilling(tx, bizData, settings)
	if err != nil {
		return err
	}
	updatedBizData, err := json.Marshal(bizData)
	if err != nil {
		return err
	}
	settings.BizData = string(updatedBizData)
	return err
}

func (m *SpendLimitModel) tryRechargeFreeCredits(settings *forms.SpendLimitSetting) (err error) {
	bizData, err := m.UnmarshallServerlessBizData(settings.BizData)
	if err != nil {
		return err
	}
	if bizData.TenantRechargePlanID != 0 {
		recharged, err := rechargePlanModel.TryRecharge(bizData.TenantRechargePlanID)
		if err != nil {
			return err
		}
		if recharged {
			// for instance, create 03-31 23:59:00, we should recharge twice, first for current month and second for next month
			_, err = rechargePlanModel.TryRecharge(bizData.TenantRechargePlanID)
			if err != nil {
				return err
			}
		}
	}
	return
}

func (m *SpendLimitModel) serverlessProcessAfterCreating(settings *forms.SpendLimitSetting) (err error) {
	return m.tryRechargeFreeCredits(settings)
}

func (m *SpendLimitModel) serverlessProcessOnUpdating(tx *gorm.DB, settings *forms.SpendLimitSetting, origin *forms.SpendLimitSetting) (err error) {
	if err = m.serverlessCheck(settings, true); err != nil {
		log.Error("failed to check spend limit with serverless_cluster", zap.Error(err), zap.Uint("clusterID", settings.RangeID), zap.Uint("tenantID", settings.TenantID))
		return
	}
	bizData, _ := m.UnmarshallServerlessBizData(settings.BizData) // check error before
	originBizData, err := m.UnmarshallServerlessBizData(origin.BizData)
	if err != nil {
		return err
	}
	bizData.TieredBillingID = originBizData.TieredBillingID
	if !bizData.InvoiceLimit.Equal(originBizData.InvoiceLimit) {
		err = m.updateServerlessTieredBilling(tx, bizData, settings)
		if err != nil {
			return err
		}
	}
	bizData.TenantRechargePlanID = originBizData.TenantRechargePlanID
	bizData.RechargePlanID = originBizData.RechargePlanID
	UpdatedBizData, err := json.Marshal(bizData)
	if err != nil {
		return err
	}
	settings.BizData = string(UpdatedBizData)
	return
}

func (m *SpendLimitModel) serverlessProcessAfterUpdating(settings *forms.SpendLimitSetting) (err error) {
	return m.tryRechargeFreeCredits(settings)
}

func (m *SpendLimitModel) serverlessProcessOnDeleting(tx *gorm.DB, settings *forms.SpendLimitSetting) (err error) {
	bizData, err := m.UnmarshallServerlessBizData(settings.BizData) // check error before
	if err != nil {
		return err
	}
	err = m.disableServerlessRechargePlan(tx, bizData)
	if err != nil {
		return err
	}
	err = m.deleteServerlessTieredBilling(tx, bizData, settings)
	if err != nil {
		return err
	}
	return
}

func (m *SpendLimitModel) createServerlessRechargePlan(tx *gorm.DB, bizData *forms.ServerlessBizData, settings *forms.SpendLimitSetting) (err error) {
	unitFilter, err := (&unit_filter.Model{}).CreateUnitFilterWithTx(
		tx, &forms.UnitFilter{
			UnitFilterSlim: forms.UnitFilterSlim{
				Name:     fmt.Sprintf("Serverless Cluster %d", settings.RangeID),
				Internal: true,
			},
			General:        false,
			Labels:         map[string]string{constant.ClusterLabelKey: fmt.Sprint(settings.RangeID)},
			Region:         forms.UnitFilterALL,
			Provider:       forms.UnitFilterALL,
			Category:       forms.UnitFilterALL,
			UnitKeyPattern: ".*",
		},
	)
	if err != nil {
		log.Error("failed to create unit filter", zap.Error(err))
		return
	}
	if bizData.RechargePlanID == 0 {
		rechargePlan, err := rechargePlanModel.CreateRechargePlanWithTx(tx, &forms.RechargeCreditsPlan{
			Value: bizData.FreeCredits,
			Usage: entity.CreditsPackageUsage_Marketing,
			Cycle: entity.RechargePlanCycle_Month,
			Rule: forms.RechargeCreditsPlanRule{
				FirstCycleStartedAt:   forms.FirstCycleStartedAt_RightNow,
				FirstCycleFinishedAt:  forms.FirstCycleFinishedAt_EndOfNaturalCycle,
				FirstCreditsConvertor: forms.FirstCreditsConvertor_Full,
			},
			StartDate:   timeutils.GetNow(),
			ExpiredDate: time.Date(2099, 12, 31, 0, 0, 0, 0, time.UTC),
			Internal:    true,
		})
		if err != nil {
			log.Error("failed to create recharge plan", zap.Error(err))
			return err
		}
		bizData.RechargePlanID = rechargePlan.PlanID
	}
	tenantRechargePlan, err := rechargePlanModel.EnableTenantRechargePlanWithTx(tx, settings.TenantID, bizData.RechargePlanID, &unitFilter.UnitFilterSlim)
	if err != nil {
		log.Error("failed to enable tenant recharge plan")
		return err
	}
	bizData.TenantRechargePlanID = tenantRechargePlan.PlanID
	return
}

func (m *SpendLimitModel) disableServerlessRechargePlan(tx *gorm.DB, bizData *forms.ServerlessBizData) error {
	var err error
	if bizData.TenantRechargePlanID == 0 {
		return nil
	}
	err = rechargePlanModel.DisableTenantRechargePlanWithTx(tx, bizData.TenantRechargePlanID)
	if err != nil {
		return err
	}
	mainPlan, err := rechargePlanModel.GetRechargePlanWithTx(tx, bizData.RechargePlanID)
	if err != nil {
		return err
	}
	if mainPlan.Source == entity.RechargeCreditsPlanSource_System {
		err = rechargePlanModel.DisableRechargePlanWithTx(tx, bizData.RechargePlanID)
		if err != nil {
			return err
		}
	}
	return nil
}

func (m *SpendLimitModel) createServerlessTieredBilling(tx *gorm.DB, bizData *forms.ServerlessBizData, settings *forms.SpendLimitSetting) (err error) {
	tiered, err := tieredBillingModel.CreateTieredBillingWithTx(tx, settings.TenantID, &forms.TieredBilling{
		Interval: constant.TieredBillingInterval_Monthly, TargetValue: bizData.InvoiceLimit,
		Action: constant.TieredBillingAction_Free, Labels: map[string]string{constant.ClusterLabelKey: fmt.Sprint(settings.RangeID)}},
	)
	if err != nil {
		return err
	}
	bizData.TieredBillingID = tiered.TieredID
	return
}

func (m *SpendLimitModel) updateServerlessTieredBilling(tx *gorm.DB, bizData *forms.ServerlessBizData, settings *forms.SpendLimitSetting) (err error) {
	if bizData.TieredBillingID == 0 {
		err = m.createServerlessTieredBilling(tx, bizData, settings)
	} else {
		_, err = tieredBillingModel.UpdateTieredBillingWithTx(tx, settings.TenantID, bizData.TieredBillingID, &forms.TieredBilling{TargetValue: bizData.InvoiceLimit})
	}
	return
}

func (m *SpendLimitModel) deleteServerlessTieredBilling(tx *gorm.DB, bizData *forms.ServerlessBizData, settings *forms.SpendLimitSetting) (err error) {
	err = tieredBillingModel.DeleteTieredBillingWithTx(tx, settings.TenantID, bizData.TieredBillingID)
	return
}

func SendManusSpendLimitSQSNotification(projectID string, projectName string, clusterName string, info *forms.SpendLimitThresholdAlert) error {

	msgMap := map[string]interface{}{
		"alert_type":   "SPENDING_LIMIT_THRESHOLD_REACHED",
		"instance_id":  info.RangeID,
		"instance":     clusterName,
		"project":      projectName,
		"project_id":   projectID,
		"cost":         info.Costs,
		"threshold":    fmt.Sprint(info.Threshold.Mul(decimal.NewFromInt(100)).IntPart()),
		"sent_at":      timeutils.GetNow().String(),
		"limit":        info.Limit.Truncate(2).StringFixed(2),
		"threshold_id": info.SpendLimitSettingID,
	}
	log.Info("SendManusSpendLimitSQSNotification", zap.Any("msgMap", msgMap))
	msgBytes, _ := json.Marshal(msgMap)
	messageBody := string(msgBytes)

	targets := []forms.SQSTarget{
		{
			Name:       "PrimarySpendLimitQueue",
			Region:     config.Get().GetString(config.ManusAWSRegionForClusterSpendLimitNotification),
			Role:       config.Get().GetString(config.ManusAWSRoleForClusterSpendLimitNotification),
			QueueURL:   config.Get().GetString(config.ManusSQSQueueForClusterSpendLimitNotification),
			ExternalID: config.Get().GetString(config.ManusAWSExternalIDForClusterSpendLimitNotification),
		},
		{
			Name:       "SecondarySpendLimitQueue",
			Region:     config.Get().GetString(config.ManusAWSRegionForClusterSpendLimitNotification2),
			Role:       config.Get().GetString(config.ManusAWSRoleForClusterSpendLimitNotification2),
			QueueURL:   config.Get().GetString(config.ManusSQSQueueForClusterSpendLimitNotification2),
			ExternalID: config.Get().GetString(config.ManusAWSExternalIDForClusterSpendLimitNotification2),
		},
	}
	for _, t := range targets {
		if t.QueueURL == "" {
			log.Info("sqs no config ", zap.String("target", t.Name))
			continue
		}
		sqsCli, err := awshandler.GetCachedSQSClient(t.Region, t.Role, t.ExternalID)
		if err != nil {
			log.Error("failed to get sqs client", zap.String("target", t.Name), zap.Error(err))
			return err
		}
		ret, err := sqsCli.SendMessage(&sqs.SendMessageInput{
			MessageBody: aws.String(messageBody),
			QueueUrl:    aws.String(t.QueueURL),
		})

		if err != nil {
			log.Error("SpendLimit failed to send message", zap.String("target", t.Name), zap.Error(err), zap.Uint("thresholdID", info.SpendLimitSettingID))
		} else {
			log.Info("SpendLimit send message success", zap.String("target", t.Name), zap.Any("ret", ret), zap.Uint("thresholdID", info.SpendLimitSettingID))
		}
	}
	return nil
}
func (m *SpendLimitModel) SendServerlessSpendLimitEmail(emailID string, info *forms.SpendLimitThresholdAlert) (err error) {
	central := centralhandler.GetCentralHandler()
	infra := infrahandler.Get()
	clusterID := info.RangeID
	cluster, err := infra.GetCluster(info.TenantID, info.RangeID)
	if err != nil {
		return err
	}

	projectID := cluster.ProjectID
	projectName, err := labels.GetProjectName(projectID)
	if err != nil {
		return err
	}
	if slices.Contains(config.Get().GetStringSlice(config.ManusTenantsToNotifyOnClusterConsumption), fmt.Sprint(info.TenantID)) {
		log.Info("SendServerlessSpendLimitEmail", zap.Any("info", info))
		SendManusSpendLimitSQSNotification(strconv.FormatUint(uint64(projectID), 10), projectName, cluster.Name, info)
		// manus no need email by
		return nil
	}

	err = central.SendTenantOwnerEmailOnce(
		info.TenantID, emailID,
		constant.ServerlessSpendLimitEmailTemplateID, map[string]string{
			constant.ServerlessSpendLimitEmailThresholdKey:            fmt.Sprint(info.Threshold.Mul(decimal.NewFromInt(100)).IntPart()),
			constant.ServerlessSpendLimitEmailThresholdClusterNameKey: cluster.Name,
			constant.ServerlessSpendLimitEmailThresholdClusterIDKey:   fmt.Sprint(clusterID),
			constant.ServerlessSpendLimitEmailThresholdProjectNameKey: projectName,
			constant.ServerlessSpendLimitEmailThresholdProjectIDKey:   fmt.Sprint(projectID),
		},
	)
	if err != nil {
		return err
	}
	return
}

func (m *SpendLimitModel) ClosePaidServerlessClustersSpendingLimits(tenantID uint) error {
	var queryResult []gorm.Model
	err := db.GetDB().Raw("SELECT `id` FROM spend_limit_settings where `deleted_at` is null and `tenant_id` = ? and `biz_type`=? and json_unquote(json_extract(`biz_data`, '$.invoice_limit')) > 0 and `status` in ?",
		tenantID, entity.SpendLimitBizType_ServerlessCluster, []string{entity.SpendLimitStatus_Fit, entity.SpendLimitStatus_Excessive, entity.SpendLimitStatus_Recovered},
	).Scan(&queryResult).Error
	if err != nil {
		return err
	}
	if len(queryResult) == 0 {
		log.Info("no paid serverless clusters", zap.Uint("tenantID", tenantID))
		return nil
	}
	paidSpendingLimitIDs := make([]uint, len(queryResult))
	paidClusterSpendingLimits := make([]*forms.SpendLimitInfo, len(queryResult))
	for i := 0; i != len(queryResult); i++ {
		paidSpendingLimitIDs[i] = queryResult[i].ID
		paidClusterSpendingLimits[i], err = m.GetSpendLimitInfo(tenantID, queryResult[i].ID)
		if err != nil {
			return err
		}
	}
	err = db.GetDB().Transaction(func(tx *gorm.DB) error {
		err := entity.NewSpendLimitSettingQuerySet(tx).TenantIDEq(tenantID).BizTypeEq(entity.SpendLimitBizType_ServerlessCluster).IDIn(paidSpendingLimitIDs...).GetUpdater().SetStatus(entity.SpendLimitStatus_Closing).Update()
		if err != nil {
			return err
		}
		for _, sli := range paidClusterSpendingLimits {
			sli.Status = entity.SpendLimitStatus_Closing
			err = m.sendSpendLimitStatusChangeEvent(tx, sli)
			if err != nil {
				return err
			}
		}
		return nil
	})
	if err != nil {
		log.Error("failed to set spending limit to closing", zap.Error(err), zap.Uint("tenantID", tenantID), zap.Uints("paidSpendingLimitIDs", paidSpendingLimitIDs))
		return err
	}
	log.Info("success to set spending limit to closing", zap.Uint("tenantID", tenantID), zap.Uints("paidSpendingLimitIDs", paidSpendingLimitIDs))
	return nil
}

func (m *SpendLimitModel) RecoverClosingSpendingLimits(tenantID uint) error {
	var queryResult []struct{ ID uint }
	err := db.GetDB().Raw("SELECT `id` FROM spend_limit_settings where `deleted_at` is null and `tenant_id` = ? and `biz_type`=? and `status` = ? ", tenantID, entity.SpendLimitBizType_ServerlessCluster, entity.SpendLimitStatus_Closing).Scan(&queryResult).Error
	if err != nil {
		return err
	}
	if len(queryResult) == 0 {
		log.Info("no closing serverless clusters", zap.Uint("tenantID", tenantID))
		return nil
	}
	closingSpendLimitIDs := make([]uint, len(queryResult))
	closingSpendLimitList := make([]*forms.SpendLimitInfo, len(queryResult))
	for i := 0; i != len(queryResult); i++ {
		closingSpendLimitIDs[i] = queryResult[i].ID
		closingSpendLimitList[i], err = m.GetSpendLimitInfo(tenantID, queryResult[i].ID)
		if err != nil {
			return err
		}
	}
	err = db.GetDB().Transaction(func(tx *gorm.DB) error {
		err := entity.NewSpendLimitSettingQuerySet(tx).TenantIDEq(tenantID).BizTypeEq(entity.SpendLimitBizType_ServerlessCluster).IDIn(closingSpendLimitIDs...).GetUpdater().SetStatus(entity.SpendLimitStatus_Recovered).Update()
		if err != nil {
			return err
		}
		for _, sli := range closingSpendLimitList {
			sli.Status = entity.SpendLimitStatus_Recovered
			err = m.sendSpendLimitStatusChangeEvent(tx, sli)
			if err != nil {
				return err
			}
		}
		return nil
	})
	if err != nil {
		log.Error("failed to recover closing spending limit", zap.Uint("tenantID", tenantID), zap.Uints("closingSpendLimitIDs", closingSpendLimitIDs))
		return err
	}
	log.Info("success to recover closing spending limit", zap.Uint("tenantID", tenantID), zap.Uints("closingSpendLimitIDs", closingSpendLimitIDs))
	for _, sid := range closingSpendLimitIDs {
		err = m.CheckSpendLimit(tenantID, sid)
		if err != nil {
			log.Error("failed to check spending limit on recovered", zap.Uint("tenantID", tenantID), zap.Uint("spendLimitID", sid))
			continue
		}
	}
	return nil
}

func (m *SpendLimitModel) ServerlessSpendLimitSettingWithCost(setting *forms.SpendLimitSetting, containsFreeQuota bool) (ret *forms.SpendLimitInfo, err error) {
	bizData, err := m.UnmarshallServerlessBizData(setting.BizData)
	if err != nil {
		return
	}
	now := timeutils.GetNow().UTC()
	acc, err := (&tiered_billing.TieredBillingModel{}).GetAccumulator(bizData.TieredBillingID, now.Unix())
	if err != nil {
		return nil, err
	}
	ret = &forms.SpendLimitInfo{
		SpendLimitSetting: *setting,
		Costs:             acc.CumulateValue.Truncate(config.Get().GetInt32(config.BillingCostsPrecisionDigits)),
		StartDate:         timeutils.GetMonthDate(now),
	}
	if containsFreeQuota {
		ret.Costs = ret.Costs.Add(bizData.ActiveTotalCredits.Sub(decimal.Max(bizData.RemainingCredits, decimal.Zero))).Truncate(config.Get().GetInt32(config.BillingCostsPrecisionDigits))
	} else {
		ret.Limit = bizData.InvoiceLimit
	}
	return ret, nil
}

func (m *SpendLimitModel) IsFreeQuotaOnlySpendingLimit(spendingLimit *forms.SpendLimitSetting) (bool, error) {
	if spendingLimit.BizType == entity.SpendLimitBizType_ServerlessCluster {
		bizData, err := m.UnmarshallServerlessBizData(spendingLimit.BizData)
		if err != nil {
			return false, err
		}
		return bizData.InvoiceLimit.IsZero(), nil
	}
	return false, nil
}

func (m *SpendLimitModel) getServerlessClusterInfoWithCache(tenantID, clusterID uint) (*forms.Cluster, error) {
	funcName := "getServerlessClusterInfoWithCache"
	var cluster forms.Cluster
	cached := cachehandler.Get().Get(funcName, &cluster, tenantID, clusterID)
	if cached {
		return &cluster, nil
	}
	c, err := infrahandler.Get().GetCluster(tenantID, clusterID)
	if err != nil {
		return nil, err
	}
	cachehandler.Get().Set(funcName, c, tenantID, clusterID)
	return c, nil
}

func (m *SpendLimitModel) ServerlessSpendingLimitToBudget(spendingLimit *forms.SpendLimitSetting, withClusterMeta bool) (*forms.Budget, error) {
	info, err := m.ServerlessSpendLimitSettingWithCost(spendingLimit, false)
	if err != nil {
		return nil, err
	}
	ret := &forms.Budget{SpendLimitInfo: *info}
	ret.BizData = "{}"
	if !info.Limit.IsZero() {
		thresholds, err := m.ListTenantThresholds(info.SpendLimitSettingID, spendingLimit.BizType)
		if err != nil {
			return nil, err
		}
		ret.Thresholds = thresholds
		var clusterName = constant.UnknownClusterName
		if withClusterMeta {
			cluster, err := m.getServerlessClusterInfoWithCache(spendingLimit.TenantID, spendingLimit.RangeID)
			if err != nil {
				if !errors.Is(err, billing_errors.ObjectNotExistsError) {
					log.Error("failed to get cluster", zap.Error(err), zap.Uint("tenantID", spendingLimit.TenantID), zap.Uint("clusterID", spendingLimit.RangeID), zap.Uint("spendingLimitID", spendingLimit.SpendLimitSettingID))
					return nil, err
				}
				log.Warn("non-existed cluster", zap.Uint("tenantID", spendingLimit.TenantID), zap.Uint("clusterID", spendingLimit.RangeID), zap.Uint("spendingLimitID", spendingLimit.SpendLimitSettingID))
				return nil, err
			} else {
				clusterName = cluster.Name
				ret.Meta = map[string]string{
					constant.ServerlessBudgetMetaProjectIDKey:   fmt.Sprint(cluster.ProjectID),
					constant.ServerlessBudgetMetaClusterIDKey:   fmt.Sprint(cluster.ID),
					constant.ServerlessBudgetMetaClusterNameKey: clusterName,
				}
			}
			ret.Name = m.serverlessSpendingLimitName(clusterName)
		}
	}
	return ret, nil
}
