package payment

import (
	"errors"
	"fmt"
	"slices"
	"sort"
	"strconv"
	"strings"
	"time"

	"github.com/pingcap/dbaas/billing2/billing_errors"
	"github.com/pingcap/dbaas/billing2/config"
	"github.com/pingcap/dbaas/billing2/db"
	"github.com/pingcap/dbaas/billing2/db/entity"
	"github.com/pingcap/dbaas/billing2/forms"
	"github.com/pingcap/dbaas/billing2/handler/centralhandler"
	"github.com/pingcap/dbaas/billing2/handler/eventbushandler"
	"github.com/pingcap/dbaas/billing2/handler/mailhandler"
	"github.com/pingcap/dbaas/billing2/model/billing"
	billingsnapshot "github.com/pingcap/dbaas/billing2/model/billing/snapshot"
	"github.com/pingcap/dbaas/billing2/model/billingconfig"
	"github.com/pingcap/dbaas/billing2/model/costexplorer"
	"github.com/pingcap/dbaas/billing2/model/marketing"
	"github.com/pingcap/dbaas/billing2/model/payment/paymentsub"
	"github.com/pingcap/dbaas/billing2/utils/errorutils"
	"github.com/pingcap/dbaas/billing2/utils/log"
	"github.com/pingcap/dbaas/billing2/utils/pagination"
	"github.com/pingcap/dbaas/billing2/utils/timeutils"
	"github.com/pingcap/dbaas/billing2/utils/user"
	"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 InvoiceModel struct {
}

var (
	InvoiceCounter = promauto.NewCounterVec(prometheus.CounterOpts{
		Namespace: "dbaas",
		Subsystem: "billing",
		Name:      "invoice",
	}, []string{"invoiceType", "status"})

	InvoiceSendCounter = promauto.NewCounter(prometheus.CounterOpts{
		Namespace: "dbaas",
		Subsystem: "billing",
		Name:      "invoice_send",
	})
)

func invoiceFromEntity(invoiceEntity *entity.Invoice) *forms.Invoice {
	billingConfigID := invoiceEntity.BindedBillingConfigID
	result := &forms.Invoice{
		Costs: forms.Costs{
			CategoryCosts: forms.CategoryCosts{
				Price: forms.Price{
					CostPrice: invoiceEntity.CostPrice,
					RawPrice:  invoiceEntity.RawPrice,
				},
			},
		},
		InvoiceID:             invoiceEntity.ID,
		TenantID:              invoiceEntity.TenantID,
		Status:                invoiceEntity.Status,
		StartDate:             invoiceEntity.InvoiceDate,
		Type:                  invoiceEntity.Type,
		SentDate:              invoiceEntity.SentDate,
		PaymentMethodID:       invoiceEntity.PaymentMethodID,
		BindedBillingConfigID: &billingConfigID,
		ChannelLoss:           invoiceEntity.ChannelLoss,
		PaymentCode:           invoiceEntity.PaymentCode,
	}
	if invoiceEntity.PaymentType != nil {
		result.PaymentType = *invoiceEntity.PaymentType
	} else if invoiceEntity.PaymentMethodID != nil {
		p, err := (&paymentsub.PaymentModel{}).GetPaymentMethodWithCache(*invoiceEntity.PaymentMethodID)
		if err != nil {
			log.Error("can not found the payment method of invoice", zap.Uint("invoiceID", invoiceEntity.ID), zap.Uint("paymentMethodID", *invoiceEntity.PaymentMethodID))
		} else {
			result.PaymentType = p.PaymentType
		}
	} else {
		p, err := (&paymentsub.PaymentModel{}).GetActivePaymentMethodWithCache(invoiceEntity.TenantID, invoiceEntity.BindedBillingConfigID)
		if err != nil {
			log.Error("can not found the active payment method of tenant for invoice", zap.Uint("tenantID", invoiceEntity.TenantID), zap.Uint("invoiceID", invoiceEntity.ID))
		} else {
			result.PaymentType = p.PaymentType
		}
	}
	if invoiceEntity.Meta != nil {
		result.Meta = forms.InvoiceMeta(*invoiceEntity.Meta)
	}
	switch invoiceEntity.Type {
	case entity.InvoiceType_MONTHLY:
		result.EndDate = result.StartDate.AddDate(0, 1, 0)
	case entity.InvoiceType_COSTQUOTA:
		result.EndDate = result.StartDate.AddDate(0, 1, 0)
	case entity.InvoiceType_HOURLY:
		result.EndDate = result.StartDate.Add(time.Hour)
	default:
		result.EndDate = result.StartDate
	}
	return result
}

func invoiceFromEntityWithCosts(invoiceEntity *entity.Invoice) (*forms.Invoice, error) {
	var err error
	invoice := invoiceFromEntity(invoiceEntity)
	if err != nil {
		return nil, err
	}

	if invoiceEntity.Type != entity.InvoiceType_COMMITMENT && invoice.Type != entity.InvoiceType_SUBORDINATED {
		costModel := costexplorer.CostExplorerModel{}
		costs, err := costModel.GetInvoiceCosts(invoiceEntity.TenantID, invoiceEntity.BindedBillingConfigID, invoiceEntity.ID, false)
		if err != nil {
			return nil, err
		}
		invoice.Costs = *costs
	}

	if invoiceEntity.TaxRateID != nil {
		invoice.TaxRate, err = (&paymentsub.TaxModel{}).GetTaxRate(*invoiceEntity.TaxRateID)
		if err != nil {
			return nil, err
		}
	} else if invoice.Status == entity.InvoiceStatus_GENERATED || invoice.Status == entity.InvoiceStatus_CONFIRMED {
		billingInfo, err := (&paymentsub.BillingInformationModel{}).GetBillingInfo(invoiceEntity.TenantID)
		if err != nil {
			if !errors.Is(err, billing_errors.ObjectNotExistsError) {
				return nil, err
			}
		} else {
			invoice.TaxRate = billingInfo.TaxRate
		}
	}

	return invoice, nil
}

func (i *InvoiceModel) ListSentInvoicesByPaymentType(year, month int, paymentType string) ([]forms.Invoice, error) {
	start := timeutils.GetMonthDateByYearMonth(year, month)
	fs := func(query entity.InvoiceQuerySet) entity.InvoiceQuerySet {
		return query.PaymentCodeEq(paymentType).InvoiceDateGte(start).InvoiceDateLt(start.AddDate(0, 1, 0)).StatusEq(entity.InvoiceStatus_SENT)
	}
	return i.listInvoices(true, fs)
}

func (*InvoiceModel) listInvoices(brief bool, fs ...func(entity.InvoiceQuerySet) entity.InvoiceQuerySet) ([]forms.Invoice, error) {
	var invoices []entity.Invoice
	query := entity.NewInvoiceQuerySet(db.GetDB()).OrderDescByInvoiceDate().OrderDescByCreatedAt()

	for _, f := range fs {
		query = f(query)
	}
	err := query.All(&invoices)
	if err != nil {
		return nil, err
	}
	infos := make([]forms.Invoice, len(invoices))
	for i, invoice := range invoices {
		var invoiceInfo *forms.Invoice
		if brief {
			invoiceInfo = invoiceFromEntity(&invoice)
		} else {
			invoiceInfo, err = invoiceFromEntityWithCosts(&invoice)
			if err != nil {
				return nil, err
			}
		}
		invoiceInfo.Brief = brief
		infos[i] = *invoiceInfo
	}
	return infos, nil
}

func (i *InvoiceModel) ListInvoicesListInvoicesForOperation(brief bool, from time.Time, to time.Time, statuses []string, activePayment string, filterTenantIDs []uint) ([]forms.InvoiceForOpsList, error) {
	paymentModel := paymentsub.PaymentModel{}
	payments, err := paymentModel.GetAllActivePaymentsWithType(entity.PaymentType_AWS, entity.PaymentType_GCP, entity.PaymentType_MSP, entity.PaymentType_AWSCPPOTransfer, entity.PaymentType_AZURE)
	if err != nil {
		return nil, err
	}
	paymentIDs := make([]uint, len(payments))
	for index, p := range payments {
		paymentIDs[index] = p.PaymentMethodID
	}

	var activePaymentTenantIDs []uint
	if activePayment != "" {
		activePayments, err := paymentModel.GetAllActivePaymentsWithType(activePayment)
		if err != nil {
			return nil, err
		}
		if len(activePayments) == 0 {
			return []forms.InvoiceForOpsList{}, nil
		}
		activePaymentTenantIDs = make([]uint, 0, len(activePayments))
		seen := make(map[uint]struct{}, len(activePayments))
		for _, p := range activePayments {
			if _, ok := seen[p.TenantID]; ok {
				continue
			}
			seen[p.TenantID] = struct{}{}
			activePaymentTenantIDs = append(activePaymentTenantIDs, p.TenantID)
		}
	}
	// billedStatuses is the set of real pay-or-paying status values that
	// should be visible in /invoices. We deliberately exclude the
	// pre-generate / lifecycle-internal states (inited, split) because
	// they are intermediate artifacts the operator never needs to act
	// on. Every carry-state status is included: below_threshold_carry
	// (sitting in the carryover pool awaiting the next month's pay run),
	// expired_carryover (TTL-expired and no longer actionable but still
	// subject to finance reconciliation). Operators need per-invoice
	// visibility into both — the carryover_summary block only surfaces
	// aggregate counts. Phase 3 NULL-tolerance (subquery 2's pm_id IS
	// NULL branch) lets pre-Phase-2 / legacy rows whose pm_id is NULL
	// hit subquery 2 without the marketplace set membership blocking
	// them.
	billedStatuses := []string{
		entity.InvoiceStatus_CONFIRMED,
		entity.InvoiceStatus_SENT,
		entity.InvoiceStatus_PAID,
		entity.InvoiceStatus_SUSPENDED,
		entity.InvoiceStatus_BELOW_THRESHOLD_CARRY,
		entity.InvoiceStatus_EXPIRED_CARRYOVER,
	}

	applyFilters := func(set entity.InvoiceQuerySet) entity.InvoiceQuerySet {
		if len(statuses) != 0 {
			set = set.StatusIn(statuses...)
		}
		if len(filterTenantIDs) != 0 {
			set = set.TenantIDIn(filterTenantIDs...)
		}
		if len(activePaymentTenantIDs) != 0 {
			set = set.TenantIDIn(activePaymentTenantIDs...)
		}
		return set.InvoiceDateGte(from).InvoiceDateLt(to)
	}
	infos0, err := i.listInvoices(brief, func(set entity.InvoiceQuerySet) entity.InvoiceQuerySet {
		return applyFilters(set).
			StatusEq(entity.InvoiceStatus_GENERATED).
			TypeNotIn(entity.InvoiceType_HOURLY, entity.InvoiceType_DAILY, entity.InvoiceType_INMONTH)
	})
	if err != nil {
		return nil, err
	}
	// subquery 2 — non-marketplace-paid invoices (Stripe / wire / alicloud /
	// resale / etc). Phase 3 NULL-tolerance: previously this query used
	// `payment_method_id NOT IN (aws_pm_set)` which evaluates to NULL/false
	// for any invoice whose pm_id is NULL, silently dropping legitimate SENT
	// rows whose payment_method_id was never written (legacy data, or pre-
	// Phase-2 Stripe paying worker output). We accept NULL pm_id here on
	// the principle that the empty set isn't "marketplace", and fall back
	// to "show it anyway" rather than hiding it.
	//
	// Implementation note: gorm-generated querySet chains filter clauses
	// with AND, so we cannot express `pm_id NOT IN (set) OR pm_id IS NULL`
	// as a single chained call. Split into two listInvoices invocations
	// (one matched on PaymentMethodIDNotIn, one on PaymentMethodIDIsNull)
	// and concat — the two result sets are disjoint because `x NOT IN (s)`
	// for x = NULL evaluates to NULL/false, so NULL rows never match the
	// first branch.
	infos1a, err := i.listInvoices(brief, func(set entity.InvoiceQuerySet) entity.InvoiceQuerySet {
		return applyFilters(set).
			StatusIn(billedStatuses...).
			PaymentMethodIDNotIn(paymentIDs...).
			TypeNotIn(entity.InvoiceType_HOURLY, entity.InvoiceType_DAILY, entity.InvoiceType_INMONTH)
	})
	if err != nil {
		return nil, err
	}
	infos1b, err := i.listInvoices(brief, func(set entity.InvoiceQuerySet) entity.InvoiceQuerySet {
		return applyFilters(set).
			StatusIn(billedStatuses...).
			PaymentMethodIDIsNull().
			TypeNotIn(entity.InvoiceType_HOURLY, entity.InvoiceType_DAILY, entity.InvoiceType_INMONTH)
	})
	if err != nil {
		return nil, err
	}
	infos1 := make([]forms.Invoice, 0, len(infos1a)+len(infos1b))
	infos1 = append(infos1, infos1a...)
	infos1 = append(infos1, infos1b...)
	// subquery 3 — marketplace-paid commitment invoices. This branch
	// deliberately keeps a strict `payment_method_id IN (marketplace_set)`
	// check: a commitment-invoice row with NULL pm_id shouldn't be inferred
	// as marketplace-paid, the operator should reconcile its payment record
	// first.
	infos2, err := i.listInvoices(brief, func(set entity.InvoiceQuerySet) entity.InvoiceQuerySet {
		return applyFilters(set).
			StatusIn(billedStatuses...).
			PaymentMethodIDIn(paymentIDs...).
			TypeIn(entity.InvoiceType_COMMITMENT)
	})
	if err != nil {
		return nil, err
	}
	i1, i2 := 0, 0
	ret := make([]forms.InvoiceForOpsList, len(infos0)+len(infos1)+len(infos2))
	tenantIDMap := map[uint]*forms.Tenant{}
	for i0 := 0; i0 != len(infos0); i0++ {
		ret[i0].Invoice = infos0[i0]
		tenantIDMap[ret[i0].TenantID] = nil
	}
	for i0 := len(infos0); i0 != len(ret); i0++ {
		if i2 >= len(infos2) {
			ret[i0].Invoice = infos1[i1]
			i1++
		} else if i1 >= len(infos1) || infos1[i1].StartDate.Before(infos2[i2].StartDate) {
			ret[i0].Invoice = infos2[i2]
			i2++
		} else {
			ret[i0].Invoice = infos1[i1]
			i1++
		}
		tenantIDMap[ret[i0].TenantID] = nil
	}
	// Sort by status first (generated → below_threshold_carry → sent →
	// suspended → other), then by invoice_date ascending within the same
	// status. This gives ops a predictable view: pending (generated) at top,
	// then in-progress (below_threshold), then completed (sent), then parked
	// (suspended).
	statusOrder := map[string]int{
		entity.InvoiceStatus_GENERATED:             0,
		entity.InvoiceStatus_BELOW_THRESHOLD_CARRY: 1,
		entity.InvoiceStatus_EXPIRED_CARRYOVER:     2,
		entity.InvoiceStatus_SENT:                  3,
		entity.InvoiceStatus_SUSPENDED:             4,
		entity.InvoiceStatus_CONFIRMED:             5,
		entity.InvoiceStatus_PAID:                  6,
	}
	sort.SliceStable(ret, func(i, j int) bool {
		si, oki := statusOrder[ret[i].Invoice.Status]
		sj, okj := statusOrder[ret[j].Invoice.Status]
		if !oki {
			si = 99
		}
		if !okj {
			sj = 99
		}
		if si != sj {
			return si < sj
		}
		return ret[i].Invoice.StartDate.Before(ret[j].Invoice.StartDate)
	})
	queryTenantIDs := make([]uint, len(tenantIDMap))
	index := 0
	for t, _ := range tenantIDMap {
		queryTenantIDs[index] = t
		index++
	}
	tenants, err := centralhandler.GetCentralHandler().GetTenants(queryTenantIDs...)
	if err != nil {
		log.Error("failed to get tenant info", zap.Uints("tenantIDs", queryTenantIDs))
		return nil, err
	}
	for i := 0; i != len(tenants); i++ {
		tenantIDMap[tenants[i].TenantID] = &tenants[i]
	}
	if config.Get().GetString(config.ENV) == "prod" {
		filtered := make([]forms.InvoiceForOpsList, 0, len(ret))
		for i := 0; i < len(ret); i++ {
			if tenant, ok := tenantIDMap[ret[i].TenantID]; ok && tenant != nil && tenant.Kind == forms.TenantKindInternal {
				log.Error("internal tenant is not allowed in prod environment", zap.Uint("tenant_id", ret[i].TenantID), zap.String("tenant_name", tenant.Name))
				continue
			}
			filtered = append(filtered, ret[i])
		}
		ret = filtered
	}
	for i := 0; i != len(ret); i++ {
		if tenant, ok := tenantIDMap[ret[i].TenantID]; ok {
			ret[i].TenantName = tenant.Name
		}
		ret[i].From = from.Format("2006-01-02T15:04:05Z")
		ret[i].To = to.Format("2006-01-02T15:04:05Z")
	}
	return ret, nil
}

func (i *InvoiceModel) ListTenantInvoices(tenantID uint, billingConfigID uint, brief bool, fs ...func(entity.InvoiceQuerySet) entity.InvoiceQuerySet) ([]forms.Invoice, error) {
	newFs := append(fs, func(set entity.InvoiceQuerySet) entity.InvoiceQuerySet {
		return set.TenantIDEq(tenantID).BindedBillingConfigIDEq(billingConfigID)
	})
	return i.listInvoices(brief, newFs...)
}

func (i *InvoiceModel) ListAllInvoices(tenantID uint, billingConfigID uint, brief bool) ([]forms.Invoice, error) {
	return i.ListTenantInvoices(tenantID, billingConfigID, brief)
}

func (i *InvoiceModel) ListAllInvoicesWithType(tenantID uint, billingConfigID uint, brief bool, invoiceTypes ...string) ([]forms.Invoice, error) {
	return i.ListTenantInvoices(tenantID, billingConfigID, brief, func(q entity.InvoiceQuerySet) entity.InvoiceQuerySet { return q.TypeIn(invoiceTypes...) })
}

func (i *InvoiceModel) ListAllInvoicesWithoutType(tenantID uint, billingConfigID uint, brief bool, invoiceTypes ...string) ([]forms.Invoice, error) {
	return i.ListTenantInvoices(tenantID, billingConfigID, brief, func(q entity.InvoiceQuerySet) entity.InvoiceQuerySet { return q.TypeNotIn(invoiceTypes...) })
}

func (*InvoiceModel) ListAllInvoicesWithStatus(tenantID uint, status string, billingConfigID uint) ([]forms.Invoice, error) {
	invoices := []entity.Invoice{}
	err := errorutils.WrapSQLError(entity.NewInvoiceQuerySet(db.GetDB()).TenantIDEq(tenantID).BindedBillingConfigIDEq(billingConfigID).
		StatusEq(status).OrderDescByInvoiceDate().All(&invoices), fmt.Sprintf("tenantID %d , status %s", tenantID, status))

	if err != nil {
		return nil, err
	}
	infos := make([]forms.Invoice, len(invoices))
	for i, invoice := range invoices {
		invoiceInfo, err := invoiceFromEntityWithCosts(&invoice)
		if err != nil {
			return nil, err
		}
		infos[i] = *invoiceInfo
	}
	return infos, nil
}

func (*InvoiceModel) GetInvoice(invoiceID uint) (*forms.Invoice, error) {
	invoice := &entity.Invoice{}
	err := errorutils.WrapSQLError(entity.NewInvoiceQuerySet(db.GetDB().Preload(clause.Associations)).IDEq(invoiceID).One(invoice), fmt.Sprintf("invoice %d", invoiceID))

	if err != nil {
		return nil, err
	}
	info, err := invoiceFromEntityWithCosts(invoice)
	if err != nil {
		return nil, err
	}
	return info, nil
}

func (*InvoiceModel) GetTenantInvoice(tenantID, billingConfigID, invoiceID uint) (*forms.Invoice, error) {
	invoice := &entity.Invoice{}
	err := errorutils.WrapSQLError(entity.NewInvoiceQuerySet(db.GetDB().Preload(clause.Associations)).TenantIDEq(tenantID).BindedBillingConfigIDEq(billingConfigID).IDEq(invoiceID).One(invoice), fmt.Sprintf("invoice %d", invoiceID))

	if err != nil {
		return nil, err
	}
	info, err := invoiceFromEntityWithCosts(invoice)
	if err != nil {
		return nil, err
	}
	return info, nil
}

func (i *InvoiceModel) GetLastSendInvoiceDate(paymentIDs []uint) (time.Time, error) {
	var invoices []entity.Invoice
	err := entity.NewInvoiceQuerySet(db.GetDB().Preload(clause.Associations)).PaymentMethodIDIn(paymentIDs...).StatusEq(entity.InvoiceStatus_SENT).OrderDescBySentDate().Limit(1).All(&invoices)
	if err != nil {
		return time.Time{}, err
	}
	if len(invoices) == 0 {
		return time.Time{}, nil
	}
	return *invoices[0].SentDate, nil
}

func (i *InvoiceModel) GetLastSendInvoiceDateByPaymentCode(paymentType, paymentCode string) (time.Time, error) {
	var invoices []entity.Invoice
	err := entity.NewInvoiceQuerySet(db.GetDB().Preload(clause.Associations)).PaymentTypeEq(paymentType).PaymentCodeEq(paymentCode).StatusEq(entity.InvoiceStatus_SENT).OrderDescBySentDate().Limit(1).All(&invoices)
	if err != nil {
		return time.Time{}, err
	}
	if len(invoices) == 0 {
		return time.Time{}, nil
	}
	return *invoices[0].SentDate, nil
}

func (i *InvoiceModel) SuspendInvoice(invoiceID uint) (*forms.Invoice, error) {
	info, err := i.GetInvoice(invoiceID)
	if err != nil {
		return nil, err
	}
	if info.Status == entity.InvoiceStatus_SUSPENDED {
		// already suspended, do nothing
		return info, nil
	}
	if info.Status != entity.InvoiceStatus_GENERATED {
		return nil, billing_errors.NewInvalidInputError(errors.New("only invoice in generated can be suspended"))
	}
	actionEntity := &entity.AuditRecord{
		Action:    entity.AuditRecordActionSuspendInvoice,
		TenantID:  info.TenantID,
		UserID:    user.GetCurrentUserID(),
		ExtraInfo: fmt.Sprintf(`{"invoice_id": %d}`, invoiceID),
		UserEmail: user.GetCurrentUserEmail(),
	}
	err = db.GetDB().Transaction(func(tx *gorm.DB) error {
		err = actionEntity.Create(tx)
		if err != nil {
			return err
		}
		err = entity.NewInvoiceQuerySet(db.GetDB()).IDEq(invoiceID).GetUpdater().SetStatus(entity.InvoiceStatus_SUSPENDED).Update()
		if err != nil {
			return err
		}
		info.Status = entity.InvoiceStatus_SUSPENDED
		return nil
	})
	if err != nil {
		return nil, err
	}
	return info, nil
}

func (i *InvoiceModel) ConfirmInvoice(invoiceID uint) (*forms.Invoice, error) {
	info, err := i.GetInvoice(invoiceID)
	if err != nil {
		return nil, err
	}
	if info.Status == entity.InvoiceStatus_SENT || info.Status == entity.InvoiceStatus_PAID || info.Status == entity.InvoiceStatus_CONFIRMED {
		// already confirmed, do nothing
		return info, nil
	}
	if info.Status != entity.InvoiceStatus_GENERATED {
		return nil, billing_errors.NewInvalidInputError(errors.New("only invoice in generated need confirm"))
	}
	actionEntity := &entity.AuditRecord{
		Action:    entity.AuditRecordActionConfirmInvoice,
		TenantID:  info.TenantID,
		UserID:    user.GetCurrentUserID(),
		ExtraInfo: fmt.Sprintf(`{"invoice_id": %d}`, invoiceID),
		UserEmail: user.GetCurrentUserEmail(),
	}
	err = db.GetDB().Transaction(func(tx *gorm.DB) error {
		err = actionEntity.Create(tx)
		if err != nil {
			return err
		}
		err = entity.NewInvoiceQuerySet(db.GetDB()).IDEq(invoiceID).GetUpdater().SetStatus(entity.InvoiceStatus_CONFIRMED).Update()
		if err != nil {
			return err
		}
		info.Status = entity.InvoiceStatus_CONFIRMED
		return nil
	})
	if err != nil {
		return nil, err
	}
	return info, nil
}

// ConfirmAndPayInvoice flips a single invoice from GENERATED straight to
// SENT (skips the wait-for-batch-tick the old `ConfirmInvoice` left
// behind). It honors the same preconditions as sendInvoice but with the
// audit + status transitions folded into one synchronous call so the
// caller observes the final state on the same HTTP round-trip.
//
// Routing decisions, in order:
//
//	terminal row (sent/paid)               -> noop, return current state
//	managed-by-sales + non-commitment      -> SUSPEND (Phase 4 parity with
//	                                         the Stripe paying worker; sales
//	                                         bills the customer out of band)
//	otherwise                              -> GENERATED -> CONFIRMED ->
//	                                         SENT via sendInvoice (same
//	                                         paymentModel.Pay path the worker
//	                                         uses, just triggered inline)
//
// On the inline pay branch, sendInvoice returns the updated invoice. On a
// downstream Pay failure the row stays in CONFIRMED so the next
// SendAllUnsentInvoice worker tick retries it; the error is surfaced so
// the API caller sees a 4xx/5xx with the failure reason.
//
// The batch worker (SendAllUnsentInvoice) is unchanged — it remains the
// catch-up path for confirmations that error out, plus the existing
// flow for /confirm endpoints not migrated to this method.
func (i *InvoiceModel) ConfirmAndPayInvoice(invoiceID uint) (*forms.Invoice, error) {
	info, err := i.GetInvoice(invoiceID)
	if err != nil {
		return nil, err
	}

	// Already terminal — don't fight a done deal.
	if info.Status == entity.InvoiceStatus_SENT || info.Status == entity.InvoiceStatus_PAID {
		return info, nil
	}

	// Sanity-gate: only generated rows can be confirmed.
	if info.Status != entity.InvoiceStatus_GENERATED {
		return nil, billing_errors.NewInvalidInputError(errors.New("only invoice in generated state can be confirmed"))
	}
	if info.BindedBillingConfigID == nil {
		return nil, billing_errors.NewInvalidInputError(errors.New("invoice has no binded_billing_config_id"))
	}

	// Phase 4 parity with the Stripe paying worker: if the active PM is
	// managed_by_sales and the type is not COMMITMENT, sales handles this
	// out of band. Skip the push, park the row in SUSPENDED.
	paymentModel := &paymentsub.PaymentModel{}
	pm, pmErr := paymentModel.GetActivePaymentMethod(info.TenantID, *info.BindedBillingConfigID)
	if pmErr != nil {
		return nil, fmt.Errorf("resolve active pm for direct-pay check: %w", pmErr)
	}
	if pm != nil && pm.ManagedBySales && info.Type != entity.InvoiceType_COMMITMENT {
		if err := i.BatchUpdateInvoicesToSuspended([]uint{uint(info.InvoiceID)}); err != nil {
			return nil, fmt.Errorf("suspend managed-by-sales non-commitment single-invoice direct-pay: %w", err)
		}
		info.Status = entity.InvoiceStatus_SUSPENDED
		log.Info("direct-pay parked single invoice in SUSPENDED via Phase 4 managed-by-sales route",
			zap.Uint("invoiceID", info.InvoiceID),
			zap.Uint("tenantID", info.TenantID),
			zap.Uint("billingConfigID", *info.BindedBillingConfigID),
			zap.String("type", info.Type))
		return info, nil
	}

	// Audit insert + status flip in one tx so a crash between the two
	// leaves no orphan unaudited confirmed row. Both writes use the same
	// *gorm.DB so they roll back together; previously the status update
	// used db.GetDB() instead of tx, breaking atomicity (an orphan audit
	// could land while the status update failed or got rolled back,
	// leaving the row inconsistent with its audit trail).
	actionEntity := &entity.AuditRecord{
		Action:    entity.AuditRecordActionConfirmInvoice,
		TenantID:  info.TenantID,
		UserID:    user.GetCurrentUserID(),
		ExtraInfo: fmt.Sprintf(`{"invoice_id": %d, "direct_pay": true}`, invoiceID),
		UserEmail: user.GetCurrentUserEmail(),
	}
	err = db.GetDB().Transaction(func(tx *gorm.DB) error {
		if err := actionEntity.Create(tx); err != nil {
			return err
		}
		// Guard the flip with a status condition: only rows still in
		// 'generated' may be confirmed. A concurrent paying worker may have
		// already pushed this row to SENT between the read at the top of
		// ConfirmAndPayInvoice and this write — the conditional UPDATE then
		// matches 0 rows and leaves the row untouched instead of clobbering
		// the SENT state (and its payment_method_id / stripe_invoice_id).
		if err := tx.Model(&entity.Invoice{}).
			Where("id = ? AND status = ?", invoiceID, entity.InvoiceStatus_GENERATED).
			Update("status", entity.InvoiceStatus_CONFIRMED).Error; err != nil {
			return err
		}
		info.Status = entity.InvoiceStatus_CONFIRMED
		return nil
	})
	if err != nil {
		return nil, err
	}

	// Inline single-invoice pay. sendInvoice drives PaymentModel.Pay
	// (multi-provider: stripe / wire / alicloud / resale) and on success
	// calls UpdateSentInvoices to flip to SENT with sent_date /
	// payment_method_id / payment_type / payment_code.
	//
	// On Pay failure the row stays CONFIRMED and the next
	// SendAllUnsentInvoice worker tick picks it up — same recovery
	// semantics as a separately-confirmed invoice that the worker
	// failed to send.
	return i.sendInvoice(invoiceID)
}

func (i *InvoiceModel) sendInvoice(invoiceID uint) (*forms.Invoice, error) {
	InvoiceSendCounter.Inc()
	info, err := i.GetInvoice(invoiceID)
	if err != nil {
		return nil, err
	}
	if info.Status == entity.InvoiceStatus_SENT || info.Status == entity.InvoiceStatus_PAID {
		// already sent, do nothing
		return info, nil
	}

	if !(info.Status == entity.InvoiceStatus_GENERATED || info.Status == entity.InvoiceStatus_CONFIRMED) {
		return nil, billing_errors.NewInvalidInputError(fmt.Errorf("invoice status error:%s", info.Status))
	}

	if info.Status == entity.InvoiceStatus_GENERATED && info.Type == entity.InvoiceType_MONTHLY {
		return nil, billing_errors.NewInvoiceNotConfirmedError(nil)
	}

	paymentModel := &paymentsub.PaymentModel{}
	invoiceable, err := paymentModel.BillingConfigInvoiceable(info.TenantID, *info.BindedBillingConfigID)
	if err != nil {
		return nil, err
	}
	if !invoiceable {
		log.Info("payment is uninvoiceable, skip paying", zap.Uint("tenantID", info.InvoiceID), zap.Uint("billingConfigID", *info.BindedBillingConfigID))
		return info, nil
	}

	result, err := paymentModel.Pay(info.TenantID, *info.BindedBillingConfigID, *info)
	if err != nil {
		log.Error("failed to pay invoice", zap.Uint("TenantID", info.TenantID), zap.Uint("InvoiceID", info.InvoiceID), zap.Error(err))
		return nil, err
	}
	log.Info("success pay invoice", zap.Uint("TenantID", info.TenantID), zap.Uint("InvoiceID", info.InvoiceID))
	err = i.UpdateSentInvoices(result)
	if err != nil {
		return nil, err
	}
	return i.GetInvoice(invoiceID)
}

func (i *InvoiceModel) sendInvoices(tenantID uint, billingConfigID uint, needSendInvoices []forms.Invoice) error {
	var invoiceIDs []uint
	for _, invoice := range needSendInvoices {
		invoiceIDs = append(invoiceIDs, invoice.InvoiceID)
	}
	paymentModel := &paymentsub.PaymentModel{}
	invoiceable, err := paymentModel.BillingConfigInvoiceable(tenantID, billingConfigID)
	if err != nil {
		return err
	}
	if !invoiceable {
		log.Info("payment is uninvoiceable, skip paying", zap.Uint("tenantID", tenantID), zap.Uint("billingConfigID", billingConfigID))
		return nil
	}
	result, err := paymentModel.Pay(tenantID, billingConfigID, needSendInvoices...)
	if err != nil {
		log.Error("failed to pay invoice", zap.Uint("TenantID", tenantID), zap.Uints("invoiceIDs", invoiceIDs), zap.Error(err))
		return err
	}
	log.Info("success pay multi invoice", zap.Uint("TenantID", tenantID), zap.Uints("invoiceIDs", invoiceIDs))

	err = i.UpdateSentInvoices(result)
	if err != nil {
		log.Error("failed to update invoice status", zap.Uint("TenantID", tenantID), zap.Uints("invoiceIDs", invoiceIDs), zap.Error(err))
		return err
	}
	return nil
}

func (i *InvoiceModel) SendAllUnsentInvoice(tenantID uint, billingConfigID uint) error {
	needSendInvoices, err := i.ListAllInvoicesWithStatus(tenantID, entity.InvoiceStatus_CONFIRMED, billingConfigID)
	if err != nil {
		log.Error("failed to query needSendInvoice", zap.Uint("tenantID", tenantID), zap.Error(err))
		return err
	}
	return i.sendInvoices(tenantID, billingConfigID, needSendInvoices)
}

func (i *InvoiceModel) SendInvoice(tenantID, billingConfigID, invoiceID uint) (*forms.Invoice, error) {
	inv, err := i.GetTenantInvoice(tenantID, billingConfigID, invoiceID)
	if err != nil {
		return nil, err
	}
	return i.sendInvoice(inv.InvoiceID)
}

func (i *InvoiceModel) UpdateSentInvoices(result *forms.PaymentResult) error {
	now := timeutils.GetNow()
	return db.GetDB().Transaction(func(tx *gorm.DB) error {
		for _, inv := range result.PaidInvoices {
			query := entity.NewInvoiceQuerySet(tx).IDEq(inv.InvoiceID).GetUpdater().SetSentDate(&now).SetChannelLoss(inv.ChannelLoss).SetPaymentMethodID(&result.PaymentMethodID).SetStatus(inv.Status).SetPaymentType(&inv.PaymentType).SetPaymentCode(inv.PaymentCode)
			if inv.TaxRate != nil {
				query = query.SetTaxRateID(&inv.TaxRate.StripeTaxRateID)
			}
			err := query.Update()
			if err != nil {
				return err
			}
			m := entity.InvoiceMeta(inv.Meta)
			err = tx.Model(&entity.Invoice{}).Where("tenant_id = ? and id = ?", inv.TenantID, inv.InvoiceID).Update(
				"payment_meta", &m,
			).Error
			if err != nil {
				return err
			}
		}
		return nil
	})
}

func (i *InvoiceModel) CreateAutoConfirmedCostQuotaInvoice(tenantID uint, billingConfigID uint, date time.Time) (*forms.Invoice, error) {
	start := timeutils.GetMonthDate(date)
	end := start.AddDate(0, 1, 0)
	return i.createInvoice(tenantID, billingConfigID, start, start, end, entity.InvoiceType_COSTQUOTA, entity.InvoiceStatus_CONFIRMED)
}

func (i *InvoiceModel) CreateInvoiceByTypeStatus(tenantID uint, billingConfigID uint, date time.Time, invoiceType string, invoiceStatus string) (*forms.Invoice, error) {
	start := timeutils.GetMonthDate(date)
	end := start.AddDate(0, 1, 0)
	return i.createInvoice(tenantID, billingConfigID, start, start, end, invoiceType, invoiceStatus)
}
func (i *InvoiceModel) CreateMonthlyInvoice(tenantID uint, billingConfigID uint, date time.Time) (*forms.Invoice, error) {
	start := timeutils.GetMonthDate(date)
	end := start.AddDate(0, 1, 0)
	return i.createInvoice(tenantID, billingConfigID, start, start, end, entity.InvoiceType_MONTHLY, entity.InvoiceStatus_GENERATED)
}

func (i *InvoiceModel) CreateCostQuotaInvoice(tenantID uint, billingConfigID uint, date time.Time) (*forms.Invoice, error) {
	start := timeutils.GetMonthDate(date)
	end := start.AddDate(0, 1, 0)
	return i.createInvoice(tenantID, billingConfigID, start, start, end, entity.InvoiceType_COSTQUOTA, entity.InvoiceStatus_GENERATED)
}

func (i *InvoiceModel) CreateConfirmedRecalInvoice(tenantID uint, billingConfigID uint, date time.Time) (*forms.Invoice, error) {
	start := timeutils.GetMonthDate(date)
	end := start.AddDate(0, 1, 0)
	return i.createInvoice(tenantID, billingConfigID, start, start, end, entity.InvoiceType_Recal, entity.InvoiceStatus_CONFIRMED)
}

func (i *InvoiceModel) RecalculateBills(tenantID uint, billingConfigID uint, req *forms.RecalculateBillsPreviewReq) (interface{}, error) {
	if err := ensureSnapshotRecalculateEnabled(tenantID); err != nil {
		return nil, err
	}
	log.Info("dry run recalculate bills requested",
		zap.Uint("tenantID", tenantID),
		zap.Uint("billingConfigID", billingConfigID),
		zap.String("startDate", req.StartDate),
		zap.String("endDate", req.EndDate),
	)
	start, end, err := parseRecalculateWindow(req.StartDate, req.EndDate)
	if err != nil {
		return nil, billing_errors.NewInvalidInputError(err)
	}
	return i.previewRecalculateBills(tenantID, billingConfigID, start, end), nil
}

func (i *InvoiceModel) InternalRecalculateBills(tenantID uint, billingConfigID uint, req *forms.RecalculateBillsApplyReq) (interface{}, error) {
	if !billingsnapshot.IsSnapshotEnabledForTenant(tenantID) {
		return map[string]string{"status": "skipped", "reason": "snapshot metering raw is not enabled for this tenant"}, nil
	}
	if req == nil || req.SourceRunID == 0 {
		return nil, billing_errors.NewInvalidInputError(errors.New("source_run_id is required for recalculate apply"))
	}
	log.Info("internal recalculate bills requested",
		zap.Uint("tenantID", tenantID),
		zap.Uint("billingConfigID", billingConfigID),
		zap.Uint("sourceRunID", req.SourceRunID),
	)
	sourceRun, err := loadSnapshotApplySourceRun(tenantID, billingConfigID, req.SourceRunID)
	if err != nil {
		return nil, billing_errors.NewInvalidInputError(err)
	}
	invoiceID, err := i.inferRangeApplyInvoiceID(sourceRun)
	if err != nil {
		return nil, billing_errors.NewInvalidInputError(err)
	}
	applyMode := entity.BillingSnapshotApplyModeReplaceUnissuedInvoice
	result, err := billingsnapshot.NewApplyRecordModel().CreateOrGetApplyRecord(billingsnapshot.CreateOrGetApplyRecordInput{
		TenantID:        tenantID,
		BillingConfigID: billingConfigID,
		InvoiceID:       invoiceID,
		SourceRunID:     req.SourceRunID,
		ApplyMode:       applyMode,
		PatchRef:        buildSnapshotApplyPatchRef(tenantID, billingConfigID, invoiceID, req.SourceRunID, applyMode),
	})
	if err != nil {
		return nil, billing_errors.NewInvalidInputError(err)
	}
	applyResult, err := billingsnapshot.NewApplyRecordModel().ApplyReplaceUnissuedInvoice(billingsnapshot.ApplyReplaceUnissuedInvoiceInput{
		ApplyID: result.ApplyRecord.ApplyID,
	})
	if err != nil {
		return nil, billing_errors.NewInvalidInputError(err)
	}
	return applyResult, nil
}

func (i *InvoiceModel) RecalculateInvoice(tenantID uint, billingConfigID uint, invoiceID uint) (interface{}, error) {
	if err := ensureSnapshotRecalculateEnabled(tenantID); err != nil {
		return nil, err
	}
	log.Info("dry run recalculate invoice requested",
		zap.Uint("tenantID", tenantID),
		zap.Uint("billingConfigID", billingConfigID),
		zap.Uint("invoiceID", invoiceID),
	)
	return i.previewRecalculateInvoice(tenantID, billingConfigID, invoiceID), nil
}

func (i *InvoiceModel) GetRecalculateRun(tenantID uint, billingConfigID uint, sourceRunID uint) (*forms.RecalculatePreviewResponse, error) {
	if err := ensureSnapshotRecalculateEnabled(tenantID); err != nil {
		return nil, err
	}
	run, err := loadSnapshotApplySourceRun(tenantID, billingConfigID, sourceRunID)
	if err != nil {
		return nil, billing_errors.NewInvalidInputError(err)
	}
	item, err := buildRecalculateRunStatusItem(run)
	if err != nil {
		return nil, err
	}
	logRecalculatePreviewCompare(item)
	response := &forms.RecalculatePreviewResponse{
		Mode:   "recalculate_run",
		Status: item.Status,
		Items:  []forms.RecalculatePreviewItem{item},
		Meta: map[string]string{
			"source_mode": run.SourceMode,
		},
	}
	finalizeRecalculatePreviewResponse(response)
	if item.Status == entity.BillingSnapshotRunStatusRunning {
		response.Status = entity.BillingSnapshotRunStatusRunning
	}
	if item.Status == entity.BillingSnapshotRunStatusFailed {
		response.Status = entity.BillingSnapshotRunStatusFailed
	}
	return response, nil
}

func (i *InvoiceModel) ListRecalculateRuns(tenantID uint, billingConfigID uint, p *pagination.Pagination) (*forms.RecalculateRunListResponse, error) {
	if err := ensureSnapshotRecalculateEnabled(tenantID); err != nil {
		return nil, err
	}
	if p == nil {
		return nil, billing_errors.NewInvalidInputError(errors.New("pagination is required"))
	}

	query := db.GetDB().Model(&entity.BillingSnapshotRun{}).
		Joins("JOIN billing_snapshot_input_bundle ON billing_snapshot_input_bundle.bundle_id = billing_snapshot_run.input_bundle_id").
		Where("billing_snapshot_run.tenant_id = ?", tenantID).
		Where("billing_snapshot_run.billing_config_id = ?", billingConfigID).
		Where("billing_snapshot_input_bundle.bundle_type IN ?", []string{
			entity.BillingSnapshotInputBundleTypeRerunCandidate,
			entity.BillingSnapshotInputBundleTypeRerunPublished,
		})

	var total int64
	if err := query.Count(&total).Error; err != nil {
		return nil, err
	}
	var runs []entity.BillingSnapshotRun
	if err := query.Select("billing_snapshot_run.*").
		Order("billing_snapshot_run.created_at DESC, billing_snapshot_run.run_id DESC").
		Limit(p.PageSize).
		Offset((p.Page - 1) * p.PageSize).
		Find(&runs).Error; err != nil {
		return nil, err
	}

	items := make([]forms.RecalculateRun, 0, len(runs))
	for _, run := range runs {
		item := forms.RecalculateRun{
			RunID:           run.RunID,
			TenantID:        run.TenantID,
			BillingConfigID: run.BillingConfigID,
			InputBundleID:   run.InputBundleID,
			Status:          run.Status,
			InvoiceType:     run.RunType,
			InvoiceDate:     run.InvoiceDate,
			PeriodMonth:     run.PeriodMonth,
			WindowStart:     run.WindowStart,
			WindowEnd:       run.WindowEnd,
			SourceMode:      run.SourceMode,
			CreatedAt:       run.CreatedAt,
			FinishedAt:      run.FinishedAt,
		}
		if run.ErrorMessage != nil {
			item.ErrorMessage = *run.ErrorMessage
		}
		items = append(items, item)
	}
	return &forms.RecalculateRunListResponse{
		Runs:         items,
		Page:         p.Page,
		PageSize:     p.PageSize,
		TotalResults: int(total),
	}, nil
}

func (i *InvoiceModel) previewRecalculateBills(tenantID uint, billingConfigID uint, start time.Time, end time.Time) *forms.RecalculateStartResponse {
	response := &forms.RecalculateStartResponse{
		Mode:   "bills_range_preview",
		Status: entity.BillingSnapshotRunStatusRunning,
	}
	for _, window := range splitRecalculateRangeByMonth(start, end) {
		item := buildRecalculateStartItem(tenantID, billingConfigID, nil, entity.InvoiceType_MONTHLY, window.InvoiceDate, window.Start, window.End)
		response.Items = append(response.Items, item)
	}
	finalizeRecalculateStartResponse(response)
	return response
}

func (i *InvoiceModel) previewRecalculateInvoice(tenantID uint, billingConfigID uint, invoiceID uint) *forms.RecalculateStartResponse {
	response := &forms.RecalculateStartResponse{
		Mode:   "invoice_preview",
		Status: entity.BillingSnapshotRunStatusRunning,
	}
	invoice := &entity.Invoice{}
	if err := db.GetDB().
		Where("id = ? AND tenant_id = ? AND binded_billing_config_id = ?", invoiceID, tenantID, billingConfigID).
		First(invoice).Error; err != nil {
		item := forms.RecalculateStartItem{
			Status:  "error",
			Error:   err.Error(),
			Missing: []string{"legacy_invoice"},
		}
		response.Items = append(response.Items, item)
		finalizeRecalculateStartResponse(response)
		return response
	}
	window, err := billingsnapshot.BuildInvoiceWindow(invoice.Type, invoice.InvoiceDate)
	if err != nil {
		item := forms.RecalculateStartItem{
			Status:  "error",
			Error:   err.Error(),
			Missing: []string{"invoice_window"},
		}
		response.Items = append(response.Items, item)
		finalizeRecalculateStartResponse(response)
		return response
	}
	item := buildRecalculateStartItem(tenantID, billingConfigID, &invoiceID, invoice.Type, window.InvoiceDate, window.Start, window.End)
	response.Items = append(response.Items, item)
	finalizeRecalculateStartResponse(response)
	return response
}

type recalculatePreviewWindow struct {
	InvoiceDate time.Time
	Start       time.Time
	End         time.Time
}

func parseRecalculateWindow(startValue string, endValue string) (time.Time, time.Time, error) {
	start, err := parseRecalculateDate(startValue)
	if err != nil {
		return time.Time{}, time.Time{}, fmt.Errorf("invalid start_date: %w", err)
	}
	endDate, err := parseRecalculateDate(endValue)
	if err != nil {
		return time.Time{}, time.Time{}, fmt.Errorf("invalid end_date: %w", err)
	}
	start = timeutils.GetDayDate(start)
	end := timeutils.GetDayDate(endDate).AddDate(0, 0, 1)
	if !end.After(start) {
		return time.Time{}, time.Time{}, fmt.Errorf("invalid recalculate window: start=%s end=%s", start.Format(time.RFC3339), end.Format(time.RFC3339))
	}
	return start, end, nil
}

func parseRecalculateDate(value string) (time.Time, error) {
	return time.Parse("2006-01-02", value)
}

func splitRecalculateRangeByMonth(start time.Time, end time.Time) []recalculatePreviewWindow {
	start = start.UTC()
	end = end.UTC()
	var ret []recalculatePreviewWindow
	cursor := start
	for cursor.Before(end) {
		monthStart := timeutils.GetMonthDate(cursor)
		nextMonth := monthStart.AddDate(0, 1, 0)
		windowEnd := nextMonth
		if end.Before(windowEnd) {
			windowEnd = end
		}
		ret = append(ret, recalculatePreviewWindow{
			InvoiceDate: monthStart,
			Start:       cursor,
			End:         windowEnd,
		})
		cursor = windowEnd
	}
	return ret
}

func buildRecalculateStartItem(tenantID uint, billingConfigID uint, invoiceID *uint, invoiceType string, invoiceDate time.Time, windowStart time.Time, windowEnd time.Time) forms.RecalculateStartItem {
	item := forms.RecalculateStartItem{
		Status: entity.BillingSnapshotRunStatusRunning,
	}

	rerunStart := windowStart
	rerunEnd := windowEnd
	run, err := billingsnapshot.NewSettlementRunModel().StartSnapshotPrimaryRun(billingsnapshot.CreateSnapshotPrimaryRunInput{
		TenantID:         tenantID,
		BillingConfigID:  billingConfigID,
		InvoiceType:      invoiceType,
		InvoiceDate:      invoiceDate,
		BundleType:       entity.BillingSnapshotInputBundleTypeRerunCandidate,
		RerunWindowStart: &rerunStart,
		RerunWindowEnd:   &rerunEnd,
		VersionSet: entity.Labels{
			"run_reason": "recalculate_preview",
		},
	})
	if err != nil {
		item.Status = "error"
		item.Error = err.Error()
		item.Missing = append(item.Missing, "rerun_candidate_run")
		return item
	}
	item.SourceRunID = run.RunID
	billingsnapshot.TriggerSnapshotPrimaryRunAsync(run.RunID)
	return item
}

func buildRecalculateRunStatusItem(run *entity.BillingSnapshotRun) (forms.RecalculatePreviewItem, error) {
	item := forms.RecalculatePreviewItem{
		Status:          run.Status,
		ComparisonScope: "recalculate_preview",
		TenantID:        run.TenantID,
		BillingConfigID: run.BillingConfigID,
		InvoiceType:     run.RunType,
		InvoiceDate:     run.InvoiceDate,
		PeriodMonth:     run.PeriodMonth,
		WindowStart:     run.WindowStart,
		WindowEnd:       run.WindowEnd,
		SourceRunID:     run.RunID,
		InputBundleID:   run.InputBundleID,
		SourceMode:      run.SourceMode,
		Meta: map[string]string{
			"preview_source": run.SourceMode,
		},
	}
	if run.ErrorMessage != nil && *run.ErrorMessage != "" {
		item.Error = *run.ErrorMessage
	}
	if invoice := findRecalculateRunInvoice(run); invoice != nil {
		item.InvoiceID = &invoice.ID
		item.Risks = append(item.Risks, invoiceApplyRisks(invoice)...)
	}
	legacyCosts, err := (&costexplorer.CostExplorerModel{}).CalculateOpenCostsForRange(run.TenantID, run.BillingConfigID, run.WindowStart, run.WindowEnd)
	if err != nil {
		item.Status = "error"
		item.Error = err.Error()
		item.Missing = append(item.Missing, "legacy_open_costs")
		return item, nil
	}
	item.LegacyRowCount = legacyCosts.Meta.LegacyRowCount
	fillRecalculatePreviewLegacyAmounts(&item, legacyCosts)
	if run.Status != entity.BillingSnapshotRunStatusSuccess && run.Status != entity.BillingSnapshotRunStatusFrozen {
		if run.Status == entity.BillingSnapshotRunStatusRunning {
			item.Missing = append(item.Missing, "snapshot_invoice_preview")
		}
		return item, nil
	}
	snapshotCosts, err := (&costexplorer.CostExplorerModel{}).CalculateRecalculateRunCosts(run.TenantID, run.BillingConfigID, run.RunID)
	if err != nil {
		item.Status = "error"
		item.Error = err.Error()
		item.Missing = append(item.Missing, "snapshot_recalculate_costs")
		return item, nil
	}
	fillRecalculatePreviewSnapshotAmounts(&item, snapshotCosts)
	item.Status = "found"
	item.SnapshotFactCount = snapshotCosts.Meta.InvoiceFactCount
	item.Meta["total_fact_count"] = strconv.FormatInt(snapshotCosts.Meta.FactCount, 10)
	item.Meta["total_cost_price"] = item.SnapshotCostPrice.String()
	item.DiffRaw = item.SnapshotRawPrice.Sub(item.LegacyRawPrice)
	item.DiffCredits = item.SnapshotCredits.Sub(item.LegacyCredits)
	item.DiffRoundingAdjustment = item.SnapshotRoundingAdjustment.Sub(item.LegacyRoundingAdjustment)
	item.DiffCost = item.SnapshotCostPrice.Sub(item.LegacyCostPrice)
	item.AbsDiffRaw = absDecimal(item.DiffRaw)
	item.AbsDiffCredits = absDecimal(item.DiffCredits)
	item.AbsDiffRoundingAdjustment = absDecimal(item.DiffRoundingAdjustment)
	item.AbsDiffCost = absDecimal(item.DiffCost)
	item.Match = item.DiffRaw.Equal(decimal.Zero) && item.DiffCredits.Equal(decimal.Zero) && item.DiffRoundingAdjustment.Equal(decimal.Zero) && item.DiffCost.Equal(decimal.Zero)
	if item.SnapshotFactCount == 0 {
		item.Missing = append(item.Missing, "snapshot_invoice_facts")
	}
	return item, nil
}

func fillRecalculatePreviewLegacyAmounts(item *forms.RecalculatePreviewItem, costs *forms.CostExplorerOpenCostsResponse) {
	if item == nil || costs == nil {
		return
	}
	overview := costs.BillsOverView.OpenCostsBillsCosts
	item.LegacyRawPrice = overview.RunningTotal
	item.LegacyCostPrice = overview.TotalCost
	item.LegacyCredits = overview.Credits
	item.LegacyDiscounts = overview.Discounts
	item.LegacyRoundingAdjustment = overview.RoundingAdjustment
	item.LegacyCreditPackageIDs = costs.Meta.MatchedCreditPackageIDs
	item.LegacyDiscountIDs = costs.Meta.MatchedDiscountIDs
	if item.Meta != nil {
		item.Meta["legacy_credit_package_ids"] = item.LegacyCreditPackageIDs
		item.Meta["legacy_discount_ids"] = item.LegacyDiscountIDs
	}
}

func fillRecalculatePreviewSnapshotAmounts(item *forms.RecalculatePreviewItem, costs *forms.CostExplorerOpenCostsResponse) {
	if item == nil || costs == nil {
		return
	}
	overview := costs.BillsOverView.OpenCostsBillsCosts
	item.SnapshotRawPrice = overview.RunningTotal
	item.SnapshotCostPrice = overview.TotalCost
	item.SnapshotCredits = overview.Credits
	item.SnapshotDiscounts = overview.Discounts
	item.SnapshotRoundingAdjustment = overview.RoundingAdjustment
	item.SnapshotCreditPackageIDs = costs.Meta.MatchedCreditPackageIDs
	item.SnapshotDiscountIDs = costs.Meta.MatchedDiscountIDs
	if item.Meta != nil {
		item.Meta["snapshot_credit_package_ids"] = item.SnapshotCreditPackageIDs
		item.Meta["snapshot_discount_ids"] = item.SnapshotDiscountIDs
	}
}

func findRecalculateRunInvoice(run *entity.BillingSnapshotRun) *entity.Invoice {
	if run == nil {
		return nil
	}
	if run.RunType != entity.InvoiceType_MONTHLY && run.RunType != entity.InvoiceType_COSTQUOTA && run.RunType != entity.InvoiceType_Recal {
		return nil
	}
	invoice := &entity.Invoice{}
	err := db.GetDB().
		Where("tenant_id = ? AND binded_billing_config_id = ?", run.TenantID, run.BillingConfigID).
		Where("type = ?", run.RunType).
		Where("invoice_date = ?", run.InvoiceDate).
		Order("id desc").
		First(invoice).Error
	if err != nil {
		return nil
	}
	return invoice
}

func loadSnapshotApplySourceRun(tenantID uint, billingConfigID uint, sourceRunID uint) (*entity.BillingSnapshotRun, error) {
	sourceRun := &entity.BillingSnapshotRun{}
	err := db.GetDB().
		Where("run_id = ?", sourceRunID).
		Where("tenant_id = ?", tenantID).
		Where("billing_config_id = ?", billingConfigID).
		First(sourceRun).Error
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, fmt.Errorf("snapshot run %d not found", sourceRunID)
	}
	if err != nil {
		return nil, err
	}
	return sourceRun, nil
}

func buildSnapshotApplyPatchRef(tenantID uint, billingConfigID uint, invoiceID *uint, sourceRunID uint, applyMode string) string {
	parts := []string{
		"snapshot-apply",
		fmt.Sprintf("tenant=%d", tenantID),
		fmt.Sprintf("billing_config=%d", billingConfigID),
		fmt.Sprintf("source_run=%d", sourceRunID),
		fmt.Sprintf("apply_mode=%s", applyMode),
	}
	if invoiceID != nil {
		parts = append(parts, fmt.Sprintf("invoice=%d", *invoiceID))
	}
	return strings.Join(parts, ":")
}

func (i *InvoiceModel) inferRangeApplyInvoiceID(sourceRun *entity.BillingSnapshotRun) (*uint, error) {
	invoice, err := i.GetMonthlyInvoice(sourceRun.TenantID, sourceRun.BillingConfigID, sourceRun.InvoiceDate)
	if err != nil {
		return nil, err
	}
	invoiceID := invoice.InvoiceID
	entityInvoice := &entity.Invoice{}
	if err := db.GetDB().Where("id = ?", invoiceID).First(entityInvoice).Error; err != nil {
		return nil, err
	}
	if err := validateSnapshotReplaceUnissuedInvoice(entityInvoice); err != nil {
		return nil, err
	}
	if err := billingsnapshot.ValidateReplaceInvoiceWindow(sourceRun, entityInvoice); err != nil {
		return nil, err
	}
	return &invoiceID, nil
}

func validateSnapshotReplaceUnissuedInvoice(invoice *entity.Invoice) error {
	if invoice == nil {
		return errors.New("invoice is required")
	}
	if invoice.SentDate != nil || invoice.Status == entity.InvoiceStatus_SENT || invoice.Status == entity.InvoiceStatus_PAID {
		return fmt.Errorf("invoice %d is already sent; use credits adjustment flow instead", invoice.ID)
	}
	if invoice.Status != entity.InvoiceStatus_GENERATED && invoice.Status != entity.InvoiceStatus_CONFIRMED {
		return fmt.Errorf("invoice %d status %s cannot be replaced by snapshot recalculate", invoice.ID, invoice.Status)
	}
	return nil
}

func invoiceApplyRisks(invoice *entity.Invoice) []string {
	if invoice == nil {
		return nil
	}
	var risks []string
	if invoice.SentDate != nil || invoice.Status == entity.InvoiceStatus_SENT || invoice.Status == entity.InvoiceStatus_PAID {
		risks = append(risks, "invoice_already_sent_use_credits_adjustment")
	}
	if invoice.Status != entity.InvoiceStatus_GENERATED && invoice.Status != entity.InvoiceStatus_CONFIRMED {
		risks = append(risks, "replace_unissued_invoice_not_allowed_for_status_"+invoice.Status)
	}
	return risks
}

func finalizeRecalculatePreviewResponse(response *forms.RecalculatePreviewResponse) {
	for _, item := range response.Items {
		response.Summary.ItemCount++
		if item.Status == entity.BillingSnapshotRunStatusRunning {
			response.Summary.RunningCount++
			response.Status = entity.BillingSnapshotRunStatusRunning
			continue
		}
		if item.Status == "error" || item.Status == entity.BillingSnapshotRunStatusFailed {
			response.Summary.ErrorCount++
			if item.Status == entity.BillingSnapshotRunStatusFailed {
				response.Status = entity.BillingSnapshotRunStatusFailed
			} else {
				response.Status = "partial_error"
			}
			continue
		}
		response.Summary.FoundCount++
		if item.Match {
			response.Summary.MatchCount++
		}
		response.Summary.LegacyRawPrice = response.Summary.LegacyRawPrice.Add(item.LegacyRawPrice)
		response.Summary.LegacyCostPrice = response.Summary.LegacyCostPrice.Add(item.LegacyCostPrice)
		response.Summary.SnapshotRawPrice = response.Summary.SnapshotRawPrice.Add(item.SnapshotRawPrice)
		response.Summary.SnapshotCostPrice = response.Summary.SnapshotCostPrice.Add(item.SnapshotCostPrice)
		response.Summary.LegacyRoundingAdjustment = response.Summary.LegacyRoundingAdjustment.Add(item.LegacyRoundingAdjustment)
		response.Summary.SnapshotRoundingAdjustment = response.Summary.SnapshotRoundingAdjustment.Add(item.SnapshotRoundingAdjustment)
	}
	response.Summary.DiffRaw = response.Summary.SnapshotRawPrice.Sub(response.Summary.LegacyRawPrice)
	response.Summary.DiffRoundingAdjustment = response.Summary.SnapshotRoundingAdjustment.Sub(response.Summary.LegacyRoundingAdjustment)
	response.Summary.DiffCost = response.Summary.SnapshotCostPrice.Sub(response.Summary.LegacyCostPrice)
	if response.Summary.ItemCount == 0 {
		response.Status = "empty"
	}
}

func finalizeRecalculateStartResponse(response *forms.RecalculateStartResponse) {
	if len(response.Items) == 0 {
		response.Status = "empty"
		return
	}
	hasRunning := false
	hasError := false
	for _, item := range response.Items {
		switch item.Status {
		case entity.BillingSnapshotRunStatusRunning:
			hasRunning = true
		case "error", entity.BillingSnapshotRunStatusFailed:
			hasError = true
		}
	}
	switch {
	case hasRunning:
		response.Status = entity.BillingSnapshotRunStatusRunning
	case hasError:
		response.Status = "partial_error"
	default:
		response.Status = "found"
	}
}

func absDecimal(value decimal.Decimal) decimal.Decimal {
	if value.LessThan(decimal.Zero) {
		return value.Neg()
	}
	return value
}

func logRecalculatePreviewCompare(item forms.RecalculatePreviewItem) {
	if item.Status != "found" {
		return
	}
	fields := []zap.Field{
		zap.String("domain", "recalculate"),
		zap.String("comparison_scope", item.ComparisonScope),
		zap.String("snapshot_status", item.Status),
		zap.Uint("tenant_id", item.TenantID),
		zap.Uint("billing_config_id", item.BillingConfigID),
		zap.String("invoice_type", item.InvoiceType),
		zap.Time("invoice_date", item.InvoiceDate),
		zap.String("period_month", item.PeriodMonth),
		zap.Time("window_start", item.WindowStart),
		zap.Time("window_end", item.WindowEnd),
		zap.Uint("snapshot_run_id", item.SourceRunID),
		zap.Uint("input_bundle_id", item.InputBundleID),
		zap.String("source_mode", item.SourceMode),
		zap.Int64("legacy_row_count", item.LegacyRowCount),
		zap.String("legacy_raw_price", item.LegacyRawPrice.String()),
		zap.String("legacy_credits", item.LegacyCredits.String()),
		zap.String("legacy_discounts", item.LegacyDiscounts.String()),
		zap.String("legacy_rounding_adjustment", item.LegacyRoundingAdjustment.String()),
		zap.String("legacy_cost_price", item.LegacyCostPrice.String()),
		zap.String("legacy_credit_package_ids", item.LegacyCreditPackageIDs),
		zap.String("legacy_discount_ids", item.LegacyDiscountIDs),
		zap.Int64("snapshot_fact_count", item.SnapshotFactCount),
		zap.String("snapshot_raw_price", item.SnapshotRawPrice.String()),
		zap.String("snapshot_credits", item.SnapshotCredits.String()),
		zap.String("snapshot_discounts", item.SnapshotDiscounts.String()),
		zap.String("snapshot_rounding_adjustment", item.SnapshotRoundingAdjustment.String()),
		zap.String("snapshot_cost_price", item.SnapshotCostPrice.String()),
		zap.String("snapshot_credit_package_ids", item.SnapshotCreditPackageIDs),
		zap.String("snapshot_discount_ids", item.SnapshotDiscountIDs),
		zap.String("diff_raw", item.DiffRaw.String()),
		zap.String("diff_credits", item.DiffCredits.String()),
		zap.String("diff_rounding_adjustment", item.DiffRoundingAdjustment.String()),
		zap.String("diff_cost", item.DiffCost.String()),
		zap.String("abs_diff_raw", item.AbsDiffRaw.String()),
		zap.String("abs_diff_credits", item.AbsDiffCredits.String()),
		zap.String("abs_diff_rounding_adjustment", item.AbsDiffRoundingAdjustment.String()),
		zap.String("abs_diff_cost", item.AbsDiffCost.String()),
		zap.Bool("match", item.Match),
		zap.Strings("missing", item.Missing),
		zap.Strings("risks", item.Risks),
	}
	if item.InvoiceID != nil {
		fields = append(fields, zap.Uint("legacy_invoice_id", *item.InvoiceID))
	}
	if item.Error != "" {
		fields = append(fields, zap.String("error", item.Error))
	}
	log.Info("billing_snapshot_recalculate_preview_compare", fields...)
}

func ensureSnapshotRecalculateEnabled(tenantID uint) error {
	if !billingsnapshot.IsSnapshotEnabledForTenant(tenantID) {
		return billing_errors.NewForbiddenError(fmt.Errorf("snapshot recalculate is disabled for tenant %d", tenantID))
	}
	return nil
}

// only for marketplace
func (i *InvoiceModel) CreateAutoConfirmedMontlyInvoice(tenantID uint, billingConfigID uint, date time.Time) (*forms.Invoice, error) {
	start := timeutils.GetMonthDate(date)
	end := start.AddDate(0, 1, 0)

	return i.createInvoice(tenantID, billingConfigID, start, start, end, entity.InvoiceType_MONTHLY, entity.InvoiceStatus_CONFIRMED)
}

func (i *InvoiceModel) CreateHourlyInvoice(tenantID uint, billingConfigID uint, date time.Time, invoiceStatus string) (*forms.Invoice, error) {
	invoice_date := timeutils.GetHourDate(date)
	start := invoice_date.Add(-config.Get().GetDuration(config.MaxHourlyInvoiceReseekDays) * time.Hour * 24)
	if start.Before(timeutils.GetMonthDate(date)) {
		start = timeutils.GetMonthDate(date)
	}
	end := invoice_date.Add(time.Hour)
	return i.createInvoice(tenantID, billingConfigID, invoice_date, start, end, entity.InvoiceType_HOURLY, invoiceStatus)
}

// only for marketplace
func (i *InvoiceModel) CreateAutoConfirmedHourlyInvoice(tenantID uint, billingConfigID uint, date time.Time) (*forms.Invoice, error) {
	return i.CreateHourlyInvoice(tenantID, billingConfigID, date, entity.InvoiceStatus_CONFIRMED)
}

func (i *InvoiceModel) CreateDailyInvoice(tenantID uint, billingConfigID uint, invoiceDate time.Time, invoice_status string) (*forms.Invoice, error) {
	invoiceDate = timeutils.GetDayDate(invoiceDate)
	start := invoiceDate.AddDate(0, 0, -config.Get().GetInt(config.MaxHourlyInvoiceReseekDays))
	if start.Before(timeutils.GetMonthDate(invoiceDate)) {
		start = timeutils.GetMonthDate(invoiceDate)
	}
	end := invoiceDate.AddDate(0, 0, 1)
	return i.createInvoice(tenantID, billingConfigID, invoiceDate, start, end, entity.InvoiceType_DAILY, invoice_status)
}

func (i *InvoiceModel) createInvoice(tenantID uint, billingConfigID uint, invoice_date, start, end time.Time, invoice_type string, invoice_status string) (*forms.Invoice, error) {
	log.Info("start to create invoice", zap.Uint("tenantID", tenantID), zap.Uint("billingConfigID", billingConfigID),
		zap.Time("invoice_date", invoice_date), zap.Time("start", start), zap.Time("end", end),
		zap.String("invoiceType", invoice_type), zap.String("invoiceStatus", invoice_status))
	measureModel := &billing.MeasureModel{}
	costExplorerModel := &costexplorer.CostExplorerModel{}

	switch invoice_type {
	case entity.InvoiceType_DAILY, entity.InvoiceType_INMONTH, entity.InvoiceType_MONTHLY:
		cnt, err := entity.NewInvoiceQuerySet(db.GetDB()).TenantIDEq(tenantID).BindedBillingConfigIDEq(billingConfigID).TypeEq(invoice_type).InvoiceDateEq(invoice_date).Count()
		if err != nil {
			dealCounter(false, false, invoice_type)
			return nil, err
		}
		if cnt > 0 {
			log.Info("createInvoice  already created ", zap.Uint("tenant", tenantID), zap.String("invoice_type", invoice_type), zap.Time("start", start))
			return nil, billing_errors.NewObjectAlreadyExistsError(fmt.Errorf("tenant %d invoice_type%s  invoice %s already created ", tenantID, invoice_type, start))
		}
	}

	query := forms.CostExplorerQuery{
		StartDate: start,
		EndDate:   end,
		UseMainDB: true,
	}
	tenantIDPrefix := fmt.Sprintf("%d-", tenantID)
	list := config.Get().GetStringSlice(config.MonthlyBillsCacheList)
	isBigCustomer := slices.ContainsFunc(list, func(s string) bool {
		return strings.HasPrefix(s, tenantIDPrefix)
	})
	if isBigCustomer {
		query.DisableAttachProjectName = true
		query.DisableAttachClusterName = true
	}
	res, err := costExplorerModel.CalculateCost(tenantID, billingConfigID, &query)

	if err != nil {
		dealCounter(false, false, invoice_type)
		return nil, err
	}

	if res.UnbindedInvoiceCosts.CostPrice.Cmp(decimal.Zero) == 0 {
		return nil, billing_errors.NewInvoiceNoCosts(nil)
	}

	if billingsnapshot.IsSnapshotEnabledForTenant(tenantID) {
		snapshotRun, snapshotErr := billingsnapshot.CreateLegacyCostSourceRunForInvoice(tenantID, billingConfigID, invoice_type, invoice_date)
		if snapshotErr != nil {
			log.Warn("failed to create legacy cost source snapshot run", zap.Uint("tenantID", tenantID), zap.Uint("billingConfigID", billingConfigID), zap.String("invoiceType", invoice_type), zap.Time("invoiceDate", invoice_date), zap.Error(snapshotErr))
		} else {
			log.Info("created legacy cost source snapshot run", zap.Uint("tenantID", tenantID), zap.Uint("billingConfigID", billingConfigID), zap.String("invoiceType", invoice_type), zap.Time("invoiceDate", invoice_date), zap.Uint("snapshotRunID", snapshotRun.RunID))
		}
	}

	dealCounter(false, true, invoice_type)

	// payment_method_id is intentionally NOT set at create time: it is
	// pay-time metadata (Phase 1+2 of the invoice/PM decoupling). Customer's
	// active PM at create time is *not* what the invoice will eventually be
	// settled with — many customers switch PM (e.g. AWS → Stripe) between
	// generation and actual pay. Pay flows (StripeInvoicePayingWorker,
	// SendInvoice → UpdateSentInvoices) write payment_method_id +
	// payment_type + payment_code atomically when the SENT transition is
	// committed. Until then the invoice row has payment_method_id = NULL.
	invoiceEntity := &entity.Invoice{
		TenantID:              tenantID,
		Status:                entity.InvoiceStatus_INITED,
		Type:                  invoice_type,
		InvoiceDate:           invoice_date,
		BindedBillingConfigID: billingConfigID,
	}
	err = invoiceEntity.Create(db.GetDB())
	if err != nil {
		dealCounter(false, false, invoice_type)
		return nil, err
	}

	err = measureModel.UnpaidCostsBindInvoice(tenantID, billingConfigID, start, end, invoiceEntity.ID)
	if err != nil {
		dealCounter(false, false, invoice_type)
		return nil, err
	}

	costs, err := costExplorerModel.GetInvoiceCosts(tenantID, invoiceEntity.BindedBillingConfigID, invoiceEntity.ID, true)
	if err != nil {
		dealCounter(false, false, invoice_type)
		return nil, err
	}
	if costs.CostPrice.Cmp(decimal.Zero) == 0 {
		return nil, billing_errors.NewInvoiceNoCosts(nil)
	}

	invoiceEntity.CostPrice = costs.CostPrice
	invoiceEntity.RawPrice = costs.RawPrice

	invoiceEntity.Status = invoice_status
	err = entity.NewInvoiceQuerySet(db.GetDB()).IDEq(invoiceEntity.ID).GetUpdater().SetCostPrice(invoiceEntity.CostPrice).SetRawPrice(invoiceEntity.RawPrice).SetStatus(invoiceEntity.Status).Update()
	if err != nil {
		dealCounter(false, false, invoice_type)
		return nil, err
	}
	info, err := i.GetInvoice(invoiceEntity.ID)
	if err != nil {
		dealCounter(false, false, invoice_type)
		return nil, err
	}
	dealCounter(true, false, invoice_type)
	billingsnapshot.LogInvoicePreviewComparison(info)

	if invoice_status == entity.InvoiceStatus_GENERATED {
		err = mailhandler.GetHandler().SendMail([]string{config.Get().GetString(config.InvoiceNotifyEmail)}, mailhandler.SendNeedConfirmInvoiceNotifyTemplateID, map[string]string{
			"tenant_id":    fmt.Sprintf("%d", tenantID),
			"cost_price":   info.CostPrice.String(),
			"invoice_type": invoice_type,
		})
		if err != nil {
			log.Error("failed to send need confirmed invoice email", zap.Uint("tenantID", tenantID), zap.Uint("invoiceID", info.InvoiceID), zap.Error(err))
		} else {
			log.Info("success send need confirmed invoice email", zap.Uint("tenantID", tenantID), zap.Uint("invoiceID", info.InvoiceID))
		}
	}

	return info, nil
}

func dealCounter(isSuccess bool, isInit bool, invoiceType string) {
	if isInit {
		InvoiceCounter.WithLabelValues(invoiceType, "init").Inc()
	} else {
		InvoiceCounter.WithLabelValues(invoiceType, strconv.FormatBool(isSuccess)).Inc()
	}

}

func (i *InvoiceModel) GetMonthlyInvoice(tenantID uint, billingConfigID uint, date time.Time) (*forms.Invoice, error) {
	invoice_date := timeutils.GetMonthDate(date)
	invoice := &entity.Invoice{}
	err := errorutils.WrapSQLError(entity.NewInvoiceQuerySet(db.GetDB()).TenantIDEq(tenantID).InvoiceDateEq(invoice_date).BindedBillingConfigIDEq(billingConfigID).One(invoice), fmt.Sprintf("tenant %d invoice date %s", tenantID, invoice_date.Format("2006-01-02")))
	if err != nil {
		return nil, err
	}
	return invoiceFromEntity(invoice), nil
}

func (i *InvoiceModel) ListAllMonthlyInvoices(tenantID uint, billingConfigID uint, date time.Time) ([]forms.Invoice, error) {
	startDate := timeutils.GetMonthDate(date)
	endDate := startDate.AddDate(0, 1, 0)
	var invoices []entity.Invoice
	err := entity.NewInvoiceQuerySet(db.GetDB()).TenantIDEq(tenantID).InvoiceDateGte(startDate).InvoiceDateLt(endDate).BindedBillingConfigIDEq(billingConfigID).All(&invoices)
	if err != nil {
		return nil, err
	}
	ret := make([]forms.Invoice, len(invoices))
	for i, inv := range invoices {
		ret[i] = *invoiceFromEntity(&inv)
	}
	return ret, nil
}

func (i *InvoiceModel) deleteInvoice(tenantID uint, invoiceID uint) error {
	measureModel := &billing.MeasureModel{}

	err := measureModel.CostsUnBindInvoice(tenantID, invoiceID)

	if err != nil {
		return err
	}

	err = errorutils.WrapSQLError(entity.NewInvoiceQuerySet(db.GetDB()).IDEq(invoiceID).TenantIDEq(tenantID).Delete(), fmt.Sprintf("invoice %d", invoiceID))
	if err != nil {
		return err
	}
	return nil
}

func (i *InvoiceModel) DeleteInvoice(invoiceID uint) error {
	invoice, err := i.GetInvoice(invoiceID)
	if err != nil {
		return err
	}
	if (invoice.Status == entity.InvoiceStatus_SENT) || (invoice.Status == entity.InvoiceStatus_PAID) {
		return billing_errors.NewInvalidInputError(errors.New("could not delete invoice, invoice already be sent"))
	}
	return i.deleteInvoice(invoice.TenantID, invoiceID)
}

func (i *InvoiceModel) DeleteInvoiceForTest(invoiceID uint) error {
	invoice, err := i.GetInvoice(invoiceID)
	if err != nil {
		return err
	}
	log.Info("start delete invoice for test")
	return i.deleteInvoice(invoice.TenantID, invoiceID)
}

func (i *InvoiceModel) ReSendInvoice(tenantID uint, invoiceID uint) error {
	invoice, err := i.GetInvoice(invoiceID)
	if err != nil {
		return err
	}
	if tenantID != invoice.TenantID {
		return billing_errors.NewInvalidInputError(errors.New("could not Modify invoice status,tenantID param err"))
	}
	err = entity.NewInvoiceQuerySet(db.GetDB()).TenantIDEq(tenantID).IDEq(invoiceID).GetUpdater().SetStatus(entity.InvoiceStatus_CONFIRMED).Update()

	if err != nil {
		return err
	}

	return nil
}

func (i *InvoiceModel) CreateCommitmentInvoice(tenantID uint, input *forms.CommitmentInvoiceInput) (*forms.Invoice, error) {
	recordEntity := &entity.CommitmentRecords{
		Amount:                input.Amount,
		Date:                  input.Date,
		Owner:                 input.Owner,
		BindedBillingConfigID: *input.BindedBillingConfigID,
		PaymentCode:           input.PaymentCode,
	}
	err := recordEntity.Create(db.GetDB())
	if err != nil {
		return nil, err
	}

	// payment_method_id is intentionally NOT set at create time (Phase 1+2).
	// The CreateCommitmentInvoice path writes a commitment row, then the
	// paying worker (Stripe paying or regular SendInvoice → UpdateSentInvoices)
	// writes payment_method_id + payment_type when the SENT transition fires.
	// Writes PaymentCode here because that is a stable identifier of the
	// commitment agreement itself (independent of which PM ultimately pays).
	invoiceEntity := &entity.Invoice{
		TenantID:              tenantID,
		Status:                entity.InvoiceStatus_GENERATED,
		Type:                  entity.InvoiceType_COMMITMENT,
		InvoiceDate:           input.Date,
		CostPrice:             input.Amount,
		RawPrice:              input.Amount,
		BindedBillingConfigID: *input.BindedBillingConfigID,
		PaymentCode:           input.PaymentCode,
	}
	err = invoiceEntity.Create(db.GetDB())
	if err != nil {
		return nil, err
	}
	return i.GetInvoice(invoiceEntity.ID)
}

func (i *InvoiceModel) GetMonthlyAggregateInvoicesByPaymentType(start time.Time, end time.Time, paymentType string) ([]*forms.MonthlyAggregateInvoice, error) {
	var err error
	var queryResults []*forms.MonthlyAggregateInvoice
	err = db.GetDB().Model(&entity.Invoice{}).
		Where("status = ?", entity.InvoiceStatus_SENT).
		Where("sent_date >= ?", start).Where("sent_date < ?", end).
		Where("(payment_method_id in (select id from payment_methods where payment_type = ? AND managed_by_sales = ?)) or (type=? and payment_method_id in (select id from payment_methods where payment_type = ? AND managed_by_sales = ?))", paymentType, false, entity.InvoiceType_COMMITMENT, paymentType, true).
		Select("tenant_id, payment_method_id, date_format(invoice_date, \"%Y-%m\") as invoice_month, SUM(cost_price - channel_loss) as invoice").
		Group("tenant_id, payment_method_id, date_format(invoice_date, \"%Y-%m\")").
		Order("tenant_id ASC, payment_method_id ASC, date_format(invoice_date, \"%Y-%m\") ASC").Scan(&queryResults).Error
	return queryResults, err
}

func (i *InvoiceModel) MarketplaceUnsubscribedProcess(tenantID uint) error {
	billingConfigID, err := (&billingconfig.BillingConfigModel{}).GetActiveBillingConfigID(tenantID)
	if err != nil {
		return err
	}
	paymentMethod, err := (&paymentsub.PaymentModel{}).GetActivePaymentMethod(tenantID, billingConfigID)
	if err != nil {
		return err
	}
	tenant, err := centralhandler.GetCentralHandler().GetTenant(tenantID)
	if err != nil {
		return err
	}
	if paymentMethod.PaymentType != paymentsub.AccountProvider2PaymentType(tenant.ThirdPartyAccountProvider) {
		log.Info("active payment do not match third party provider, skip it", zap.Uint("tenantID", tenantID), zap.String("paymentType", paymentMethod.PaymentType), zap.String("ThirdPartyAccountProvider", tenant.ThirdPartyAccountProvider))
		return nil
	}

	// Added: if the users are from alibaba cloud, we should cancel their binded support plan before sending unsent invoices
	if paymentMethod.PaymentType == entity.PaymentType_AliCloud {
		log.Info("customer from alicloud, need to cancel their bound support plan before sending unsent invoices", zap.Uint("tenantID", tenantID))
		supportPlanModel := marketing.SupportPlanModel{}
		supportPlan, err := supportPlanModel.GetTenantActiveBindedPlan(tenantID)
		if err != nil {
			if !errors.Is(err, billing_errors.ObjectNotExistsError) {
				log.Error("failed to query active binded support plan for users from alibaba cloud", zap.Uint("tenantID", tenantID))
				return err
			}
		} else {
			_, err = supportPlanModel.CancelBindedSupportPlan(supportPlan.BindedSupportPlanID)
			if err != nil {
				log.Error("failed to cancel binded support plan for users from alibaba cloud when unsbuscribe", zap.Uint("tenantID", tenantID), zap.Uint("bindedSupportPlanID", supportPlan.BindedSupportPlanID))
				return err
			}
		}
	}

	log.Info("active payment match third party provider, start to process unsubscribed invoice sending", zap.Uint("tenantID", tenantID), zap.String("paymentType", paymentMethod.PaymentType))
	date := timeutils.GetLastMonth(timeutils.GetNow())
	for j := 0; j != 2; j++ {
		start := date.AddDate(0, j, 0)
		inv, err := i.CreateAutoConfirmedCostQuotaInvoice(tenantID, billingConfigID, start)
		if errors.Is(err, billing_errors.InvoiceNoCostsError) {
			log.Info("no cost to create invoice", zap.Time("start", start), zap.Uint("tenantID", tenantID))
			continue
		}
		if err != nil {
			log.Error("failed to create invoice on unsubscribed", zap.Error(err), zap.Time("start", start), zap.Uint("tenantID", tenantID), zap.String("paymentType", paymentMethod.PaymentType))
			return err
		}
		log.Info("success to create invoice on unsubscribed", zap.Uint("invoiceID", inv.InvoiceID), zap.Time("start", start), zap.Uint("tenantID", tenantID), zap.String("paymentType", paymentMethod.PaymentType), zap.Uint("invoiceID", inv.InvoiceID))
	}
	err = i.SendAllUnsentInvoice(tenantID, billingConfigID)
	if err != nil {
		log.Error("failed to send invoices on unsubscribed", zap.Error(err), zap.Uint("tenantID", tenantID), zap.String("paymentType", paymentMethod.PaymentType))
		return err
	}
	log.Info("success to send invoices on unsubscribed", zap.Uint("tenantID", tenantID), zap.String("paymentType", paymentMethod.PaymentType))
	return nil
}

func (i *InvoiceModel) RegisterMarketplaceEventHandler() error {
	group := "invoices"
	err := eventbushandler.Get().RegisterEventHandleFunc(paymentsub.TenantThirdPartyNormalUnsubscribed, group, forms.TenantID{}, func(event *eventbushandler.Event) error {
		return i.MarketplaceUnsubscribedProcess(event.EventData.(*forms.TenantID).TenantID)
	})
	return err
}
