package paymentsub

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

	"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/eventbushandler"
	"github.com/pingcap/dbaas/billing2/handler/paymenthandler"
	"github.com/pingcap/dbaas/billing2/model/billingconfig"
	"github.com/pingcap/dbaas/billing2/utils/errorutils"
	"github.com/pingcap/dbaas/billing2/utils/log"
	"github.com/pingcap/dbaas/billing2/utils/timeutils"
	"github.com/pingcap/dbaas/billing2/utils/uncutils"
	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/promauto"
	"github.com/shopspring/decimal"
	"go.uber.org/zap"
	"gorm.io/gorm"
	"gorm.io/gorm/clause"
)

type PaymentModel struct {
}

var (
	InvoicePayCounter = promauto.NewCounterVec(prometheus.CounterOpts{
		Namespace: "dbaas",
		Subsystem: "billing",
		Name:      "invoice_pay",
	}, []string{"paymentType", "status"})
)

const TenantPayableEventType = "tenant-payable"
const AliCloudSubscriberBecomePayableEventType = "alicloud-subscriber-become-payable"
const InvoicePaidEventType = "invoice-paid"

func getPaymentInfoFromEntity(payment *entity.PaymentMethod) *forms.PaymentMethod {
	ret := forms.PaymentMethod{
		PaymentMethodID:       payment.ID,
		TenantID:              payment.TenantID,
		PaymentType:           payment.PaymentType,
		PaymentCode:           payment.PaymentCode,
		PaymentSubCode:        payment.PaymentSubCode,
		Payable:               payment.Payable,
		InvoiceableAt:         payment.InvoiceableAt,
		ManagedBySales:        payment.ManagedBySales,
		Status:                payment.Status,
		UnpayableAt:           payment.UnpayableAt,
		BindedBillingConfigID: &payment.BindedBillingConfigID,
	}
	if payment.Invoiceable == nil {
		ret.Invoiceable = true
	} else {
		ret.Invoiceable = *payment.Invoiceable
	}
	if payment.Meta != nil {
		ret.PaymentMeta = forms.PaymentMeta(*payment.Meta)
	} else {
		ret.PaymentMeta = forms.PaymentMeta{
			EnlargeMarketplaceMetering: false,
			EnlargerDiscount:           decimal.NewFromInt(1),
		}
	}
	return &ret
}

func (*PaymentModel) GetActivePaymentMethod(tenantID uint, billingConfigID uint) (*forms.PaymentMethod, error) {
	paymentEntity := &entity.PaymentMethod{}
	err := errorutils.WrapSQLError(entity.NewPaymentMethodQuerySet(db.GetDB()).
		TenantIDEq(tenantID).StatusEq(entity.PaymentMethodStatus_Active).
		BindedBillingConfigIDEq(billingConfigID).One(paymentEntity), fmt.Sprintf("tenant %d billingConfigID %d  payment method", tenantID, billingConfigID))
	if err != nil {
		return nil, err
	}
	return getPaymentInfoFromEntity(paymentEntity), nil
}

func (m *PaymentModel) GetActivePaymentMethodWithCache(tenantID uint, billingConfigID uint) (*forms.PaymentMethod, error) {
	funcName := "GetActivePaymentMethod"
	var ret forms.PaymentMethod
	found := cachehandler.Get().Get(funcName, &ret, tenantID, billingConfigID)
	if found {
		return &ret, nil
	}
	paymentMethod, err := m.GetActivePaymentMethod(tenantID, billingConfigID)
	if err != nil {
		return nil, err
	}
	cachehandler.Get().Set(funcName, paymentMethod, tenantID, billingConfigID)
	return paymentMethod, nil
}

func (*PaymentModel) GetPaymentMethodListWithPaymentCode(paymentType string, paymentCode string) ([]entity.PaymentMethod, error) {
	var entitySlice []entity.PaymentMethod
	err := errorutils.WrapSQLError(entity.NewPaymentMethodQuerySet(db.GetDB()).PaymentCodeEq(paymentCode).PaymentTypeEq(paymentType).All(&entitySlice), fmt.Sprintf("GetPaymentMethodListWithPaymentCode payment method type %s (code: %s)", paymentCode, paymentType))
	if err != nil {
		return nil, err
	}
	return entitySlice, nil
}

func (*PaymentModel) GetPaymentMethodListWithPaymentSubCode(paymentType string, paymentCode string) ([]entity.PaymentMethod, error) {
	var entitySlice []entity.PaymentMethod
	err := errorutils.WrapSQLError(entity.NewPaymentMethodQuerySet(db.GetDB()).PaymentSubCodeEq(paymentCode).PaymentTypeEq(paymentType).All(&entitySlice), fmt.Sprintf("GetPaymentMethodListWithPaymentCode payment method type %s (code: %s)", paymentCode, paymentType))
	if err != nil {
		return nil, err
	}
	return entitySlice, nil
}

func (*PaymentModel) GetPaymentMethod(paymentMethodID uint) (*forms.PaymentMethod, error) {
	paymentEntity := &entity.PaymentMethod{}
	err := errorutils.WrapSQLError(entity.NewPaymentMethodQuerySet(db.GetDB()).IDEq(paymentMethodID).One(paymentEntity), fmt.Sprintf("payment method %d", paymentMethodID))
	if err != nil {
		return nil, err
	}
	return getPaymentInfoFromEntity(paymentEntity), nil
}

func (m *PaymentModel) GetPaymentMethodWithCache(paymentMethodID uint) (*forms.PaymentMethod, error) {
	funcName := "GetPaymentMethod"
	var ret forms.PaymentMethod
	found := cachehandler.Get().Get(funcName, &ret, paymentMethodID)
	if found {
		return &ret, nil
	}
	paymentMethod, err := m.GetPaymentMethod(paymentMethodID)
	if err != nil {
		return nil, err
	}
	cachehandler.Get().Set(funcName, paymentMethod, paymentMethodID)
	return paymentMethod, nil
}

func (*PaymentModel) GetPaymentMethodWithPaymentType(tenantID, billingConfigID uint, paymentType string) (*forms.PaymentMethod, error) {
	paymentEntity := &entity.PaymentMethod{}
	err := errorutils.WrapSQLError(entity.NewPaymentMethodQuerySet(db.GetDB()).TenantIDEq(tenantID).BindedBillingConfigIDEq(billingConfigID).PaymentTypeEq(paymentType).One(paymentEntity), fmt.Sprintf("tenant %d payment method type %s", tenantID, paymentType))
	if err != nil {
		return nil, err
	}
	return getPaymentInfoFromEntity(paymentEntity), nil
}
func (*PaymentModel) GetPaymentMethodWithPaymentTypeEntity(tenantID uint, paymentType string) ([]entity.PaymentMethod, error) {
	var entitySlice []entity.PaymentMethod
	err := errorutils.WrapSQLError(entity.NewPaymentMethodQuerySet(db.GetDB()).TenantIDEq(tenantID).PaymentTypeEq(paymentType).All(&entitySlice), fmt.Sprintf("tenant %d payment method type %s", tenantID, paymentType))
	if err != nil {
		return nil, err
	}
	return entitySlice, nil
}

func (m *PaymentModel) GetPaymentMethodWithPaymentTypeCurrentBillingConfig(tenantID uint, paymentType string) (*forms.PaymentMethod, error) {
	billingConfigID, err := (&billingconfig.BillingConfigModel{}).GetActiveBillingConfigID(tenantID)
	if err != nil {
		return nil, err
	}
	return m.GetPaymentMethodWithPaymentType(tenantID, billingConfigID, paymentType)
}

func (*PaymentModel) GetPaymentMethodWithPaymentCode(paymentType string, paymentCode string) (*forms.PaymentMethod, error) {
	paymentEntity := &entity.PaymentMethod{}
	err := errorutils.WrapSQLError(entity.NewPaymentMethodQuerySet(db.GetDB()).PaymentCodeEq(paymentCode).PaymentTypeEq(paymentType).One(paymentEntity), fmt.Sprintf("payment method type %s (code: %s)", paymentCode, paymentType))
	if err != nil {
		return nil, err
	}
	return getPaymentInfoFromEntity(paymentEntity), nil
}

func (*PaymentModel) ListPaymentMethods(tenantID uint, billingConfigID uint) ([]forms.PaymentMethod, error) {
	paymentEntities := []entity.PaymentMethod{}
	err := entity.NewPaymentMethodQuerySet(db.GetDB()).TenantIDEq(tenantID).BindedBillingConfigIDEq(billingConfigID).All(&paymentEntities)
	if err != nil {
		return nil, err
	}
	infos := []forms.PaymentMethod{}
	for _, paymentEntity := range paymentEntities {
		paymentInfo := getPaymentInfoFromEntity(&paymentEntity)
		infos = append(infos, *paymentInfo)
	}
	return infos, nil
}

func (m *PaymentModel) IsHourlyPayment(tenantID uint, billingConfigID uint) (bool, error) {
	method, err := m.GetActivePaymentMethod(tenantID, billingConfigID)
	if err != nil {
		if errors.Is(err, billing_errors.ObjectNotExistsError) {
			return false, nil
		}
		return false, err
	}
	return method.PaymentType == entity.PaymentType_AWS || method.PaymentType == entity.PaymentType_GCP, nil
}

func (m *PaymentModel) IsMspPayment(tenantID uint, billingConfigID uint) (bool, error) {
	method, err := m.GetActivePaymentMethod(tenantID, billingConfigID)
	if err != nil {
		if errors.Is(err, billing_errors.ObjectNotExistsError) {
			return false, nil
		}
		return false, err
	}
	return method.PaymentType == entity.PaymentType_MSP, nil
}
func (m *PaymentModel) sendPaymentMethodChangedEvent(tx *gorm.DB, tenantID uint) error {
	var err error
	err = eventbushandler.Get().PublishEventWithTransaction(constant.PaymentMethodChangedEventType,
		&eventbushandler.Event{
			EventID:   fmt.Sprint(tenantID),
			EventType: constant.PaymentMethodChangedEventType,
			EventData: forms.TenantID{
				TenantID: tenantID,
			},
		},
		tx,
	)
	if err != nil {
		log.Error("failed to send event when sync payment methods", zap.Uint("tenantID", tenantID), zap.Error(err))
		return err
	}
	return err
}

func (m *PaymentModel) sendPayableEventNoTransaction(tenantID uint) error {
	var err error
	err = eventbushandler.Get().PublishEvent(TenantPayableEventType,
		&eventbushandler.Event{
			EventID:   fmt.Sprint(tenantID),
			EventType: TenantPayableEventType,
			EventData: forms.TenantID{
				TenantID: tenantID,
			},
		},
	)
	if err != nil {
		log.Error("failed to send event when sync payment methods", zap.Uint("tenantID", tenantID), zap.Error(err))
		return err
	}
	return err
}

func (m *PaymentModel) sendPayableEvent(tx *gorm.DB, tenantID uint) error {
	var err error
	err = eventbushandler.Get().PublishEventWithTransaction(TenantPayableEventType,
		&eventbushandler.Event{
			EventID:   fmt.Sprint(tenantID),
			EventType: TenantPayableEventType,
			EventData: forms.TenantID{
				TenantID: tenantID,
			},
		},
		tx,
	)
	if err != nil {
		log.Error("failed to send event when sync payment methods", zap.Uint("tenantID", tenantID), zap.Error(err))
		return err
	}
	return err
}

func (m *PaymentModel) createPaymentMethod(tenantID uint, paymentInfo *forms.PaymentMethodCreateQuery) (*forms.PaymentMethod, error) {
	return m.createPaymentMethodWithTx(db.GetDB(), tenantID, paymentInfo)
}

func (m *PaymentModel) createPaymentMethodWithTx(tx *gorm.DB, tenantID uint, paymentInfo *forms.PaymentMethodCreateQuery) (*forms.PaymentMethod, error) {
	cnt, err := entity.NewPaymentMethodQuerySet(tx).TenantIDEq(tenantID).PaymentTypeEq(paymentInfo.PaymentType).BindedBillingConfigIDEq(*paymentInfo.BindedBillingConfigID).Count()
	if err != nil {
		return nil, err
	}
	if cnt > 0 {
		return nil, billing_errors.NewObjectAlreadyExistsError(errors.New("tenant payment method with specify already exists"))
	}
	paymentHandler, err := paymenthandler.GetPaymentHandler().GetProvider(paymentInfo.PaymentType)
	if err != nil {
		return nil, err
	}
	tenant, err := centralhandler.GetCentralHandler().GetTenant(tenantID)
	if err != nil {
		return nil, err
	}
	var userCode string
	var subCode *string
	if paymentInfo.SetUpPaymentCode {
		userCode = paymentInfo.PaymentCode
		subCode = paymentInfo.PaymentSubCode
	} else {
		userCode, err = paymentHandler.Setup(tenant)
		if err != nil {
			return nil, err
		}
	}
	invoiceable := true
	if paymentInfo.Invoiceable != nil {
		invoiceable = *paymentInfo.Invoiceable
	}
	payable, err := paymentHandler.Payable(tenantID, userCode)
	paymentEntity := &entity.PaymentMethod{
		TenantID:              tenantID,
		PaymentType:           paymentInfo.PaymentType,
		PaymentCode:           userCode,
		PaymentSubCode:        subCode,
		Invoiceable:           &invoiceable,
		Payable:               payable,
		ManagedBySales:        paymentInfo.ManagedBySales,
		Status:                entity.PaymentMethodStatus_InActive,
		BindedBillingConfigID: *paymentInfo.BindedBillingConfigID,
	}

	if paymentInfo.PaymentMeta.EnlargeMarketplaceMetering || paymentInfo.PaymentMeta.IsThirdParty != nil {
		enlargerDiscount := paymentInfo.PaymentMeta.EnlargerDiscount
		if enlargerDiscount.IsZero() {
			enlargerDiscount = decimal.NewFromInt(1)
		}
		paymentEntity.Meta = &entity.PaymentMeta{
			EnlargeMarketplaceMetering: paymentInfo.PaymentMeta.EnlargeMarketplaceMetering,
			EnlargerDiscount:           enlargerDiscount,
			IsThirdParty:               paymentInfo.PaymentMeta.IsThirdParty,
		}
	}
	err = paymentEntity.Create(tx)
	if err != nil {
		return nil, err
	}
	log.Info("success create payment method", zap.Uint("tenantID", tenantID))
	return getPaymentInfoFromEntity(paymentEntity), nil
}

func (m *PaymentModel) setPaymentMethodActiveWithTx(tx *gorm.DB, tenantID uint, billingConfigID uint, paymentMethodID uint) error {
	var err error
	defer func() {
		if err != nil {
			log.Info("failed to active payment method", zap.Error(err), zap.Uint("tenantID", tenantID), zap.Uint("paymentMethodID", paymentMethodID))
		}
	}()
	var payments []entity.PaymentMethod
	err = entity.NewPaymentMethodQuerySet(tx.Clauses(clause.Locking{Strength: "UPDATE"})).TenantIDEq(tenantID).BindedBillingConfigIDEq(billingConfigID).StatusEq(entity.PaymentMethodStatus_Active).All(&payments)
	if err != nil {
		return err
	}
	if len(payments) != 0 {
		for _, p := range payments {
			if p.ID == paymentMethodID {
				continue
			}
			err = entity.NewPaymentMethodQuerySet(tx).IDEq(p.ID).GetUpdater().SetStatus(entity.PaymentMethodStatus_InActive).Update()
			if err != nil {
				return err
			}
		}
	}
	rows, err := entity.NewPaymentMethodQuerySet(tx).TenantIDEq(tenantID).IDEq(paymentMethodID).GetUpdater().SetStatus(entity.PaymentMethodStatus_Active).UpdateNum()
	if err != nil {
		return err
	} else if rows == 0 {
		return billing_errors.ObjectNotExistsError
	}
	record := entity.PaymentMethodActiveRecord{PaymentMethodID: paymentMethodID, TenantID: tenantID}
	err = record.Create(tx)
	if err != nil {
		return err
	}
	err = m.sendPayableEvent(tx, tenantID)
	if err != nil {
		return err
	}
	err = m.sendPaymentMethodChangedEvent(tx, tenantID)
	if err != nil {
		return err
	}
	log.Info("success set payment method active", zap.Uint("tenantID", tenantID), zap.Uint("paymentMethodID", paymentMethodID))
	return nil
}

func (m *PaymentModel) CreatePaymentMethodAndActive(tenantID uint, paymentInfo *forms.PaymentMethodCreateQuery) (*forms.PaymentMethod, error) {
	var ret *forms.PaymentMethod
	err := db.GetDB().Transaction(func(tx *gorm.DB) error {
		paymentMethod, err := m.createPaymentMethodWithTx(tx, tenantID, paymentInfo)
		if err != nil {
			return err
		}
		err = m.setPaymentMethodActiveWithTx(tx, tenantID, *paymentInfo.BindedBillingConfigID, paymentMethod.PaymentMethodID)
		if err != nil {
			return err
		}
		paymentMethod.Status = entity.PaymentMethodStatus_Active
		ret = paymentMethod
		return nil
	})
	return ret, err
}

func (m *PaymentModel) ActivePaymentMethod(tenantID uint, billingConfigID uint, paymentMethodID uint) error {
	return db.GetDB().Transaction(func(tx *gorm.DB) error {
		return m.setPaymentMethodActiveWithTx(tx, tenantID, billingConfigID, paymentMethodID)
	})
}

// only for test
func (m *PaymentModel) ActiveWireTransferPayment(tenantID uint, billingConfigID uint) (*forms.PaymentMethod, error) {
	payment, err := m.GetPaymentMethodWithPaymentTypeCurrentBillingConfig(tenantID, entity.PaymentType_WireTransfer)
	if err != nil {
		if errors.Is(err, billing_errors.ObjectNotExistsError) {
			paymentInfo := &forms.PaymentMethodCreateQuery{
				PaymentType:           entity.PaymentType_WireTransfer,
				BindedBillingConfigID: &billingConfigID,
			}
			return m.CreatePaymentMethodAndActive(tenantID, paymentInfo)
		}
		return nil, err
	}
	err = m.ActivePaymentMethod(tenantID, billingConfigID, payment.PaymentMethodID)
	if err != nil {
		return nil, err
	}
	return m.GetPaymentMethodWithPaymentTypeCurrentBillingConfig(tenantID, entity.PaymentType_WireTransfer)
}

func (m *PaymentModel) syncPaymentMethod(tenantID uint, paymentID uint) error {
	tenant, err := centralhandler.GetCentralHandler().GetTenant(tenantID)
	if err != nil {
		return err
	}
	if tenant.Status != centralhandler.TenantStatusActive {
		log.Info("skip to sync payment method, tenant is not active")
		return nil
	}
	log.Info("start to sync payment method", zap.Uint("tenant", tenantID), zap.Uint("paymentID", paymentID))
	err = db.GetDB().Transaction(func(tx *gorm.DB) error {
		var ent entity.PaymentMethod
		err := entity.NewPaymentMethodQuerySet(tx.Clauses(clause.Locking{Strength: "UPDATE"})).IDEq(paymentID).TenantIDEq(tenantID).One(&ent)
		if err != nil {
			return err
		}
		paymentInfo := getPaymentInfoFromEntity(&ent)
		payable := paymentInfo.ManagedBySales
		if !paymentInfo.ManagedBySales {
			paymentHandler, err := paymenthandler.GetPaymentHandler().GetProvider(paymentInfo.PaymentType)
			if err != nil {
				return err
			}
			payable, err = paymentHandler.Payable(tenantID, paymentInfo.PaymentCode)
			if err != nil {
				return err
			}
		}
		if payable == paymentInfo.Payable {
			// not changed
			return nil
		}
		var unpayableAt *time.Time
		if !payable {
			unpayableAt = timeutils.GetNowAddr()
		}
		err = entity.NewPaymentMethodQuerySet(tx).IDEq(paymentID).GetUpdater().
			SetPayable(payable).SetUnpayableAt(unpayableAt).
			Update()
		if err != nil {
			return err
		}
		if paymentInfo.Status == entity.PaymentMethodStatus_Active {
			err = m.sendPayableEvent(tx, tenantID)
			if err != nil {
				return err
			}
		}
		log.Info("synced tenant payment method",
			zap.Uint("tenant", tenantID), zap.Bool("payable", payable),
			zap.Uint("paymentID", paymentID), zap.String("paymentType", paymentInfo.PaymentType),
			zap.String("paymentCode", paymentInfo.PaymentCode),
		)
		return nil
	})
	return err
}

func (m *PaymentModel) SyncPayable(tenantID uint) error {
	model := &billingconfig.BillingConfigModel{}
	billingConfigID, err := model.GetActiveBillingConfigID(tenantID)
	if err != nil {
		return billing_errors.NewInvalidInputError(err)
	}
	paymentInfo, err := m.GetActivePaymentMethod(tenantID, billingConfigID)
	if err != nil {
		if errors.Is(err, billing_errors.ObjectNotExistsError) {
			return nil
		}
		return err
	}
	if paymentInfo.PaymentType == entity.PaymentType_AZURE {
		return nil
	}
	return m.syncPaymentMethod(tenantID, paymentInfo.PaymentMethodID)
}

func (m *PaymentModel) SyncPayableByBillingConfigID(tenantID, billingConfigID uint) error {
	paymentInfo, err := m.GetActivePaymentMethod(tenantID, billingConfigID)
	if err != nil {
		if errors.Is(err, billing_errors.ObjectNotExistsError) {
			return nil
		}
		return err
	}
	return m.syncPaymentMethod(tenantID, paymentInfo.PaymentMethodID)
}

func (m *PaymentModel) Payable(tenantID uint) (bool, error) {
	billingConfigID, err := (&billingconfig.BillingConfigModel{}).GetActiveBillingConfigID(tenantID)
	if err != nil {
		return false, billing_errors.NewInvalidInputError(err)
	}
	return m.BillingConfigPayable(tenantID, billingConfigID)
}

func (m *PaymentModel) BillingConfigPayable(tenantID, billingConfigID uint) (bool, error) {
	paymentInfo, err := m.GetActivePaymentMethod(tenantID, billingConfigID)
	if err != nil {
		if errors.Is(err, billing_errors.ObjectNotExistsError) {
			return false, nil
		}
		return false, err
	}
	return paymentInfo.Payable, nil
}

func (m *PaymentModel) BillingConfigInvoiceable(tenantID uint, billingConfigID uint) (bool, error) {
	paymentInfo, err := m.GetActivePaymentMethod(tenantID, billingConfigID)
	if err != nil {
		if errors.Is(err, billing_errors.ObjectNotExistsError) {
			return false, nil
		}
		return false, err
	}
	if !paymentInfo.Invoiceable || (paymentInfo.InvoiceableAt != nil && paymentInfo.InvoiceableAt.Add(config.Get().GetDuration(config.InvoicePayableDurationAfterInvoiceableHour)*time.Hour).After(timeutils.GetNow())) {
		return false, nil
	}
	return true, nil
}

func (m *PaymentModel) GetAllPayableTenantIDs() ([]forms.TenantID, error) {
	result := []forms.TenantID{}
	err := db.GetDB().Raw("select distinct tenant_id from payment_methods where payable=1 and status=?", entity.PaymentMethodStatus_Active).Scan(&result).Error
	if err != nil {
		return nil, billing_errors.NewSystemError(err)
	}
	return result, nil
}

func (m *PaymentModel) GetAllPayableAndActivePaymentInfo() ([]entity.PaymentMethod, error) {
	var entitySlice []entity.PaymentMethod
	err := errorutils.WrapSQLError(entity.NewPaymentMethodQuerySet(db.GetDB()).PayableEq(true).StatusEq(entity.PaymentMethodStatus_Active).All(&entitySlice), fmt.Sprintf(" GetAllPayableAndActiveTenantIDs method"))
	if err != nil {
		return nil, err
	}
	return entitySlice, nil
}

func (m *PaymentModel) GetAllActivePaymentsWithType(paymentType ...string) ([]*forms.PaymentMethod, error) {
	var entitySlice []entity.PaymentMethod
	err := errorutils.WrapSQLError(entity.NewPaymentMethodQuerySet(db.GetDB()).StatusEq(entity.PaymentMethodStatus_Active).PaymentTypeIn(paymentType...).All(&entitySlice), fmt.Sprintf(" GetAllPayableAndActiveTenantIDs method"))
	if err != nil {
		return nil, err
	}
	ret := make([]*forms.PaymentMethod, len(entitySlice))
	for i := 0; i != len(entitySlice); i++ {
		ret[i] = getPaymentInfoFromEntity(&entitySlice[i])
	}
	return ret, nil
}

func (m *PaymentModel) pay(tenantID uint, billingConfigID uint, invoices []forms.Invoice) (ret *forms.PaymentResult, retError error) {
	var err error
	paymentInfo, err := m.GetActivePaymentMethod(tenantID, billingConfigID)
	if err != nil {
		return nil, err
	}
	invoiceable, err := m.BillingConfigInvoiceable(tenantID, billingConfigID)
	if err != nil {
		return nil, err
	}
	if !invoiceable {
		log.Info("payment is uninvoiceable, skip paying", zap.Uint("tenantID", tenantID), zap.Uint("billingConfigID", billingConfigID), zap.Uint("paymentID", paymentInfo.PaymentMethodID))
		return nil, billing_errors.UninvoiceableError
	}
	if len(invoices) == 0 {
		return &forms.PaymentResult{PaymentMethodID: paymentInfo.PaymentMethodID}, nil
	}
	billingInfoModel := &BillingInformationModel{}
	tenantCompany, err := billingInfoModel.GetTenantBillingCompany(tenantID)
	if err != nil {
		return nil, err
	}
	billingCompany, err := billingInfoModel.GetBillingCompany(tenantCompany.CompanyKey)
	if err != nil {
		return nil, err
	}
	for i := 0; i != len(invoices); i++ {
		invoices[i].PaymentMethodID = &paymentInfo.PaymentMethodID
		invoices[i].Meta = forms.InvoiceMeta{PaymentMeta: paymentInfo.PaymentMeta}
		if billingCompany.DisplayLocalCurrencyCommentOnInvoice {
			appliedFXRate, err := (&FXRateModel{}).GetInvoiceAppliedFX(forms.CurrencyUSD, forms.Currency(billingCompany.LocalCurrency))
			if err != nil {
				log.Error("failed get applied fx on sent invoice")
				// TODO old invoice resend may not have applied fx rate, so continue at first step
				continue
			}
			invoices[i].Meta.AppliedFXRate = appliedFXRate
		}
	}

	paymentHandler, err := paymenthandler.GetPaymentHandler().GetProvider(paymentInfo.PaymentType)
	if err != nil {
		return nil, err
	}
	if paymentHandler.EnableTaxCalculation() {
		log.Info("start to retrieve tax")
		taxRate, err := (&TaxModel{}).RetrieveAndUpdateTenantTaxRate(tenantID)
		if err != nil {
			log.Error("failed to retrieve tax rate", zap.Error(err), zap.Uint("tenantID", tenantID))
			if !errors.Is(err, billing_errors.InvalidBillingProfileInputError) {
				return nil, err
			}
		} else {
			for i := 0; i != len(invoices); i++ {
				invoices[i].TaxRate = taxRate
			}
		}
	}
	if paymentInfo.ManagedBySales {
		var payInvoices []forms.Invoice
		for _, invoice := range invoices {
			if invoice.Type == entity.InvoiceType_COMMITMENT {
				payInvoices = append(payInvoices, invoice)
			}

		}
		if len(invoices)-len(payInvoices) > 0 {
			InvoicePayCounter.WithLabelValues(paymentInfo.PaymentType, "managedBySales").Inc()
			log.Info("skip paying because managed by sales", zap.Uint("tenantID", tenantID))
			if len(payInvoices) == 0 {
				ret = &forms.PaymentResult{
					PaymentMethodID: paymentInfo.PaymentMethodID,
					PaidInvoices:    invoices,
				}
				return ret, nil
			}

		}
		invoices = payInvoices
		log.Info("InvoiceType_COMMITMENT ignore  managed by sales", zap.Uint("tenantID", tenantID), zap.Any("invoices", invoices))
	}
	InvoicePayCounter.WithLabelValues(paymentInfo.PaymentType, "init").Inc()

	defer func() {
		if retError != nil {
			if errors.Is(err, billing_errors.FrequentSendMeteringError) {
				InvoicePayCounter.WithLabelValues(paymentInfo.PaymentType, "wait").Inc()
			} else {
				InvoicePayCounter.WithLabelValues(paymentInfo.PaymentType, "false").Inc()
			}
		} else {
			InvoicePayCounter.WithLabelValues(paymentInfo.PaymentType, "true").Inc()
		}
	}()

	var paidInvoices []forms.Invoice
	if paymentHandler.NeedMultiInvoiceSendOnce() {
		paidInvoices, err = paymentHandler.PayMultiInvoice(paymentInfo, billingCompany, invoices)
		if err != nil {
			log.Error("failed to pay invoice", zap.Uint("TenantID", tenantID), zap.String("Type", paymentInfo.PaymentType), zap.String("Code", paymentInfo.PaymentCode), zap.String("SubCode", billingCompany.PaymentSubCode), zap.Error(err))
		}
	} else {
		for i := 0; i != len(invoices); i++ {
			var invoice *forms.Invoice
			invoice, err = paymentHandler.Pay(paymentInfo, billingCompany, &invoices[i])
			if err != nil {
				if !errors.Is(err, billing_errors.FrequentSendMeteringError) {
					log.Error("failed to pay invoice", zap.Uint("tenantID", tenantID), zap.Uint("invoiceID", invoices[i].InvoiceID), zap.Error(err))
				}
				continue
			}
			paidInvoices = append(paidInvoices, *invoice)
		}
	}
	log.Info("success pay invoice", zap.Int("totalInvoices", len(invoices)), zap.Int("paidInvoices", len(paidInvoices)), zap.Uint("TenantID", tenantID), zap.String("Type", paymentInfo.PaymentType), zap.String("Code", paymentInfo.PaymentCode), zap.String("SubCode", billingCompany.PaymentSubCode))
	ret = &forms.PaymentResult{
		PaymentMethodID: paymentInfo.PaymentMethodID,
		PaidInvoices:    paidInvoices,
	}
	for i := 0; i != len(ret.PaidInvoices); i++ {
		if ret.PaidInvoices[i].Status == entity.InvoiceStatus_GENERATED || ret.PaidInvoices[i].Status == entity.InvoiceStatus_CONFIRMED {
			ret.PaidInvoices[i].Status = entity.InvoiceStatus_SENT
		}
		if ret.PaidInvoices[i].PaymentType == "" {
			ret.PaidInvoices[i].PaymentType = paymentInfo.PaymentType
		}
		if ret.PaidInvoices[i].PaymentCode == nil {
			ret.PaidInvoices[i].PaymentCode = &paymentInfo.PaymentCode
		}
	}
	return ret, nil
}

func (m *PaymentModel) Pay(tenantID uint, billingConfigID uint, invoices ...forms.Invoice) (*forms.PaymentResult, error) {
	result, err := m.pay(tenantID, billingConfigID, invoices)
	if err != nil {
		return result, err
	}

	// Publish invoice-paid event for promotion reward
	if result != nil && len(result.PaidInvoices) > 0 {
		var totalPaid decimal.Decimal
		for _, inv := range result.PaidInvoices {
			totalPaid = totalPaid.Add(inv.CostPrice)
		}
		if totalPaid.GreaterThan(decimal.Zero) {
			m.sendInvoicePaidEvent(tenantID, billingConfigID, totalPaid)
		}
	}

	return result, nil
}

func (m *PaymentModel) sendInvoicePaidEvent(tenantID uint, billingConfigID uint, paidAmount decimal.Decimal) {
	err := eventbushandler.Get().PublishEvent(InvoicePaidEventType,
		&eventbushandler.Event{
			EventID:   fmt.Sprint(tenantID),
			EventType: InvoicePaidEventType,
			EventData: forms.InvoicePaidEvent{
				TenantID:        tenantID,
				BillingConfigID: billingConfigID,
				PaidAmount:      paidAmount,
			},
		},
	)
	if err != nil {
		log.Error("failed to send invoice-paid event",
			zap.Uint("tenantID", tenantID),
			zap.String("paidAmount", paidAmount.String()),
			zap.Error(err))
	}
}

func (m *PaymentModel) UpdatePaymentCode(tenantID uint, paymentMethodID uint, code string, subCode *string) (*forms.PaymentMethod, error) {
	num, err := entity.NewPaymentMethodQuerySet(db.GetDB()).TenantIDEq(tenantID).IDEq(paymentMethodID).GetUpdater().SetPaymentCode(code).SetPaymentSubCode(subCode).UpdateNum()
	if err != nil {
		return nil, err
	} else if num == 0 {
		return nil, billing_errors.ObjectNotExistsError
	}
	return m.GetPaymentMethod(paymentMethodID)
}

func (m *PaymentModel) UpdatePaymentMeta(tenantID uint, paymentMethodID uint, meta *forms.PaymentMeta) (*forms.PaymentMethod, error) {
	exec := db.GetDB().Model(&entity.PaymentMethod{}).Where("tenant_id = ? and id = ?", tenantID, paymentMethodID).Update(
		"meta", entity.PaymentMeta(*meta),
	)
	num := exec.RowsAffected
	err := exec.Error
	if err != nil {
		return nil, err
	} else if num == 0 {
		return nil, billing_errors.ObjectNotExistsError
	}
	return m.GetPaymentMethod(paymentMethodID)
}

func (m *PaymentModel) UpdatePaymentInvoiceable(paymentMethodID uint) (*forms.PaymentMethod, error) {
	payment, err := m.GetPaymentMethod(paymentMethodID)
	if err != nil {
		return nil, err
	}
	invoiceable := true
	if !payment.Invoiceable {
		err := entity.NewPaymentMethodQuerySet(db.GetDB()).IDEq(paymentMethodID).InvoiceableEq(false).
			GetUpdater().SetInvoiceable(&invoiceable).SetInvoiceableAt(timeutils.GetNowAddr()).Update()
		if err != nil {
			return nil, err
		}
		return m.GetPaymentMethod(paymentMethodID)
	}
	return payment, nil
}

func (m *PaymentModel) PartnerPaymentProcessOnScheduleActivate(tenantID, billingConfigID uint) error {
	billingConfigModel := &billingconfig.BillingConfigModel{}
	bcfg, err := billingConfigModel.GetBillingConfig(billingConfigID)
	if err != nil {
		return err
	}
	switch bcfg.Type {
	case entity.BillingConfigType_MSPCustomer:
		paymentInfo, err := m.GetPaymentMethodWithPaymentType(tenantID, billingConfigID, entity.PaymentType_MSP)
		if err != nil {
			if !errors.Is(err, billing_errors.ObjectNotExistsError) {
				return err
			}
			paymentInfo, err = m.CreatePaymentMethodAndActive(tenantID, &forms.PaymentMethodCreateQuery{
				PaymentType:           entity.PaymentType_MSP,
				BindedBillingConfigID: &bcfg.BillingConfigID,
			})
			if err != nil {
				log.Error("failed to create payment method on msp customer activated", zap.Uint("tenantID", tenantID), zap.Error(err))
				return err
			}
			log.Info("success to create payment method on msp customer activated", zap.Uint("tenantID", tenantID), zap.Uint("paymentID", paymentInfo.PaymentMethodID))
		} else {
			log.Info("msp tenant have that payment method, try to sync",
				zap.Uint("tenantID", tenantID), zap.String("paymentType", entity.PaymentType_MSP), zap.Uint("paymentID", paymentInfo.PaymentMethodID),
			)
			err = m.syncPaymentMethod(tenantID, paymentInfo.PaymentMethodID)
			if err != nil {
				return err
			}
		}

	case entity.BillingConfigType_MarketplaceResellerManagedCustomer:
		resellerBillingConfig, err := billingConfigModel.GetBillingConfigByManagedTenantID(tenantID)
		if err != nil {
			return err
		}
		cp, err := m.GetActivePaymentMethod(tenantID, bcfg.BillingConfigID)
		if err != nil {
			log.Error("failed to get cppo/mcpo customer payment method", zap.Error(err), zap.Uint("tenantID", tenantID))
			return err
		}
		err = m.syncPaymentMethod(tenantID, cp.PaymentMethodID)
		if err != nil {
			return err
		}
		rp, err := m.GetActivePaymentMethod(resellerBillingConfig.TenantID, resellerBillingConfig.BillingConfigID)
		if err != nil {
			log.Error("failed to get cppo/mcpo customer payment method", zap.Error(err), zap.Uint("tenantID", tenantID))
			return err
		}
		err = m.syncPaymentMethod(resellerBillingConfig.TenantID, rp.PaymentMethodID)
		if err != nil {
			return err
		}
	case entity.BillingConfigType_MSP, entity.BillingConfigType_ResalePartner:
		log.Info("do nothing on MSP / Resale Partner activated", zap.Uint("tenant_id", tenantID))
	case entity.BillingConfigType_MarketplaceReseller:
		log.Info("do nothing on Marketplace Reseller activated", zap.Uint("tenant_id", tenantID))
	default:
		log.Info("not partner tenant, do nothing", zap.Uint("tenant_id", tenantID))
	}
	return nil
}

func (m *PaymentModel) PaymentProcessOnBillingConfigSwitched(billingConfigID uint) error {
	customerBCFG, err := (&billingconfig.BillingConfigModel{}).GetBillingConfig(billingConfigID)
	if err != nil {
		return err
	}
	err = m.PartnerPaymentProcessOnScheduleActivate(customerBCFG.TenantID, billingConfigID)
	if err != nil {
		return err
	}
	log.Info("success to process partner payment")
	err = m.SyncPayableByBillingConfigID(customerBCFG.TenantID, billingConfigID)
	if err != nil {
		return err
	}
	log.Info("success to sync payable on billing config switched", zap.Uint("billingConfigID", billingConfigID), zap.Uint("tenantID", customerBCFG.BillingConfigID))
	err = m.sendPayableEventNoTransaction(customerBCFG.TenantID)
	if err != nil {
		return err
	}
	if customerBCFG.Type == entity.BillingConfigType_MarketplaceResellerManagedCustomer {
		resellerBCFG, err := (&billingconfig.BillingConfigModel{}).GetBillingConfigByManagedTenantID(customerBCFG.TenantID)
		if err != nil {
			return err
		}
		err = m.SyncPayableByBillingConfigID(resellerBCFG.TenantID, resellerBCFG.BillingConfigID)
		if err != nil {
			return err
		}
		err = m.sendPayableEventNoTransaction(resellerBCFG.TenantID)
		if err != nil {
			return err
		}
		// send in-app notification
		m.sendDirectChangeToPartnerNotification(customerBCFG.TenantID, resellerBCFG.TenantID, customerBCFG.Type, customerBCFG.StartActiveTime)
	}

	if customerBCFG.Type == entity.BillingConfigType_MSPCustomer {
		paymentInfo, err := m.GetPaymentMethodWithPaymentType(customerBCFG.TenantID, billingConfigID, entity.PaymentType_MSP)
		if err != nil {
			return err
		}
		_resellerTenantID, err := strconv.ParseUint(paymentInfo.PaymentCode, 10, 0)
		if err != nil {
			return err
		}
		// send in-app notification
		m.sendDirectChangeToPartnerNotification(customerBCFG.TenantID, uint(_resellerTenantID), customerBCFG.Type, customerBCFG.StartActiveTime)
	}
	return nil
}

const TenantRegisteredEventType = "tenant_registered"
const TenantBindThirdPartyEventType = "tenant-bind-thirdparty"
const TenantThirdPartyNormalSubscribedEventType = "tenant-third-party-normal-subscribed"
const TenantThirdPartyNormalUnsubscribed = "tenant-third-party-normal-unsubscribed"

func (m *PaymentModel) RegisterPaymentEventHandler() error {
	group := "payment_method"
	eventHandlerFunc := func(event *eventbushandler.Event) error {
		tenantID := event.EventData.(*forms.TenantID).TenantID
		log.Info("received the event sent by account", zap.Uint("tenantID", tenantID), zap.String("event_type", event.EventType))
		err := m.CreateOrReplacePaymentMethodAfterMarketplaceBind(tenantID)
		if err != nil {
			return err
		}
		// tenant_registered: user signed up in TiDB Cloud
		// TenantThirdPartyNormalSubscribedEventType: user has signed up in TiDB Cloud, and bound to Alicloud before
		// TenantBindThirdPartyEventType: user has signed up in TiDB Cloud, but never bound to Alicloud before
		m.sendMarketplaceSubscriptionChangeNotification(tenantID, true)
		// send payable event for alicloud subscriber if need
		return db.GetDB().Transaction(func(tx *gorm.DB) error {
			return m.sendAliCloudSubscriberBecomePayableEvent(tx, tenantID)
		})
	}
	err := eventbushandler.Get().RegisterEventHandleFunc(TenantRegisteredEventType, group, forms.TenantID{}, eventHandlerFunc)
	if err != nil {
		return err
	}

	err = eventbushandler.Get().RegisterEventHandleFunc(TenantBindThirdPartyEventType, group, forms.TenantID{}, eventHandlerFunc)
	if err != nil {
		return err
	}

	err = eventbushandler.Get().RegisterEventHandleFunc(TenantThirdPartyNormalSubscribedEventType, group, forms.TenantID{}, eventHandlerFunc)
	if err != nil {
		return err
	}

	err = eventbushandler.Get().RegisterEventHandleFunc(TenantThirdPartyNormalUnsubscribed, group, forms.TenantID{}, func(event *eventbushandler.Event) error {
		m.sendMarketplaceSubscriptionChangeNotification(event.EventData.(*forms.TenantID).TenantID, false)
		return m.MarkMarketplacePaymentMethodUnsubscribed(event.EventData.(*forms.TenantID).TenantID)
	})
	if err != nil {
		return err
	}

	err = eventbushandler.Get().RegisterEventHandleFunc(constant.BillingConfigSwitchedEvent, group, forms.BillingConfig{}, func(event *eventbushandler.Event) error {
		bcfg := event.EventData.(*forms.BillingConfig)
		return m.PaymentProcessOnBillingConfigSwitched(bcfg.BillingConfigID)
	})

	if err != nil {
		return err
	}
	return nil
}

func (m *PaymentModel) sendMarketplaceSubscriptionChangeNotification(tenantID uint, isSubscribed bool) {
	notificationEvent := ""
	if isSubscribed {
		notificationEvent = uncutils.MarketplaceSubscribe
	} else {
		notificationEvent = uncutils.MarketplaceUnsubscribe
	}
	// query tenant info
	tenant, err := centralhandler.GetCentralHandler().GetTenant(tenantID)
	if err != nil {
		log.Error("fail to send in-app notification for marketplace subscription change, get tenant info error", zap.Uint("tenantID", tenantID), zap.Error(err))
		return
	}
	var cloudProvider string
	if tenant.ThirdPartyAccountProvider == constant.AwsAccountProvider {
		cloudProvider = "AWS"
	}
	if tenant.ThirdPartyAccountProvider == constant.GCPAccountProvider {
		cloudProvider = "GCP"
	}
	if tenant.ThirdPartyAccountProvider == constant.AliCloudAccountProvider {
		cloudProvider = "AliCloud"
	}
	if tenant.ThirdPartyAccountProvider == constant.AzureAccountProvider {
		cloudProvider = "Azure"
	}
	// only send notification when tenant bind to third party account
	if cloudProvider != "" {
		uncutils.SendInAppNotification(tenantID, notificationEvent, map[string]string{
			"cloudProvider": cloudProvider,
			"orgName":       tenant.Name,
		})
	}
}

func (m *PaymentModel) sendDirectChangeToPartnerNotification(tenantID uint, resellerTenantID uint, billingConfigType string, startDateTime *time.Time) {
	notificationEvent := ""
	if billingConfigType == entity.BillingConfigType_MarketplaceResellerManagedCustomer {
		notificationEvent = uncutils.DirectToCPPOCustomer
	}
	if billingConfigType == entity.BillingConfigType_MSPCustomer {
		notificationEvent = uncutils.DirectToMSPCustomer
	}
	// query customer tenant info
	tenant, err := centralhandler.GetCentralHandler().GetTenant(tenantID)
	if err != nil {
		log.Error("fail to send in-app notification for direct customer change to partner customer, get tenant info error", zap.Uint("tenantID", tenantID), zap.String("billingConfigType", billingConfigType), zap.Error(err))
		return
	}
	// query reseller tenant info
	resellerTenant, err := centralhandler.GetCentralHandler().GetTenant(resellerTenantID)
	if err != nil {
		log.Error("fail to send in-app notification for direct customer change to partner customer, get reseller tenant info error", zap.Uint("resellerTenantID", resellerTenantID), zap.String("billingConfigType", billingConfigType), zap.Error(err))
		return
	}
	uncutils.SendInAppNotification(tenantID, notificationEvent, map[string]string{
		"orgName":            tenant.Name,
		"channelPartnerName": resellerTenant.Name,
		"startDateTime":      startDateTime.Format("2006-01-02 15:04:05 UTC±00:00"),
		"orgId":              fmt.Sprintf("%d", tenantID),
	})
}

func (m *PaymentModel) sendAliCloudSubscriberBecomePayableEvent(tx *gorm.DB, tenantID uint) error {
	paymentMethod, err := m.GetPaymentMethodWithPaymentTypeCurrentBillingConfig(tenantID, entity.PaymentType_AliCloud)
	if err != nil {
		if !errors.Is(err, billing_errors.ObjectNotExistsError) {
			log.Error("failed to send payable event for alicloud", zap.Uint("tenantID", tenantID), zap.Error(err))
			return err
		}
	} else {
		if !paymentMethod.Payable {
			log.Info("no need to send payable event because alicloud subscriber is not payable", zap.Uint("tenantID", tenantID))
			return nil
		}
		err = eventbushandler.Get().PublishEventWithTransaction(AliCloudSubscriberBecomePayableEventType,
			&eventbushandler.Event{
				EventID:   fmt.Sprint(tenantID),
				EventType: AliCloudSubscriberBecomePayableEventType,
				EventData: forms.TenantID{
					TenantID: tenantID,
				},
			},
			tx,
		)
		if err != nil {
			log.Error("failed to send event when alicloud subscriber become payable", zap.Uint("tenantID", tenantID), zap.Error(err))
			return err
		}
		return nil
	}
	return nil
}
