package marketing

import (
	"errors"
	"fmt"
	"strconv"
	"time"

	"github.com/pingcap/dbaas/billing2/config"
	"github.com/pingcap/dbaas/billing2/constant"
	"github.com/pingcap/dbaas/billing2/handler/centralhandler"
	"github.com/pingcap/dbaas/billing2/handler/eventbushandler"
	"github.com/pingcap/dbaas/billing2/handler/paymenthandler"
	"github.com/pingcap/dbaas/billing2/model/billingconfig"
	"github.com/pingcap/dbaas/billing2/utils/dateinterval"
	"github.com/pingcap/dbaas/billing2/utils/uncutils"
	"github.com/tidbcloud/account/idl/pbgen/proto/account"
	"gorm.io/gorm"

	"github.com/pingcap/dbaas/billing2/billing_errors"
	"github.com/pingcap/dbaas/billing2/db"
	"github.com/pingcap/dbaas/billing2/db/entity"
	"github.com/pingcap/dbaas/billing2/forms"
	"github.com/pingcap/dbaas/billing2/model/billing"
	"github.com/pingcap/dbaas/billing2/model/marketing/supportcalc"
	"github.com/pingcap/dbaas/billing2/model/payment/paymentsub"
	"github.com/pingcap/dbaas/billing2/utils/errorutils"
	"github.com/pingcap/dbaas/billing2/utils/jsm"
	"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/clause"
)

type SupportPlanModel struct {
}

const (
	SKUCategory = supportcalc.SKUCategory
	MonthDay    = supportcalc.MonthDay
)

const (
	SupportPlan_PercentagePriceBase = supportcalc.SupportPlanPercentageBase
)

const BindedSupportPlanUpdateEventType = "binded-support-plan-update"

// isPartnerManagedCustomer reports whether the given customer billing-
// config type represents a customer whose support-plan cost is paid
// by a partner (and therefore must be pushed to the partner tenant by
// SendReseller* and ignored by the partner's Pay transfer).
//
// Match list (must be in lockstep with the partner Pay handlers —
// see handler/paymenthandler/impl/):
//   - MarketplaceResellerManagedCustomer → AWSCPPOCustomerInvoiceTransferHandlerImpl
//   - ResaleCustomer                     → ResaleHandlerImpl (Pay inherited from MspHandlerImpl)
//
// All three partner modes are 1:N at the partner-tenant level (one
// partner has many customers). What differs is *where the support plan
// is bound* and *how the partner's books are partitioned*:
//
//   - MSPCustomer: support plan bound on the PARTNER (see
//     MspCustomerSyncSupportPlan); partner has 1 BC for all its
//     customers. The support fee is computed against the partner's
//     own user_unit_costs and never pushed by SendReseller* (which
//     is why MSPCustomer is intentionally NOT in this switch).
//   - MarketplaceResellerManagedCustomer (CPPO): support plan bound on
//     the CUSTOMER; partner has N BCs (one per customer, atomically
//     created via CreateMarketplaceResellerBillingConfig). The
//     per-customer partner BC mirrors the customer's usage, so
//     computing from customer BC vs from the partner's per-customer
//     BC yields the same result.
//   - ResaleCustomer: support plan bound on the CUSTOMER; partner
//     has 1 BC shared by all N customers. Computing from the
//     partner BC would aggregate all customers' costs (causing
//     cross-subsidization), which is why sendToBillNormalMetering /
//     sendToBillCommitmentMetering take an explicit source/customer
//     parameter rather than reading from the partner BC.
//
// MSPCustomer is intentionally NOT here. MSP partners bind support
// plans on their own tenant (see MspCustomerSyncSupportPlan); the
// cost lives on the partner's user_unit_costs. SendReseller* is
// never invoked for MSPCustomer billing configs because the worker
// only walks customer-bound plans.
func isPartnerManagedCustomer(t string) bool {
	switch t {
	case entity.BillingConfigType_MarketplaceResellerManagedCustomer,
		entity.BillingConfigType_ResaleCustomer:
		return true
	}
	return false
}

// getResellerTenantAndBillingConfig returns the reseller tenant ID
// and billing-config ID for the given partner-managed customer.
//
//   - MarketplaceResellerManagedCustomer: the link is via
//     billing_configs.ManagedTenantID = customerID, set when the
//     customer was created through CreateMarketplaceResellerBillingConfig.
//   - ResaleCustomer: the link is via payment_methods.PaymentCode
//     (set by ResaleHandlerImpl.Setup). billing_configs.ManagedTenantID
//     is NOT populated for Resale customers — Resale uses a different
//     partner-customer association mechanism. Look up the customer's
//     active Resale payment method, parse the partner tenantID out of
//     PaymentCode, then load the Resale partner's billing config.
//
// Add new cases here as new partner-managed customer types come
// online, and update isPartnerManagedCustomer in lockstep.
func (m *SupportPlanModel) getResellerTenantAndBillingConfig(
	customerTenantID uint,
	customerBillingConfig *forms.BillingConfig,
) (resellerTenantID uint, resellerBillingConfigID uint, err error) {
	switch customerBillingConfig.Type {
	case entity.BillingConfigType_MarketplaceResellerManagedCustomer:
		resellerBC, err := (&billingconfig.BillingConfigModel{}).GetBillingConfigByManagedTenantID(customerTenantID)
		if err != nil {
			return 0, 0, err
		}
		return resellerBC.TenantID, resellerBC.BillingConfigID, nil
	case entity.BillingConfigType_ResaleCustomer:
		payment, err := (&paymentsub.PaymentModel{}).GetActivePaymentMethod(customerTenantID, customerBillingConfig.BillingConfigID)
		if err != nil {
			return 0, 0, err
		}
		if payment.PaymentType != entity.PaymentType_Resale {
			return 0, 0, fmt.Errorf("expected Resale payment method for tenant %d, got %s", customerTenantID, payment.PaymentType)
		}
		partnerTenantID, err := strconv.ParseUint(payment.PaymentCode, 10, 64)
		if err != nil {
			return 0, 0, fmt.Errorf("parse partner tenantID from Resale payment code %q: %w", payment.PaymentCode, err)
		}
		var bc entity.BillingConfig
		if err := entity.NewBillingConfigQuerySet(db.GetDB()).
			TenantIDEq(uint(partnerTenantID)).
			TypeEq(entity.BillingConfigType_ResalePartner).
			One(&bc); err != nil {
			return 0, 0, err
		}
		return bc.TenantID, bc.ID, nil
	default:
		return 0, 0, fmt.Errorf("not a partner-managed customer type: %s", customerBillingConfig.Type)
	}
}

func DefaultBindedSupportPlan(tenantID uint) *forms.BindedSupportPlan {
	return &forms.BindedSupportPlan{
		TenantID: tenantID,
		Status:   entity.BindedSupportPlanStatus_ACTIVE,
		SupportPlan: forms.SupportPlan{
			FixedChargePrice: decimal.Zero,
			Type:             entity.SupportPlanType_FixedPrice,
			Public:           true,
			Level:            entity.SupportPlanLevel_Basic,
		},
		ActiveDate:  time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC),
		ExpiredDate: timeutils.GetTimeAddr(time.Date(2099, 12, 31, 23, 59, 59, 0, time.UTC)),
	}
}

func supportPlanFromEntity(entity *entity.SupportPlan) *forms.SupportPlan {
	return &forms.SupportPlan{
		SupportPlanID:          entity.ID,
		FixedChargePrice:       entity.FixedChargePrice,
		SupportPricePercentage: entity.SupportPricePercentage,
		Type:                   entity.Type,
		Public:                 entity.Public,
		Level:                  entity.Level,
		TieredPriceRules:       entity.TieredPriceRules,
		IsSupportFreeTrial:     entity.IsSupportFreeTrial,
	}
}

func (*SupportPlanModel) CreateSupportPlanUnits() error {
	unitModel := &billing.UnitModel{}
	_, err := unitModel.CreateAndUpdateUnit(&forms.UnitInfo{
		UnitKey:   constant.FixedPrice_SKU,
		Category:  SKUCategory,
		Price:     decimal.RequireFromString("0.001"),
		PriceType: entity.PriceType_Normal,
		Provider:  "PingCAP",
		SkuType:   entity.SkuType_Normal,
	})
	if err != nil {
		return err
	}

	_, err = unitModel.CreateAndUpdateUnit(&forms.UnitInfo{
		UnitKey:   constant.PercentagePrice_SKU,
		Category:  SKUCategory,
		Price:     decimal.RequireFromString("0.001"),
		PriceType: entity.PriceType_Normal,
		Provider:  "PingCAP",
		SkuType:   entity.SkuType_Normal,
	})
	if err != nil {
		return err
	}

	_, err = unitModel.CreateAndUpdateUnit(&forms.UnitInfo{
		UnitKey:   constant.TieredPrice_SKU,
		Category:  SKUCategory,
		Price:     decimal.RequireFromString("0.001"),
		PriceType: entity.PriceType_Normal,
		Provider:  "PingCAP",
		SkuType:   entity.SkuType_Normal,
	})
	if err != nil {
		return err
	}

	_, err = unitModel.CreateAndUpdateUnit(&forms.UnitInfo{
		UnitKey:   constant.CommintmentPrice_SKU,
		Category:  SKUCategory,
		Price:     decimal.RequireFromString("0.001"),
		PriceType: entity.PriceType_Normal,
		Provider:  "PingCAP",
		SkuType:   entity.SkuType_Normal,
	})
	if err != nil {
		return err
	}

	return nil
}

func (m *SupportPlanModel) CreateOrUpdatePublicSupportPlanWithLevel(level string, supportPlan *forms.SupportPlan) (*forms.SupportPlan, error) {
	if supportPlan == nil || !supportPlan.Public {
		return nil, billing_errors.InvalidInputError
	}
	supportPlanEntity := entity.SupportPlan{}
	err := entity.NewSupportPlanQuerySet(db.GetDB()).LevelEq(level).PublicEq(true).One(&supportPlanEntity)
	if err != nil {
		// create
		if errors.Is(err, billing_errors.ObjectNotExistsError) {
			return m.CreateSupportPlan(supportPlan)
		}
	} else {
		// update
		return m.UpdateSupportPlan(supportPlanEntity.ID, supportPlan)
	}
	return nil, err
}

func (m *SupportPlanModel) CreateSupportPlan(supportPlan *forms.SupportPlan) (*forms.SupportPlan, error) {
	err := m.validateSupportPlan(supportPlan)
	if err != nil {
		return nil, err
	}
	supportPlanEntity := entity.SupportPlan{
		FixedChargePrice:       supportPlan.FixedChargePrice,
		SupportPricePercentage: supportPlan.SupportPricePercentage,
		Type:                   supportPlan.Type,
		Public:                 supportPlan.Public,
		Level:                  supportPlan.Level,
		TieredPriceRules:       supportPlan.TieredPriceRules,
		IsSupportFreeTrial:     supportPlan.IsSupportFreeTrial,
	}
	err = supportPlanEntity.Create(db.GetDB())
	if err != nil {
		return nil, err
	}

	plan := supportPlanFromEntity(&supportPlanEntity)
	if err != nil {
		return nil, err
	}
	return plan, nil
}

func (m *SupportPlanModel) UpdateSupportPlan(supportPlanID uint, supportPlan *forms.SupportPlan) (*forms.SupportPlan, error) {
	err := m.validateSupportPlan(supportPlan)
	if err != nil {
		return nil, err
	}
	supportPlanEntity := entity.SupportPlan{}
	err = errorutils.WrapSQLError(entity.NewSupportPlanQuerySet(db.GetDB()).IDEq(supportPlanID).One(&supportPlanEntity), fmt.Sprintf("supportplan %d", supportPlanID))
	if err != nil {
		return nil, err
	}
	supportPlanEntity.FixedChargePrice = supportPlan.FixedChargePrice
	supportPlanEntity.SupportPricePercentage = supportPlan.SupportPricePercentage
	supportPlanEntity.Type = supportPlan.Type
	supportPlanEntity.Level = supportPlan.Level
	supportPlanEntity.IsSupportFreeTrial = supportPlan.IsSupportFreeTrial

	plan := supportPlanFromEntity(&supportPlanEntity)

	if err != nil {
		return nil, err
	}

	err = db.GetDB().Model(supportPlanEntity).Select("*").Omit("created_at, deleted_at").Updates(supportPlanEntity).Error
	if err != nil {
		return nil, billing_errors.NewSystemError(err)
	}

	return plan, nil
}

func (m *SupportPlanModel) DeleteSupportPlan(supportPlanID uint) error {
	err := entity.NewSupportPlanQuerySet(db.GetDB()).IDEq(supportPlanID).Delete()
	if err != nil {
		return err
	}
	return nil
}

func (*SupportPlanModel) GetSupportPlan(supportPlanID uint) (*forms.SupportPlan, error) {
	supportPlanEntity := entity.SupportPlan{}
	err := errorutils.WrapSQLError(entity.NewSupportPlanQuerySet(db.GetDB()).IDEq(supportPlanID).One(&supportPlanEntity), fmt.Sprintf("supportplan %d", supportPlanID))
	if err != nil {
		return nil, err
	}
	return supportPlanFromEntity(&supportPlanEntity), nil
}

func (*SupportPlanModel) ListSupportPlan() ([]forms.SupportPlan, error) {
	entities := []entity.SupportPlan{}
	err := entity.NewSupportPlanQuerySet(db.GetDB()).All(&entities)
	if err != nil {
		return nil, err
	}
	result := []forms.SupportPlan{}
	for _, v := range entities {
		result = append(result, *supportPlanFromEntity(&v))
	}
	return result, nil
}

func (*SupportPlanModel) ListPublicSupportPlan() ([]forms.SupportPlan, error) {
	entities := []entity.SupportPlan{}
	err := entity.NewSupportPlanQuerySet(db.GetDB()).PublicEq(true).All(&entities)
	if err != nil {
		return nil, err
	}
	result := []forms.SupportPlan{}
	for _, v := range entities {
		result = append(result, *supportPlanFromEntity(&v))
	}
	return result, nil
}

func bindedSupportPlanInfoFromEntity(bindedSupportPlanEntity *entity.BindedSupportPlan) *forms.BindedSupportPlan {
	return supportcalc.BindedSupportPlanFromEntity(bindedSupportPlanEntity)
}

func (m *SupportPlanModel) validateSupportPlan(supportPlan *forms.SupportPlan) error {
	switch supportPlan.Type {
	case entity.SupportPlanType_FixedPrice:
		if !supportPlan.SupportPricePercentage.IsZero() {
			return billing_errors.NewInvalidInputError(errors.New("the percentage price for a fixed price plan should be set to zero"))
		}
	case entity.SupportPlanType_PercentagePrice:
		// No check
	case entity.SupportPlanType_TieredPrice:
		if len(supportPlan.TieredPriceRules) == 0 {
			return billing_errors.NewInvalidInputError(errors.New("the tiered price must include at least one tier"))
		}
		if !supportPlan.TieredPriceRules[0].LowerBound.Equal(decimal.NewFromInt(0)) || supportPlan.TieredPriceRules[len(supportPlan.TieredPriceRules)-1].UpperBound != nil {
			return billing_errors.NewInvalidInputError(errors.New("the lowest tier must start at 0, and the highest tier must have no upper limit"))
		}
		// check the first n-1 tiers.
		for i := 0; i < len(supportPlan.TieredPriceRules)-1; i++ {
			// upperBound > lowerBound
			if supportPlan.TieredPriceRules[i].UpperBound.LessThanOrEqual(supportPlan.TieredPriceRules[i].LowerBound) {
				return billing_errors.NewInvalidInputError(errors.New("the upper bound must be greater than the lower bound for each tier"))
			}
			if !supportPlan.TieredPriceRules[i].UpperBound.Equal(supportPlan.TieredPriceRules[i+1].LowerBound) {
				return billing_errors.NewInvalidInputError(errors.New("the upper bound should be equal to the lower bound of the next tier"))
			}
		}
	default:
		return billing_errors.NewInvalidInputError(fmt.Errorf("not valid support plan price type :%s", supportPlan.Type))
	}
	return nil
}

func (*SupportPlanModel) GetBindedSupportPlan(bindedSupportPlanID uint) (*forms.BindedSupportPlan, error) {
	bindedSupportPlanEntity := &entity.BindedSupportPlan{}
	// It's bindded SupportPlan may be soft deleted, need to be get: Unscoped()
	err := errorutils.WrapSQLError(entity.NewBindedSupportPlanQuerySet(db.GetDB().Unscoped().Preload(clause.Associations)).IDEq(bindedSupportPlanID).One(bindedSupportPlanEntity), fmt.Sprintf("BindedSupportPlan %d", bindedSupportPlanID))
	if err != nil {
		return nil, err
	}

	return bindedSupportPlanInfoFromEntity(bindedSupportPlanEntity), nil
}

func (*SupportPlanModel) ListTenantsBindedSupportPlan(tenantID uint, statuses ...string) ([]*forms.BindedSupportPlan, error) {
	var entities []entity.BindedSupportPlan
	query := entity.NewBindedSupportPlanQuerySet(db.GetDB().Unscoped().Preload(clause.Associations)).TenantIDEq(tenantID)
	if len(statuses) != 0 {
		query = query.StatusIn(statuses...)
	}
	err := query.OrderAscByActiveDate().All(&entities)
	if err != nil {
		return nil, err
	}
	res := make([]*forms.BindedSupportPlan, len(entities))
	for i, bindedSupportPlanEntity := range entities {
		res[i] = bindedSupportPlanInfoFromEntity(&bindedSupportPlanEntity)
	}
	return res, nil
}

func (*SupportPlanModel) GetBindedSupportPlanTenantIDs() ([]uint, error) {
	var res []uint
	err := db.GetDB().Model(&entity.BindedSupportPlan{}).
		Where("status in ?", []string{entity.BindedSupportPlanStatus_ACTIVE, entity.BindedSupportPlanStatus_TOBEACTIVATED, entity.BindedSupportPlanStatus_Expired}).
		Distinct("tenant_id").Find(&res).Error
	if err != nil {
		return nil, err
	}
	return res, nil
}

func (*SupportPlanModel) ListAllActiveBindedSupportPlan(levels ...string) ([]forms.BindedSupportPlan, error) {
	var entities []entity.BindedSupportPlan
	err := entity.NewBindedSupportPlanQuerySet(db.GetDB().Unscoped().Preload(clause.Associations)).StatusEq(entity.BindedSupportPlanStatus_ACTIVE).All(&entities)
	if err != nil {
		return nil, err
	}
	var res []forms.BindedSupportPlan
	for _, bindedSupportPlanEntity := range entities {
		for _, level := range levels {
			if bindedSupportPlanEntity.SupportPlan.Level == level {
				res = append(res, *bindedSupportPlanInfoFromEntity(&bindedSupportPlanEntity))
				break
			}
		}
	}
	return res, nil
}

func (*SupportPlanModel) ListAllBindedSupportPlan() ([]forms.BindedSupportPlan, error) {
	var entities []entity.BindedSupportPlan
	err := entity.NewBindedSupportPlanQuerySet(db.GetDB().Unscoped().Preload(clause.Associations)).All(&entities)
	if err != nil {
		return nil, err
	}
	var res []forms.BindedSupportPlan
	for _, bindedSupportPlanEntity := range entities {
		res = append(res, *bindedSupportPlanInfoFromEntity(&bindedSupportPlanEntity))
	}
	return res, nil
}

func (m *SupportPlanModel) BindSupportPlanForPayableCustomer(tenantID uint, supportPlanID uint, interval forms.DateInterval) (*forms.BindedSupportPlan, error) {
	payable, err := (&paymentsub.PaymentModel{}).Payable(tenantID)
	if err != nil {
		return nil, err
	}
	if !payable {
		return nil, billing_errors.NewInvalidInputError(errors.New("non payable tenant can not buy support plan"))
	}
	return m.bindSupportPlan(tenantID, supportPlanID, interval)
}

func (m *SupportPlanModel) bindSupportPlan(tenantID uint, supportPlanID uint, interval forms.DateInterval) (*forms.BindedSupportPlan, error) {
	supportPlan, err := m.GetSupportPlan(supportPlanID)
	if err != nil {
		return nil, err
	}
	now := timeutils.GetDayDate(timeutils.GetNow())
	// Note: To prevent the start date of a newly binded Support Plan from being earlier than the effective date of the BillingConfig under the current Support Plan.
	if interval.StartDate.Compare(now) < 0 {
		return nil, billing_errors.NewInvalidInputError(errors.New("the start date for the newly binded Support Plan cannot be earlier than the current day"))
	}
	bindedSupportPlanEntity := entity.BindedSupportPlan{
		TenantID:               tenantID,
		SupportPlanID:          supportPlanID,
		ActiveDate:             *interval.StartDate,
		ExpiredDate:            interval.EndDate,
		IsEligibleForFreeTrial: false,
	}
	// set 'IsEligibleForFreeTrial': first time to subscribe SupportPlan and IsSupportFreeTrial
	if supportPlan.IsSupportFreeTrial {
		total, err := entity.NewBindedSupportPlanQuerySet(db.GetDB()).TenantIDEq(tenantID).Count()
		if err != nil {
			return nil, err
		}
		if total == 0 {
			bindedSupportPlanEntity.IsEligibleForFreeTrial = true
		}
	}
	// check time conflict and create record
	err = db.GetDB().Transaction(func(tx *gorm.DB) error {
		var currentPlans []entity.BindedSupportPlan
		err = entity.NewBindedSupportPlanQuerySet(db.GetDB()).TenantIDEq(tenantID).
			StatusIn(entity.BindedSupportPlanStatus_ACTIVE, entity.BindedSupportPlanStatus_TOBEACTIVATED).All(&currentPlans)
		if err != nil {
			return err
		}
		for _, plan := range currentPlans {
			if dateinterval.IsDisjoint(interval, forms.DateInterval{StartDate: &plan.ActiveDate, EndDate: plan.ExpiredDate}) {
				log.Info("conflicted time range detected", zap.Uint("conflictedSupportPlanID", plan.ID),
					zap.Time("planActiveDate", plan.ActiveDate), zap.Timep("planExpiredDate", plan.ExpiredDate),
					zap.Timep("targetStartDate", interval.StartDate), zap.Timep("targetEndDate", interval.EndDate))
				if plan.Status == entity.BindedSupportPlanStatus_ACTIVE {
					log.Info("update expiredDate for active bindedSupportPlan when time conflicted", zap.Uint("conflictedSupportPlanID", plan.ID), zap.Time("expiredDate", *interval.StartDate))
					// update 'active' BindedSupportPlan effective time (activeDate - 1s), and modify its status if needed
					settedExpiredDate := interval.StartDate.Add(-time.Second)
					if settedExpiredDate.Compare(now) < 0 {
						err = entity.NewBindedSupportPlanQuerySet(db.GetDB()).IDEq(plan.ID).GetUpdater().SetExpiredDate(&settedExpiredDate).SetStatus(entity.BindedSupportPlanStatus_Expired).Update()
					} else {
						err = entity.NewBindedSupportPlanQuerySet(db.GetDB()).IDEq(plan.ID).GetUpdater().SetExpiredDate(&settedExpiredDate).Update()
					}
				} else {
					log.Info("cancel to_be_active bindedSupportPlan when time conflicted", zap.Uint("conflictedSupportPlanID", plan.ID))
					// cancel 'to_be_active' BindedSupportPlan directly
					err = entity.NewBindedSupportPlanQuerySet(db.GetDB()).IDEq(plan.ID).GetUpdater().SetStatus(entity.BindedSupportPlanStatus_CANCELED).SetClosedDate(timeutils.GetNowAddr()).Update()
				}
				if err != nil {
					return err
				}
			}
		}
		if dateinterval.IsWithin(now, interval) {
			log.Info("need to active this support plan", zap.Timep("targetStartDate", interval.StartDate), zap.Timep("targetEndDate", interval.EndDate))
			bindedSupportPlanEntity.Status = entity.BindedSupportPlanStatus_ACTIVE
		} else {
			log.Info("The start date is neither past nor expired yet", zap.Uint("supportPlanID", supportPlanID), zap.Uint("tenantID", tenantID), zap.Timep("targetStartDate", interval.StartDate), zap.Timep("targetEndDate", interval.EndDate))
			bindedSupportPlanEntity.Status = entity.BindedSupportPlanStatus_TOBEACTIVATED
		}
		err = bindedSupportPlanEntity.Create(tx)
		return err
	})
	if err != nil {
		return nil, err
	}
	support, err := m.GetBindedSupportPlan(bindedSupportPlanEntity.ID)
	if err != nil {
		return nil, err
	}
	if bindedSupportPlanEntity.Status == entity.BindedSupportPlanStatus_ACTIVE {
		jsm.SendJSMSupport(support)
		// sync to eventbus
		m.SyncCurrentSupportToEventBus(support)
		err = m.MspSupportPlanChangedSync(tenantID)
		if err != nil {
			log.Error("failed sync msp support plan", zap.Error(err))
		}
		// bind support plan and be active immediately, send in-app notification
		m.sendBindedSupportPlanUpdateNotification(support)
	}
	return support, nil
}

/*
Trigger Conditions:
1. User manually cancels via API.
2. BillingConfig switch: automatic trigger, The status of billingConfig is 'Closing'
3. Tenant Close

When it's called:
The status of billingConfig can only be in one of two states: 'Active' or 'Closing'(When BillingConfig change).
The stauts of bindedSupportPlanID can be 'active', 'to_be_activated', 'expired'(When BillingConfig changes, may be this status)
*/
func (m *SupportPlanModel) CancelBindedSupportPlan(bindedSupportPlanID uint) (*forms.BindedSupportPlan, error) {
	bindedSupportPlan, err := m.GetBindedSupportPlan(bindedSupportPlanID)
	if err != nil {
		return nil, err
	}

	if bindedSupportPlan.Status == entity.BindedSupportPlanStatus_CANCELED {
		return nil, billing_errors.NewInvalidInputError(errors.New("the binded supportplan has be canceled"))
	}

	now := timeutils.GetNow()

	err = entity.NewBindedSupportPlanQuerySet(db.GetDB()).IDEq(bindedSupportPlanID).GetUpdater().SetStatus(entity.BindedSupportPlanStatus_CANCELED).SetClosedDate(&now).Update()
	if err != nil {
		return nil, err
	}
	// before canceled, if it's status is 'active' or 'expired', need to send metering data
	if bindedSupportPlan.Status == entity.BindedSupportPlanStatus_ACTIVE || bindedSupportPlan.Status == entity.BindedSupportPlanStatus_Expired {
		defer func() {
			jsm.SendJSMSupport(DefaultBindedSupportPlan(bindedSupportPlan.TenantID))
			// trigger to sync current support info to eventbus
			m.SyncCurrentSupportToEventBus(DefaultBindedSupportPlan(bindedSupportPlan.TenantID))
			// active/expired -> closed: send in-app notification
			m.sendBindedSupportPlanUpdateNotification(DefaultBindedSupportPlan(bindedSupportPlan.TenantID))
		}()
		if bindedSupportPlan.Type == entity.SupportPlanType_FixedPrice {
			// deal with fixed_price
			_, err = m.SendNormalMetering(bindedSupportPlanID, now.AddDate(0, 0, -1))
			if err != nil {
				return nil, err
			}
			_, err = m.SendResellerNormalMeteringIfNeed(bindedSupportPlanID, now.AddDate(0, 0, -1))
			if err != nil {
				return nil, err
			}
		} else {
			// deal with DirectCustomer
			_, err = m.SendNormalMetering(bindedSupportPlanID, now.AddDate(0, 0, -2))
			if err != nil {
				return nil, err
			}
			_, err = m.SendNormalMetering(bindedSupportPlanID, now.AddDate(0, 0, -1))
			if err != nil {
				return nil, err
			}
			_, _, err = m.SendCommitmentMetering(bindedSupportPlanID, timeutils.GetLastMonth(now))
			if err != nil {
				return nil, err
			}
			_, _, err = m.SendCommitmentMetering(bindedSupportPlanID, now)
			if err != nil {
				return nil, err
			}

			// deal with Marketplace Reseller Managed Customer
			_, err = m.SendResellerNormalMeteringIfNeed(bindedSupportPlanID, now.AddDate(0, 0, -2))
			if err != nil {
				return nil, err
			}
			_, err = m.SendResellerNormalMeteringIfNeed(bindedSupportPlanID, now.AddDate(0, 0, -1))
			if err != nil {
				return nil, err
			}
			_, _, err = m.SendResellerCommitmentMeteringIfNeed(bindedSupportPlanID, timeutils.GetLastMonth(now))
			if err != nil {
				return nil, err
			}
			_, _, err = m.SendResellerCommitmentMeteringIfNeed(bindedSupportPlanID, now)
			if err != nil {
				return nil, err
			}
		}
	}

	if bindedSupportPlan.BindedPackageID != nil {
		err = (&billing.BindedPackageModel{}).UnbindPackage(*bindedSupportPlan.BindedPackageID)
		if err != nil {
			return nil, err
		}
		err = (&billing.BindedPackageModel{}).InactivePackage(*bindedSupportPlan.BindedPackageID)
		if err != nil {
			return nil, err
		}
	}
	bindedSupportPlan.Status = entity.BindedSupportPlanStatus_CANCELED
	bindedSupportPlan.ClosedDate = &now
	return bindedSupportPlan, nil
}

func (m *SupportPlanModel) GetTenantActiveBindedPlan(tenantID uint) (*forms.BindedSupportPlan, error) {
	bindedSupportPlanEntity := &entity.BindedSupportPlan{}
	err := errorutils.WrapSQLError(entity.NewBindedSupportPlanQuerySet(db.GetDB().Unscoped().Preload(clause.Associations)).
		TenantIDEq(tenantID).StatusEq(entity.BindedSupportPlanStatus_ACTIVE).One(bindedSupportPlanEntity), fmt.Sprintf("Active Tenant %d BindedSupportPlan", tenantID))
	if err != nil {
		return nil, err
	}
	bindedSupportPlan := bindedSupportPlanInfoFromEntity(bindedSupportPlanEntity)
	// query status free trial
	isInFreeTrial, restDays, err := m.IsInSupportPlanFreeTrialForTenant(tenantID, bindedSupportPlan)
	if err != nil {
		return nil, err
	}
	bindedSupportPlan.IsInFreeTrial = isInFreeTrial
	bindedSupportPlan.RestDaysOfFreeTrial = uint(restDays)
	return bindedSupportPlan, nil
}

func (m *SupportPlanModel) GetMeasureDay(bindedInfoStartTime time.Time, bindedInfoExpiredTime, bindedInfoClosedTime *time.Time, measureDateStartTime time.Time, measureDateEndTime time.Time) (time.Time, time.Time, int) {
	return supportcalc.GetMeasureDay(bindedInfoStartTime, bindedInfoExpiredTime, bindedInfoClosedTime, measureDateStartTime, measureDateEndTime)
}

/*
Note:
1. If a billingConfig expires, the BindedSupportPlan will be canceled, and any related metering will be reissued
2. The scheduled task processes only active and expired BindedSupportPlans (expired ones will be canceled next month).
3. Therefore, any bindedSupportPlan processed by scheduled task is definitely not in a canceled state, and the corresponding billingConfig is definitely active.
*/
func (m *SupportPlanModel) SendNormalMetering(bindedSupportPlanID uint, date time.Time) (int, error) {
	measureDate := timeutils.GetDayDate(date)
	bindedSupportPlanInfo, err := m.GetBindedSupportPlan(bindedSupportPlanID)
	if err != nil {
		return 0, err
	}
	tenantID := bindedSupportPlanInfo.TenantID
	// Check whether it is within the effective time period of BindedSupportPlan
	_, _, measureDay := m.GetMeasureDay(bindedSupportPlanInfo.ActiveDate, bindedSupportPlanInfo.ExpiredDate, bindedSupportPlanInfo.ClosedDate, measureDate, measureDate.Add(time.Hour*24))
	if measureDay == 0 {
		log.Info("measureDay == 0, skip send metering data", zap.Uint("TenantID", tenantID), zap.Time("measureDate", measureDate), zap.Uint("bindedSupportPlanID", bindedSupportPlanID))
		return 0, nil
	}
	// Check whether corresponding billingConfig is still effective. When billingConfig is set to 'closing', it will be 'closed' after 4 days, still effective when sending normal metering
	billingConfig, err := (&billingconfig.BillingConfigModel{}).GetUseBillingConfigByTimeStampWithCache(tenantID, measureDate.Unix())
	if err != nil {
		log.Info("get use billingConfig error", zap.Uint("TenantID", tenantID), zap.Time("measureDate", measureDate), zap.Uint("bindedSupportPlanID", bindedSupportPlanID), zap.Error(err))
		return 0, err
	}

	// Check whether it is in free trial for the measureDate
	isInFreeTrial, _, err := m.isInSupportPlanFreeTrialForMeasureDate(measureDate, bindedSupportPlanInfo, billingConfig)
	if err != nil {
		return 0, err
	}
	if isInFreeTrial {
		log.Info("In the Support Plan's free trial, no metering data need to send", zap.Uint("TenantID", tenantID), zap.Time("measureDate", measureDate), zap.Uint("bindedSupportPlanID", bindedSupportPlanID))
		return 0, nil
	}
	costID := fmt.Sprintf("support-plan-normal-%d-%d", tenantID, measureDate.Unix())
	return m.sendToBillNormalMetering(costID, measureDate, tenantID, billingConfig.BillingConfigID, tenantID, billingConfig.BillingConfigID, bindedSupportPlanInfo, false)
}

func (m *SupportPlanModel) SendResellerNormalMeteringIfNeed(bindedSupportPlanID uint, date time.Time) (int, error) {
	measureDate := timeutils.GetDayDate(date)
	bindedSupportPlanInfo, err := m.GetBindedSupportPlan(bindedSupportPlanID)
	if err != nil {
		return 0, err
	}
	_, _, measureDay := m.GetMeasureDay(bindedSupportPlanInfo.ActiveDate, bindedSupportPlanInfo.ExpiredDate, bindedSupportPlanInfo.ClosedDate, measureDate, measureDate.Add(time.Hour*24))
	if measureDay == 0 {
		log.Info("measureDay == 0, skip send commitment metering to reseller", zap.Uint("tenantID", bindedSupportPlanInfo.TenantID), zap.Time("measureDate", measureDate), zap.Uint("customerBindedSupportPlanID", bindedSupportPlanInfo.BindedSupportPlanID))
		return 0, nil
	}
	tenantBillingConfig, err := (&billingconfig.BillingConfigModel{}).GetUseBillingConfigByTimeStampWithCache(bindedSupportPlanInfo.TenantID, measureDate.Unix())
	if err != nil {
		return 0, err
	}
	// Re-verify if the current tenant's billing config represents a
	// partner-managed customer (Resale customer, AWS CPPO customer).
	// See isPartnerManagedCustomer for the rationale.
	if !isPartnerManagedCustomer(tenantBillingConfig.Type) {
		log.Info("tenant have not be managed by reseller", zap.Uint("tenantID", bindedSupportPlanInfo.TenantID), zap.Time("measureDate", measureDate), zap.Uint("customerBindedSupportPlanID", bindedSupportPlanInfo.BindedSupportPlanID), zap.Uint("customerBillingConfigID", tenantBillingConfig.BillingConfigID), zap.String("billingConfigType", tenantBillingConfig.Type))
		return 0, nil
	}
	resellerTenantID, resellerBillingConfigID, err := m.getResellerTenantAndBillingConfig(bindedSupportPlanInfo.TenantID, tenantBillingConfig)
	if err != nil {
		return 0, err
	}
	costID := fmt.Sprintf("support-plan-normal-transfer-from-%d-to-%d-%d", bindedSupportPlanInfo.TenantID, resellerTenantID, measureDate.Unix())
	customerTenantID := bindedSupportPlanInfo.TenantID
	customerBillingConfigID := tenantBillingConfig.BillingConfigID
	log.Info("get the associated reseller info", zap.Uint("customerTenantID", customerTenantID), zap.Time("measureDate", measureDate), zap.Uint("customerBillingConfigID", customerBillingConfigID), zap.Uint("resellerTenantID", resellerTenantID), zap.Uint("resellerBillingConfigID", resellerBillingConfigID))

	// source = customer (where the actual usage lives), target = reseller
	// (whose books the support fee lands on). See sendToBillNormalMetering
	// for the per-customer base rationale.
	return m.sendToBillNormalMetering(costID, measureDate, customerTenantID, customerBillingConfigID, resellerTenantID, resellerBillingConfigID, bindedSupportPlanInfo, true)
}

// SupportPlanSkuUnits are the Unit keys written by SendNormalMetering /
// SendResellerNormalMeteringIfNeed / SendCommitmentMetering. They must be
// excluded from the customer's usage base so the support fee does not
// observe its own previous output sitting in user_unit_costs and
// recompute against an inflated base — every subsequent day would grow
// geometrically. See sendToBillNormalMetering for context.
//
// Exported so the AR table estimate (model/payment/reconciliation) can
// reuse the same exclusion list rather than maintaining a parallel
// copy that will silently drift.
var SupportPlanSkuUnits = []string{
	constant.FixedPrice_SKU,
	constant.PercentagePrice_SKU,
	constant.TieredPrice_SKU,
	constant.CommintmentPrice_SKU,
}

// getCustomerUsageBase sums customer CostPrice for a single day, scoped
// to the given billing config, and excluding the support plan SKUs
// themselves. This is the raw usage base that the support fee scales
// against.
//
// The support SKUs must be excluded or SendNormal's own row, already
// persisted to customer user_unit_costs from a prior call, would be
// read back and inflate the next day's base. For a single BindSupportPlan
// invocation per day this would still propagate via subsequent days:
// day N reads its own day-N-1 row as part of the "base" and the fee
// drifts upward.
func (m *SupportPlanModel) getCustomerUsageBase(customerTenantID uint, customerBillingConfigID uint, measureDate time.Time) (decimal.Decimal, error) {
	var rows []entity.UserUnitCost
	err := entity.NewUserUnitCostQuerySet(db.GetDB()).
		TenantIDEq(customerTenantID).
		BindedBillingConfigIDEq(customerBillingConfigID).
		CostTimeStampGte(measureDate.Unix()).
		CostTimeStampLt(measureDate.Add(time.Hour * 24).Unix()).
		StatusEq(entity.UserUnitCostStatus_DONE).
		UnitNotIn(SupportPlanSkuUnits...).
		All(&rows)
	if err != nil {
		return decimal.Zero, err
	}
	var sum decimal.Decimal
	for _, row := range rows {
		sum = sum.Add(row.CostPrice)
	}
	return sum, nil
}

// getCustomerSupportFeeSpend sums customer CostPrice across the
// support fee SKUs (Fixed / Percentage / Tiered) for the given time
// range, scoped to the given billing config. CommintmentPrice_SKU is
// excluded so the commitment row this function is about to write is
// not read back into the gap calculation. This is the spend base
// used by sendToBillCommitmentMetering.
//
// Equivalent to the previous
// CategoryUnitsCosts[SKUCategory="Support Plan"].CostPrice from
// CalculateCost, minus the CommintmentPrice_SKU contribution.
func (m *SupportPlanModel) getCustomerSupportFeeSpend(customerTenantID uint, customerBillingConfigID uint, startTime time.Time, endTime time.Time) (decimal.Decimal, error) {
	var rows []entity.UserUnitCost
	err := entity.NewUserUnitCostQuerySet(db.GetDB()).
		TenantIDEq(customerTenantID).
		BindedBillingConfigIDEq(customerBillingConfigID).
		CostTimeStampGte(startTime.Unix()).
		CostTimeStampLt(endTime.Unix()).
		StatusEq(entity.UserUnitCostStatus_DONE).
		UnitIn(constant.FixedPrice_SKU, constant.PercentagePrice_SKU, constant.TieredPrice_SKU).
		All(&rows)
	if err != nil {
		return decimal.Zero, err
	}
	var sum decimal.Decimal
	for _, row := range rows {
		sum = sum.Add(row.CostPrice)
	}
	return sum, nil
}

func (m *SupportPlanModel) sendToBillNormalMetering(costID string, measureDate time.Time, sourceTenantID uint, sourceBillingConfigID uint, targetTenantID uint, targetBillingConfigID uint, bindedSupportPlanInfo *forms.BindedSupportPlan, isMarketplaceReseller bool) (int, error) {
	if bindedSupportPlanInfo == nil {
		return 0, billing_errors.InvalidInputError
	}
	cnt, err := entity.NewSupportPlanMeteringRecordsQuerySet(db.GetDB()).CostIDEq(costID).Count()
	if err != nil {
		return 0, err
	}
	if cnt > 0 {
		return 0, nil
	}
	baseCostPrice := decimal.Zero
	previousTieredUsage := decimal.Zero
	if bindedSupportPlanInfo.Type != entity.SupportPlanType_FixedPrice {
		// Read the cost base from the SOURCE tenant's billing config (the
		// customer), not the target (the reseller). For Resale 1:N
		// partners, every customer's support fee must be computed off its
		// own usage, not the partner's aggregate.
		base, err := m.getCustomerUsageBase(sourceTenantID, sourceBillingConfigID, measureDate)
		if err != nil {
			return 0, err
		}
		baseCostPrice = base
		if bindedSupportPlanInfo.Type == entity.SupportPlanType_TieredPrice {
			// Previous-day cumulative-usage record is read from the
			// TARGET tenant; it is the same tenant that receives the
			// SecondaryMeasure write below.
			var meteringOfLastDay entity.SupportPlanMeteringRecords
			err = entity.NewSupportPlanMeteringRecordsQuerySet(db.GetDB()).TenantIDEq(targetTenantID).BindedSupportPlanIDEq(bindedSupportPlanInfo.BindedSupportPlanID).CostTimeStampEq(measureDate.AddDate(0, 0, -1)).One(&meteringOfLastDay)
			if err != nil && !errors.Is(err, billing_errors.ObjectNotExistsError) {
				return 0, err
			}
			// Monthly reset: cumulative usage only carries over within
			// the same calendar month. On the first day of a new month,
			// prevUsage is zero, so the tiered pricing restarts from the
			// first tier instead of accumulating across months.
			yesterday := measureDate.AddDate(0, 0, -1)
			if yesterday.Year() == measureDate.Year() && yesterday.Month() == measureDate.Month() {
				previousTieredUsage = meteringOfLastDay.Usage
			}
		}
	}
	// SecondaryMeasure: without get projectName by projectID and clusterName by clusterID for labels, No impact on current billing
	measureModel := &billing.MeasureModel{}
	labels := map[string]string{}
	if isMarketplaceReseller {
		labels[constant.MarketplaceResellerManagedCustomerIDLabel] = strconv.FormatUint(uint64(bindedSupportPlanInfo.TenantID), 10)
	}
	metering, err := supportcalc.CalculateNormalMetering(supportcalc.NormalMeteringInput{
		CostID:              costID,
		MeasureDate:         measureDate,
		TenantID:            targetTenantID,
		BillingConfigID:     targetBillingConfigID,
		BindedSupportPlan:   bindedSupportPlanInfo,
		BaseCostPrice:       baseCostPrice,
		PreviousTieredUsage: previousTieredUsage,
		Labels:              labels,
	})
	if err != nil {
		return 0, err
	}
	_, err = measureModel.SecondaryMeasure(targetTenantID, targetBillingConfigID, metering.UnitCost)
	if err != nil {
		log.Error("Send to bill normal metering for Support Plan ,err", zap.Uint("sourceTenantID", sourceTenantID), zap.Uint("targetTenantID", targetTenantID), zap.Time("measureDate", measureDate), zap.Uint("bindedSupportPlanID", bindedSupportPlanInfo.BindedSupportPlanID), zap.Uint("billingConfigID", targetBillingConfigID), zap.Bool("isMarketplaceReseller", isMarketplaceReseller), zap.Int("Cost", metering.UnitCost.Cost), zap.Error(err))
		return 0, err
	}
	log.Info("Send to bill normal metering for Support Plan", zap.Uint("sourceTenantID", sourceTenantID), zap.Uint("targetTenantID", targetTenantID), zap.Time("measureDate", measureDate), zap.Uint("bindedSupportPlanID", bindedSupportPlanInfo.BindedSupportPlanID), zap.Uint("billingConfigID", targetBillingConfigID), zap.Bool("isMarketplaceReseller", isMarketplaceReseller), zap.Int("Cost", metering.UnitCost.Cost), zap.Any("Usage", metering.Usage))
	recordEntity := &entity.SupportPlanMeteringRecords{
		CostID:              costID,
		CostTimeStamp:       measureDate,
		TenantID:            targetTenantID,
		BindedSupportPlanID: bindedSupportPlanInfo.BindedSupportPlanID,
		Unit:                metering.UnitCost.Unit,
		Cost:                metering.UnitCost.Cost,
		Usage:               metering.Usage,
	}
	err = recordEntity.Create(db.GetDB())
	if err != nil {
		return 0, err
	}
	return metering.UnitCost.Cost, nil
}

// TieredPricing applies a tiered support-price rule set to a usage
// window and returns the integer scaled cost. Keep this exported wrapper for
// AR table estimation while sharing the algorithm with rerun support facts.
func TieredPricing(rules []forms.TieredPriceRule, previousTotalUsage, totalUsage decimal.Decimal) int {
	return supportcalc.TieredPricing(rules, previousTotalUsage, totalUsage)
}

/*
Send commitment metering data on a monthly basis and is triggered under the following three scenarios:
1. Scheduled Worker Task: only the 'Active' BillingConfig's time range left to be measured and sent.
2. BindedSupportPlan Cancel: Same as above.
3. BillingConfig Switch: The status of billingConfig must be 'Closing'.
*/
func (m *SupportPlanModel) SendCommitmentMetering(bindedSupportPlanID uint, date time.Time) (int, int, error) {
	measureDate := timeutils.GetMonthDate(date)
	bindedSupportPlanInfo, err := m.GetBindedSupportPlan(bindedSupportPlanID)
	if err != nil {
		return 0, 0, err
	}
	tenantID := bindedSupportPlanInfo.TenantID
	if bindedSupportPlanInfo.Type == entity.SupportPlanType_FixedPrice {
		log.Info("type = fixed_price, skip send commitment metering", zap.Uint("tenantID", tenantID), zap.Time("measureDate", measureDate), zap.Uint("bindedSupportPlanID", bindedSupportPlanID))
		return 0, 0, nil
	}
	// Get bindedSupportPlan billing time range in measure month, startTime and endTime format: "YYYY-MM-dd 00:00:00"
	startTime, endTime, measureDay := m.GetMeasureDay(bindedSupportPlanInfo.ActiveDate, bindedSupportPlanInfo.ExpiredDate, bindedSupportPlanInfo.ClosedDate, measureDate, measureDate.AddDate(0, 1, 0))
	if measureDay == 0 {
		log.Info("measureDay == 0, skip send commitment metering", zap.Uint("tenantID", tenantID), zap.Time("measureDate", measureDate), zap.Uint("bindedSupportPlanID", bindedSupportPlanID))
		return 0, 0, nil
	}
	log.Info("Get effective range in measure month for current bindedSupportPlan", zap.Uint("tenantID", tenantID), zap.Time("monthDate", measureDate), zap.Uint("bindedSupportPlanID", bindedSupportPlanID), zap.Time("startTime", startTime), zap.Time("endTime", endTime), zap.Int("measureDay", measureDay))

	// Get the using BillingConfig under current SupportPlan. Note: under the current SupportPlan, BillingConfig will not change when trigger sending Commitment
	billingConfig, err := (&billingconfig.BillingConfigModel{}).GetUseBillingConfigByTimeStampWithCache(tenantID, startTime.Add(endTime.Sub(startTime)/2).Unix())
	if err != nil {
		return 0, 0, err
	}
	// Slice time range based on trial status of BindedSupportPlan
	statusTimeRange, err := m.SplitSupportPlanMeteringRangeByTrialStatus(startTime, endTime, bindedSupportPlanInfo)
	if err != nil {
		return 0, 0, err
	}
	for _, timeRange := range statusTimeRange {
		log.Info("Time range slicing based on trial status", zap.Uint("tenantID", tenantID), zap.Time("monthDate", measureDate), zap.Uint("bindedSupportPlanID", bindedSupportPlanID), zap.Uint("billingConfigID", billingConfig.BillingConfigID), zap.Time("startDate", timeRange.ActiveDate), zap.Time("endDate", *timeRange.ExpiredDate), zap.Bool("isInFreeTrial", timeRange.IsInFreeTrial))
		// Deal with free trial, only available for DirectCustomer
		if billingConfig.Type == entity.BillingConfigType_DirectCustomer && timeRange.IsInFreeTrial {
			// update metering time range
			startTime = *timeRange.ExpiredDate
			measureDay = int(endTime.Sub(startTime).Hours() / 24)
		}
	}
	log.Info("Get the final time range for metering", zap.Uint("tenantID", tenantID), zap.Time("monthDate", measureDate), zap.Uint("bindedSupportPlanID", bindedSupportPlanID), zap.Time("starTime", startTime), zap.Time("endTime", endTime), zap.Int("measureDay", measureDay))
	// All in free trail
	if measureDay <= 0 {
		log.Info("In free trial, no need to send commitment metering", zap.Uint("tenantID", tenantID), zap.Time("monthDate", measureDate), zap.Uint("bindedSupportPlanID", bindedSupportPlanID), zap.Time("starTime", startTime), zap.Time("endTime", endTime), zap.Int("measureDay", measureDay))
		return measureDay, 0, nil
	}
	costID := fmt.Sprintf("support-plan-commitment-%d-%d-%d", tenantID, bindedSupportPlanID, startTime.Unix())
	return m.sendToBillCommitmentMetering(costID, tenantID, billingConfig.BillingConfigID, tenantID, billingConfig.BillingConfigID, startTime, endTime, measureDay, bindedSupportPlanInfo, false)
}

// Same as the method: SendCommitmentMetering
func (m *SupportPlanModel) SendResellerCommitmentMeteringIfNeed(bindedSupportPlanID uint, date time.Time) (int, int, error) {
	measureDate := timeutils.GetMonthDate(date)
	bindedSupportPlanInfo, err := m.GetBindedSupportPlan(bindedSupportPlanID)
	if err != nil {
		return 0, 0, err
	}
	tenantID := bindedSupportPlanInfo.TenantID
	if bindedSupportPlanInfo.Type == entity.SupportPlanType_FixedPrice {
		log.Info("type = fixed_price, skip send reseller commitment metering", zap.Uint("customerTenantID", tenantID), zap.Time("measureDate", measureDate), zap.Uint("bindedSupportPlanID", bindedSupportPlanID))
		return 0, 0, nil
	}
	// Get bindedSupportPlan effective range in measure month, startTime and endTime format: "YYYY-MM-dd 00:00:00"
	startTime, endTime, measureDay := m.GetMeasureDay(bindedSupportPlanInfo.ActiveDate, bindedSupportPlanInfo.ExpiredDate, bindedSupportPlanInfo.ClosedDate, measureDate, measureDate.AddDate(0, 1, 0))
	if measureDay == 0 {
		log.Info("measureDay == 0, skip send commitment metering to reseller", zap.Uint("customerTenantID", tenantID), zap.Time("measureDate", measureDate), zap.Uint("bindedSupportPlanID", bindedSupportPlanID))
		return 0, 0, nil
	}

	// Get the using BillingConfig by current SupportPlan, use endTime, billingConfig Switch
	customerBillingConfig, err := (&billingconfig.BillingConfigModel{}).GetUseBillingConfigByTimeStampWithCache(tenantID, startTime.Add(endTime.Sub(startTime)/2).Unix())
	if err != nil {
		return 0, 0, err
	}

	if !isPartnerManagedCustomer(customerBillingConfig.Type) {
		log.Info("customer billing config is not partner-managed, no need to send commitment metering to reseller", zap.Uint("tenantID", tenantID), zap.Time("measureDate", measureDate), zap.Uint("bindedSupportPlanID", bindedSupportPlanID), zap.Time("startTime", startTime), zap.Time("endTime", endTime), zap.String("billingConfigType", customerBillingConfig.Type))
		return measureDay, 0, nil
	}
	log.Info("Get the final time range for reseller metering", zap.Uint("tenantID", tenantID), zap.Time("MonthDate", measureDate), zap.Uint("bindedSupportPlanID", bindedSupportPlanID), zap.Time("startDate", startTime), zap.Time("endDate", endTime), zap.Int("meteringDays", measureDay))

	resellerTenantID, resellerBillingConfigID, err := m.getResellerTenantAndBillingConfig(tenantID, customerBillingConfig)
	if err != nil {
		return 0, 0, err
	}
	costID := fmt.Sprintf("support-plan-commitment-from-%d-to-%d-%d-%d", bindedSupportPlanInfo.TenantID, resellerTenantID, bindedSupportPlanID, startTime.Unix())
	customerTenantID := tenantID
	customerBillingConfigID := customerBillingConfig.BillingConfigID
	// source = customer (own actual cost feeds the commitment gap),
	// target = reseller (whose books the gap lands on). See
	// sendToBillCommitmentMetering for the per-customer base rationale.
	return m.sendToBillCommitmentMetering(costID, customerTenantID, customerBillingConfigID, resellerTenantID, resellerBillingConfigID, startTime, endTime, measureDay, bindedSupportPlanInfo, true)
}

func (m *SupportPlanModel) sendToBillCommitmentMetering(costID string, sourceTenantID uint, sourceBillingConfigID uint, targetTenantID uint, targetBillingConfigID uint, startTime time.Time, endTime time.Time, measureDay int, bindedSupportPlanInfo *forms.BindedSupportPlan, isMarketplaceReseller bool) (int, int, error) {
	if bindedSupportPlanInfo == nil {
		return 0, 0, billing_errors.InvalidInputError
	}
	cnt, err := entity.NewSupportPlanMeteringRecordsQuerySet(db.GetDB()).CostIDEq(costID).Count()
	if err != nil {
		return 0, 0, err
	}
	if cnt > 0 {
		return measureDay, 0, nil
	}
	// Read the customer's own support-fee spend in the period from the
	// SOURCE tenant's billing config (the customer), not the target
	// (the reseller). Same rationale as sendToBillNormalMetering: in a
	// Resale 1:N setup each customer's commitment gap = own commitment -
	// own actual cost, not the partner's aggregate.
	//
	// CommintmentPrice_SKU itself is excluded so the commitment row this
	// function is about to write is not read back into the gap
	// calculation (the row lands in user_unit_costs before the next
	// call observes it).
	costPrice, err := m.getCustomerSupportFeeSpend(sourceTenantID, sourceBillingConfigID, startTime, endTime)
	if err != nil {
		return 0, 0, err
	}
	cost := 0
	commitment := bindedSupportPlanInfo.FixedChargePrice.Mul(decimal.NewFromInt(int64(measureDay))).DivRound(decimal.NewFromInt(MonthDay), 2)
	log.Info("Calculate the metering data for current bindedSupportPlan", zap.Uint("sourceTenantID", sourceTenantID), zap.Uint("targetTenantID", targetTenantID), zap.Uint("bindedSupportPlanID", bindedSupportPlanInfo.BindedSupportPlanID), zap.Uint("billingConfigID", targetBillingConfigID), zap.Time("startTime", startTime), zap.Time("endTime", endTime), zap.String("costPrice", costPrice.String()), zap.String("commitment", commitment.String()))
	costTime := endTime.Add(-1 * time.Second)
	metering, _, err := supportcalc.CalculateCommitmentMetering(supportcalc.CommitmentMeteringInput{
		CostID:                costID,
		CostTime:              costTime,
		TenantID:              targetTenantID,
		BillingConfigID:       targetBillingConfigID,
		BindedSupportPlan:     bindedSupportPlanInfo,
		MeasureDay:            measureDay,
		SupportPlanCostPrice:  costPrice,
		IsMarketplaceReseller: isMarketplaceReseller,
		MarketplaceCustomerID: bindedSupportPlanInfo.TenantID,
	})
	if err != nil {
		return 0, 0, err
	}
	if metering != nil {
		measureModel := &billing.MeasureModel{}
		cost = metering.Cost
		_, err = measureModel.SecondaryMeasure(targetTenantID, targetBillingConfigID, metering)

		if err != nil {
			return 0, 0, err
		}
	}
	recordEntity := &entity.SupportPlanMeteringRecords{
		CostID:              costID,
		CostTimeStamp:       costTime,
		TenantID:            targetTenantID,
		BindedSupportPlanID: bindedSupportPlanInfo.BindedSupportPlanID,
		Unit:                constant.CommintmentPrice_SKU,
		Cost:                cost,
	}
	err = recordEntity.Create(db.GetDB())
	if err != nil {
		return 0, 0, err
	}
	return measureDay, cost, nil
}

func (m *SupportPlanModel) MspCustomerSyncSupportPlan(tenantID, billingConfigID uint) error {
	billingConfig, err := (&billingconfig.BillingConfigModel{}).GetBillingConfig(billingConfigID)
	if err != nil {
		log.Error("MspCustomerSyncSupportPlan GetActiveBillingConfigID ,err", zap.Uint("TenantID", tenantID), zap.Error(err))
		return nil
	}
	if billingConfig.Type != entity.BillingConfigType_MSPCustomer || billingConfig.Status != entity.BillingConfigStatus_Active {
		log.Info("org active billing config is not msp customer, skip sync support plan",
			zap.Uint("TenantID", tenantID), zap.Uint("billingConfigID", billingConfig.BillingConfigID), zap.String("billingConfigType", billingConfig.Type))
		return nil
	}
	provider, err := paymenthandler.GetPaymentHandler().GetProvider(entity.PaymentType_MSP)
	if err != nil {
		log.Error("failed to get MSP provider", zap.Error(err))
		return err
	}
	customerInfo, err := centralhandler.GetCentralHandler().GetTenant(tenantID)
	if err != nil {
		log.Error("failed to get tenant on msp customer support sync", zap.Error(err), zap.Uint("customerID", tenantID))
		return err
	}
	mspTenantIDStr, err := provider.Setup(customerInfo)
	if err != nil {
		log.Error("failed to get msp tenantID", zap.Error(err), zap.Uint("customerID", tenantID))
		return err
	}
	mspTenantIDUint64, err := strconv.ParseUint(mspTenantIDStr, 10, 0)
	if err != nil {
		log.Error("MspCustomerSyncSupportPlan.strconv.ParseUint ,err", zap.Uint("TenantID", tenantID), zap.Error(err))
		return nil
	}
	mspTenantID := uint(mspTenantIDUint64)
	info, err := m.GetTenantActiveBindedPlan(mspTenantID)
	if err != nil {
		log.Error("MspCustomerSyncSupportPlan.GetTenantActiveBindedPlan ,err", zap.Uint("TenantID", tenantID), zap.Error(err))
		return nil
	}
	info.TenantID = tenantID
	jsm.SendJSMSupport(info)
	// sync to Eventbus
	m.SyncCurrentSupportToEventBus(info)
	// send in-app notification
	m.sendBindedSupportPlanUpdateNotification(info)
	return nil

}

func (m *SupportPlanModel) MspSupportPlanChangedSync(tenantID uint) error {
	tenantInfo, err := centralhandler.GetCentralHandler().GetTenant(tenantID)
	if err != nil {
		log.Error("MspSupportPlanChangedSync failed to get tenant from account.", zap.Error(err), zap.Uint("tenantID", tenantID))
		return err
	}

	if tenantInfo.IsMsp {
		log.Info("start to sync msp support plan to customers", zap.Uint("msp", tenantID))
		paymentMethods, err := (&paymentsub.PaymentModel{}).GetPaymentMethodListWithPaymentCode(entity.PaymentType_MSP, strconv.FormatUint(uint64(tenantID), 10))
		if err != nil {
			log.Error("MspSupportPlanChangedSync failed to get paymentMethods.", zap.Error(err), zap.Uint("tenantID", tenantID))
			return err
		}
		for _, paymentMethod := range paymentMethods {
			err = m.MspCustomerSyncSupportPlan(paymentMethod.TenantID, paymentMethod.BindedBillingConfigID)
			if err != nil {
				log.Error("failed to sync msp customer's support plan", zap.Error(err), zap.Uint("msp", tenantID), zap.Uint("customer", paymentMethod.TenantID))
			}
			log.Info("success to sync support plan to customer", zap.Uint("msp", tenantID), zap.Uint("customer", paymentMethod.TenantID))
		}
	}

	return nil
}

func (m *SupportPlanModel) NotifyCurrentSupport(tenantID uint) {
	support, err := m.GetTenantActiveBindedPlan(tenantID)
	if err != nil {
		if !errors.Is(err, billing_errors.ObjectNotExistsError) {
			log.Error("failed to get support plan", zap.Error(err), zap.Uint("tenantId", tenantID))
			return
		}
		support = DefaultBindedSupportPlan(tenantID)
	}
	// sync to jsm
	jsm.SendJSMSupport(support)
	// sync to eventbus
	m.SyncCurrentSupportToEventBus(support)
	// send in-app notification
	m.sendBindedSupportPlanUpdateNotification(support)
}

// sync to eventbus
func (m *SupportPlanModel) SyncCurrentSupportToEventBus(support *forms.BindedSupportPlan) {
	if !config.Get().GetBool(config.SupportPlanUpdateSyncToEventBusEnable) {
		log.Info("forbiden to sync current support info to eventbus", zap.Uint("tenantID", support.TenantID))
		return
	}
	err := db.GetDB().Transaction(func(tx *gorm.DB) error {
		return eventbushandler.Get().PublishEventWithTransaction(BindedSupportPlanUpdateEventType, &eventbushandler.Event{
			EventID: fmt.Sprint(support.BindedSupportPlanID),
			EventData: &jsm.SupportMessage{
				TenantID: support.TenantID,
				Level:    support.Level,
			},
		}, tx)
	})
	if err != nil {
		log.Error("fail to sync current support info to eventbus", zap.Uint("tenantID", support.TenantID), zap.Any("bindedSupportPlan", support), zap.Error(err))
		return
	}
	log.Info("success to sync current support info to eventbus", zap.Uint("tenantID", support.TenantID), zap.Any("bindedSupportPlan", support))
}

// send in-app notification
func (m *SupportPlanModel) sendBindedSupportPlanUpdateNotification(support *forms.BindedSupportPlan) {
	notificationEvent := uncutils.ChangeBindedSupportPlan
	// query tenant info
	tenant, err := centralhandler.GetCentralHandler().GetTenant(support.TenantID)
	if err != nil {
		log.Error("fail to send in-app notification for bindedSupportPlan changed, get tenant info error", zap.Uint("tenantID", support.TenantID), zap.Error(err))
		return
	}
	uncutils.SendInAppNotification(support.TenantID, notificationEvent, map[string]string{
		"orgName":      tenant.Name,
		"supportLevel": support.Level,
		"orgId":        fmt.Sprintf("%d", support.TenantID),
	})
}

func (m *SupportPlanModel) RegisterSupportPlanEventHandler() (err error) {
	group := "support_plan"
	eventHandlerFuncMsp := func(event *eventbushandler.Event) error {
		return m.MspSupportPlanChangedSync(event.EventData.(*forms.TenantID).TenantID)
	}
	err = eventbushandler.Get().RegisterEventHandleFunc(constant.SupportPlanLevelChangedEventType, group, forms.TenantID{}, eventHandlerFuncMsp)

	if err != nil {
		return err
	}

	eventHandlerFunc := func(event *eventbushandler.Event) error {
		billingConfig := event.EventData.(*forms.BillingConfig)
		return m.SupportPlanProcessOnBillingConfigSwitched(billingConfig.BillingConfigID)
	}
	err = eventbushandler.Get().RegisterEventHandleFunc(constant.BillingConfigSwitchedEvent, group, forms.BillingConfig{}, eventHandlerFunc)
	if err != nil {
		return err
	}

	// Added: when user subscribe tidb from alicloud, we should bind a 'enterprise' level support plan to him directly
	// we listen to then event: `TenantPayableEventType` to make sure their payment method have been payable
	eventHandlerFuncBindSupportPlanForAlicloudSubscriber := func(event *eventbushandler.Event) error {
		tenantID := event.EventData.(*forms.TenantID).TenantID
		return m.BindSupportPlanToAliCloudSubscriber(tenantID)
	}
	err = eventbushandler.Get().RegisterEventHandleFunc(paymentsub.AliCloudSubscriberBecomePayableEventType, group, forms.TenantID{}, eventHandlerFuncBindSupportPlanForAlicloudSubscriber)
	if err != nil {
		return err
	}
	return nil
}

/*
Bind a 'enterprise' level support plan to user subscribe tidb from alicloud
*/
func (m *SupportPlanModel) BindSupportPlanToAliCloudSubscriber(tenantID uint) error {
	log.Info("start to bind support plan to subscriber from alicloud", zap.Uint("tenantID", tenantID))
	paymentInfo, err := (&paymentsub.PaymentModel{}).GetPaymentMethodWithPaymentTypeCurrentBillingConfig(tenantID, entity.PaymentType_AliCloud)
	if err != nil {
		log.Error("failed to get alicloud payment info for binding support plan", zap.Uint("tenantID", tenantID), zap.Error(err))
		return err
	}
	if paymentInfo.PaymentMeta.IsThirdParty != nil && *paymentInfo.PaymentMeta.IsThirdParty {
		log.Info("skip binding support plan, aliyun product is third-party", zap.Uint("tenantID", tenantID))
		return nil
	}
	allPublicPlans, err := m.ListPublicSupportPlan()
	if err != nil {
		return err
	}
	var supportPlanID uint
	for _, plan := range allPublicPlans {
		if plan.Level == entity.SupportPlanLevel_Enterprise {
			supportPlanID = plan.SupportPlanID
			break
		}
	}
	_, err = m.BindSupportPlanForPayableCustomer(tenantID, supportPlanID, forms.DateInterval{StartDate: timeutils.GetTimeAddr(timeutils.GetDayDate(timeutils.GetNow()))})
	if err != nil {
		log.Error("failed to bind support plan to subscriber from alicloud", zap.Uint("tenantID", tenantID), zap.Error(err))
	}
	return err
}

/*
To inform users whether they are currently in the free trial period.
*/
func (m *SupportPlanModel) IsInSupportPlanFreeTrialForTenant(tenantID uint, bindedSupportPlan *forms.BindedSupportPlan) (bool, int, error) {
	measureDate := timeutils.GetNow()
	// Retrieve billingConfig utilized
	billingConfig, err := (&billingconfig.BillingConfigModel{}).GetUseBillingConfigByTimeStampWithCache(tenantID, measureDate.Unix())
	if err != nil {
		return false, 0, err
	}
	return m.isInSupportPlanFreeTrialForMeasureDate(measureDate, bindedSupportPlan, billingConfig)
}

func (m *SupportPlanModel) isInSupportPlanFreeTrialForMeasureDate(measureDate time.Time, bindedSupportPlan *forms.BindedSupportPlan, billingConfig *forms.BillingConfig) (bool, int, error) {
	if bindedSupportPlan == nil || billingConfig == nil {
		return false, 0, billing_errors.InvalidInputError
	}
	if bindedSupportPlan.TenantID == 0 || billingConfig.TenantID != bindedSupportPlan.TenantID {
		return false, 0, billing_errors.InvalidInputError
	}
	// free trial is available only for the support plan which is eligible for free trial
	if !bindedSupportPlan.IsEligibleForFreeTrial {
		log.Info("not in supportPlan free trial for measureDate, it is not eligible for free trial", zap.Uint("tenantID", bindedSupportPlan.TenantID), zap.Time("measureDate", measureDate), zap.Uint("bindedSupportPlanID", bindedSupportPlan.BindedSupportPlanID))
		return false, 0, nil
	}
	// free trail is only available to direct customer (not available for MSP and MarketplaceResellerManagedCUstomer)
	if billingConfig.Type != entity.BillingConfigType_DirectCustomer {
		log.Info("not in supportPlan free trial for measureDate, is MarketplaceResellerManagedCustomer", zap.Uint("tenantID", bindedSupportPlan.TenantID), zap.Time("measureDate", measureDate), zap.Uint("bindedSupportPlanID", bindedSupportPlan.BindedSupportPlanID))
		return false, 0, nil
	}
	formatTrialEndDay := timeutils.GetNextMidnight(bindedSupportPlan.ActiveDate.AddDate(0, 0, 30))
	if supportcalc.IsInFreeTrialForMeasureDate(measureDate, bindedSupportPlan, billingConfig) {
		restNumOfFreeTrial := int(formatTrialEndDay.Sub(measureDate).Hours() / 24)
		return true, restNumOfFreeTrial, nil
	}
	return false, 0, nil
}

func (m *SupportPlanModel) SplitSupportPlanMeteringRangeByTrialStatus(startDateOfRange time.Time, endDateOfRange time.Time, bindedSupportPlan *forms.BindedSupportPlan) ([]forms.BindedSupportPlan, error) {
	if bindedSupportPlan == nil {
		return nil, billing_errors.InvalidInputError
	}
	// not in the effective time of bindedSupportPlan
	if startDateOfRange.Compare(bindedSupportPlan.ActiveDate) < 0 {
		return nil, billing_errors.TimeTimeRangeConflicted
	}
	if !bindedSupportPlan.IsEligibleForFreeTrial {
		return []forms.BindedSupportPlan{
			{
				ActiveDate:    startDateOfRange,
				ExpiredDate:   &endDateOfRange,
				ClosedDate:    &endDateOfRange,
				IsInFreeTrial: false,
			},
		}, nil
	}
	// Check if the 30-day free trial overlaps with the time range
	// -1s
	trialExpireDate := bindedSupportPlan.ActiveDate.AddDate(0, 0, 30).Add(-time.Second)
	// The effective time of the SupportPlan may be less than 30 days
	trialClosedDate := timeutils.GetMinTime(bindedSupportPlan.ClosedDate, bindedSupportPlan.ExpiredDate)
	// time format of return: "yyyy-mm-dd 00:00:00"
	trialStartDate, trialEndDate, measureDay := m.GetMeasureDay(bindedSupportPlan.ActiveDate, &trialExpireDate, trialClosedDate, startDateOfRange, endDateOfRange)
	// Not in free trial period
	if measureDay == 0 {
		return []forms.BindedSupportPlan{
			{
				ActiveDate:    startDateOfRange,
				ExpiredDate:   &endDateOfRange,
				ClosedDate:    &endDateOfRange,
				IsInFreeTrial: false,
			},
		}, nil
	}
	// TimeRange(trialStartDate ~ trialEndDate) must in Range(startDateOfRange, endDateOfRange)
	if !trialStartDate.Equal(startDateOfRange) || trialEndDate.After(endDateOfRange) {
		return nil, billing_errors.SystemError
	}
	return []forms.BindedSupportPlan{
		{
			ActiveDate:    trialStartDate,
			ExpiredDate:   &trialEndDate,
			ClosedDate:    &trialEndDate,
			IsInFreeTrial: true,
		},
		{
			ActiveDate:    trialEndDate,
			ExpiredDate:   &endDateOfRange,
			ClosedDate:    &endDateOfRange,
			IsInFreeTrial: false,
		},
	}, nil
}

func (m *SupportPlanModel) SupportPlanProcessOnBillingConfigSwitched(billingConfigID uint) error {
	billingConfig, err := (&billingconfig.BillingConfigModel{}).GetBillingConfig(billingConfigID)
	if err != nil {
		return err
	}
	if billingConfig.Status != entity.BillingConfigStatus_Active || (billingConfig.Type != entity.BillingConfigType_MSPCustomer && billingConfig.Type != entity.BillingConfigType_MarketplaceResellerManagedCustomer) {
		log.Info("skip support plan processing because of the status or type", zap.Uint("billingConfigID", billingConfigID), zap.String("status", billingConfig.Status), zap.String("type", billingConfig.Type))
		return err
	}
	/*
		1. Billing Config switch, the old one will expired and closed after 4 days.
		2. BillingConfig once expired, The current correspond BindedSupportPlan need to be canceled, and send metering data about SupportPlan immediately, before the billigConfig closed.
		3. Both *active* and *expired* BindedSupportPlans should be immediately set to *canceled* status.
		Why?: 10th: bindedSupportPlan expires and is marked 'expired' (set to 'closed' next month).
			  15th: billingConfig Switch, it's status is set to 'expired', and 19th is set to 'closed'.
			  Next Month: 'expired' bindedSupportPlan need to send Commitment metering by Worker，but it's corresponding billingConfig is closed, no commitment metering data will be send for 0~10 days.

	*/
	bindedSupportPlans, err := m.ListTenantsBindedSupportPlan(billingConfig.TenantID, entity.BindedSupportPlanStatus_ACTIVE, entity.BindedSupportPlanStatus_Expired)
	if err != nil {
		log.Error("CancelBindedSupportPlan ListTenantsBindedSupportPlan error", zap.Uint("tenantID", billingConfig.TenantID), zap.Uint("billingConfigID", billingConfig.BillingConfigID), zap.Error(err))
	} else {
		for _, info := range bindedSupportPlans {
			_, err = m.CancelBindedSupportPlan(info.BindedSupportPlanID)
			if err != nil {
				log.Error("CancelBindedSupportPlan error", zap.Uint("tenantID", billingConfig.TenantID), zap.Uint("billingConfigID", billingConfig.BillingConfigID), zap.Any("SupportPlanInfo", info), zap.Error(err))
			}
		}
	}
	if billingConfig.BindSupportPlanOnEnabled {
		log.Info("should to bind new support plan", zap.Uint("billingConfigID", billingConfigID), zap.Uint("tenantID", billingConfig.TenantID), zap.Uintp("supportPlanID", billingConfig.SupportPlanID), zap.Timep("start", billingConfig.StartActiveTime), zap.Timep("end", billingConfig.SupportPlanExpiredDate))
		_, err = m.bindSupportPlan(billingConfig.TenantID, *billingConfig.SupportPlanID, forms.DateInterval{StartDate: billingConfig.StartActiveTime, EndDate: billingConfig.SupportPlanExpiredDate})
		if err != nil {
			log.Error("failed bind support plan on billing config switched", zap.Error(err), zap.Uint("billingConfigID", billingConfigID), zap.Uint("tenantID", billingConfig.TenantID), zap.Uintp("supportPlanID", billingConfig.SupportPlanID))
		}
	}
	if billingConfig.Type == entity.BillingConfigType_MSPCustomer {
		err = m.MspCustomerSyncSupportPlan(billingConfig.TenantID, billingConfigID)
		if err != nil {
			log.Error("failed to sync msp customer support plan", zap.Error(err), zap.Uint("billingConfigID", billingConfigID), zap.Uint("tenantID", billingConfig.TenantID))
			return err
		}
	}
	return nil
}

func (m *SupportPlanModel) GetBindedSupportPlansEndingFreeTrialSoon(now time.Time) ([]entity.BindedSupportPlan, error) {
	var plans []entity.BindedSupportPlan
	trialEndDateStart := now.AddDate(0, 0, 5)
	trialEndDateEnd := now.AddDate(0, 0, 6)

	err := entity.NewBindedSupportPlanQuerySet(db.GetDB()).
		IsEligibleForFreeTrialEq(true).
		StatusEq(entity.BindedSupportPlanStatus_ACTIVE).
		ActiveDateGte(trialEndDateStart.AddDate(0, 0, -30)).
		ActiveDateLt(trialEndDateEnd.AddDate(0, 0, -30)).
		All(&plans)

	if err != nil {
		return nil, err
	}

	return plans, nil
}

func (m *SupportPlanModel) SendFreeTrialReminderEmail(bindedSupportPlanID uint) error {
	bindedSupportPlan, err := m.GetBindedSupportPlan(bindedSupportPlanID)
	if err != nil {
		return err
	}

	if !bindedSupportPlan.IsEligibleForFreeTrial {
		log.Info("support plan is not eligible for free trial, skip sending reminder email",
			zap.Uint("bindedSupportPlanID", bindedSupportPlanID))
		return nil
	}

	tenant, err := centralhandler.GetCentralHandler().GetTenant(bindedSupportPlan.TenantID)
	if err != nil {
		log.Error("failed to get tenant", zap.Uint("tenantID", bindedSupportPlan.TenantID), zap.Error(err))
		return err
	}

	templateID := "d-a7f8884b323b4684888100af628f4d73"
	emailKey := fmt.Sprintf("support_plan_free_trial_ending_%d", bindedSupportPlanID)

	vars := map[string]string{
		"Org_ID":   strconv.FormatUint(uint64(tenant.TenantID), 10),
		"Org_name": tenant.Name,
	}

	err = centralhandler.GetCentralHandler().SendTenantEmailWithLabelOnce(
		tenant.TenantID,
		emailKey,
		templateID,
		vars,
		account.MailLabels_MAIL_LABELS_ORG_OWNER,
		[]account.MailLabels{account.MailLabels_MAIL_LABELS_ORG_BILLING_ADMINS},
	)

	if err != nil {
		log.Error("failed to send free trial reminder email",
			zap.Uint("tenantID", tenant.TenantID),
			zap.Uint("bindedSupportPlanID", bindedSupportPlanID),
			zap.Error(err))
		return err
	}

	log.Info("successfully sent free trial reminder email",
		zap.Uint("tenantID", tenant.TenantID),
		zap.Uint("bindedSupportPlanID", bindedSupportPlanID))

	return nil
}
