package paymenthandler

import (
	"bytes"
	"errors"
	"fmt"
	"sort"
	"strconv"
	"strings"
	"text/template"
	"time"

	"github.com/pingcap/dbaas/billing2/billing_errors"
	"github.com/pingcap/dbaas/billing2/config"
	"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/utils/log"
	"github.com/pingcap/dbaas/billing2/utils/timeutils"
	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/promauto"
	"github.com/shopspring/decimal"
	"github.com/stripe/stripe-go/v76"
	scclient "github.com/stripe/stripe-go/v76/client"
	"go.uber.org/zap"
)

type StripePaymentHandler interface {
	SetupIntent(userCode string) (*forms.SetupIntent, error)
	ListPaymentMethods(userCode string) ([]forms.StripePaymentMethod, error)
	UpdatePaymentMethod(userCode, methodId string, method *forms.StripePaymentMethod) (*forms.StripePaymentMethod, error)
	DeletePaymentMethod(userCode, methodId string) error
	SetPaymentMethodDefault(userCode, methodId string) (*forms.StripePaymentMethod, error)
	GetCustomerPaymentMethod(userCode, methodId string) (*forms.StripePaymentMethod, error)
	GetPaymentMethod(methodId string) (*forms.StripePaymentMethod, error)
	GetCustomerInfo(userCode string) (*forms.StripeCustomerInfo, error)
	UpdateCustomerBillingInfo(userCode string, billingInfo *forms.BillingInformation) (*forms.StripeCustomerInfo, error)
	ListStripeInvoices(userCode string) ([]*forms.StripeInvoice, error)
	ListStripeInvoicesWithCache(userCode string) ([]*forms.StripeInvoice, error)
	ConfirmRequiredConfirmedPaymentIntent(paymentIntentID string) error
	ConfirmPaymentIntent(cent decimal.Decimal, paymentMethodID string, customerId string) (string, error)
	Init3DSPaymentIntent(cent decimal.Decimal, paymentMethodID string, customerId string) (*stripe.PaymentIntent, error)
	GetPaymentIntent(paymentIntentID string) (*stripe.PaymentIntent, error)
	RefundBackground(paymentIntentID string, refundPrice decimal.Decimal) (string, error)
	CancelPaymentIntent(paymentIntentID string) error
	QueryRefundStatus(refundId string) (string, error)
	GetMonthlyCreatedInvoices(date time.Time) ([]*forms.StripeInvoice, error)
	CalculateTax(billingInfo *forms.BillingInformation, subCode string, price decimal.Decimal) (*forms.StripeCalculateTaxResult, error)
	CreateTaxRate(rate *forms.TaxRate) (*forms.TaxRate, error)
	GetTaxRate(rateID string) (*forms.TaxRate, error)
	InactiveTaxRate(rateID string) error
	GetStripeInvoice(userCode string, invoiceID string) (*forms.StripeInvoiceDetail, error)
	PushInvoice(paymentInfo *forms.PaymentMethod, billingCompany *forms.BillingCompany, invoices []forms.Invoice, chargeMode InvoiceChargeMode) (*forms.Invoice, error)
}

var (
	RefundFailCount = promauto.NewCounter(
		prometheus.CounterOpts{
			Namespace: "dbaas",
			Subsystem: "billing",
			Name:      "create_refund_fail",
		})
	CancelPaymentFailCount = promauto.NewCounter(
		prometheus.CounterOpts{
			Namespace: "dbaas",
			Subsystem: "billing",
			Name:      "cancel_payment_fail",
		})
)

func GetStripeProvider() (StripePaymentHandler, error) {
	handler, err := GetPaymentHandler().GetProvider(entity.PaymentType_STRIPE)
	if err != nil {
		return nil, err
	}
	return handler.(StripePaymentHandler), nil
}

type StripePaymentHandlerImpl struct {
}

// getStripeClient returns the default Stripe API client used by the handler.
// It is a package-level variable so tests can substitute a stub backed by an
// httptest server while still relying on the same *scclient.API surface.
var getStripeClient = func() *scclient.API {
	sc := &scclient.API{}
	sc.Init(config.Get().GetString(config.StripeSecretKey), nil)
	return sc
}

func getSubStripeClient(subCode string) (*scclient.API, error) {
	sc := &scclient.API{}
	secKeys := config.Get().GetString(config.StripeSubAccountSecretKeys) // example subCodeA:secretKeyA,subCodeB:secretKeyB
	index := strings.Index(secKeys, subCode)
	if index < 0 {
		return nil, errors.New("unknown subCode")
	}
	for _, pair := range strings.Split(secKeys, ",") {
		if !strings.HasPrefix(pair, subCode) {
			continue
		}
		kv := strings.Split(pair, ":")
		if len(kv) != 2 {
			return nil, errors.New("failed to parse stripe sub account secret keys")
		}
		val := kv[1]
		sc.Init(val, nil)
		return sc, nil
	}
	return nil, errors.New("failed to init sub account client")
}

const (
	//StripeDBaaSIDLabel is the dbaas tenant id label name in stripe
	StripeDBaaSIDLabel = "dbaas-id"
)

func stripeEnabled() bool {
	return config.Get().GetBool(config.StripePaymentEnable)
}

func (s *StripePaymentHandlerImpl) Setup(tenant *forms.Tenant) (string, error) {
	if !stripeEnabled() {
		return "", nil
	}
	tenantID := tenant.TenantID
	client := getStripeClient()
	params := &stripe.CustomerParams{
		Params: stripe.Params{
			IdempotencyKey: stripe.String(fmt.Sprintf("%s-%d", config.Get().Get(config.Project), tenantID)),
		},
		Name:     stripe.String(tenant.Name),
		Email:    stripe.String(tenant.Email),
		Metadata: map[string]string{StripeDBaaSIDLabel: fmt.Sprintf("%d", tenantID)},
	}
	customer, err := client.Customers.New(params)
	if err != nil {
		log.Error("failed to create customer", zap.Uint("tenant id", tenantID), zap.Error(err))
		return "", billing_errors.NewSystemError(err)
	}
	log.Info("created customer on stripe", zap.String("customer id", customer.ID), zap.Uint("tenant id", tenantID))
	return customer.ID, nil
}

func getIdempotencyKey(keyType string, userCode string, invoiceID uint) string {
	return fmt.Sprintf("%s-%s-%d", keyType, userCode, invoiceID)
}

const (
	InvoiceIdempotencyKey = "invoice"
	StripeCurrencyFactor  = 100
	Start                 = "invoice_start"
	End                   = "invoice_end"
)

func (s *StripePaymentHandlerImpl) Payable(tenantID uint, userCode string) (bool, error) {
	if !stripeEnabled() {
		return true, nil
	}
	cards, err := s.ListPaymentMethods(userCode)
	if err != nil {
		return false, err
	}
	log.Info("stripe payable result", zap.Int("cards num", len(cards)))
	return len(cards) > 0, nil
}

func (s *StripePaymentHandlerImpl) SetCardAsDefault(userCode string) (bool, error) {
	if !stripeEnabled() {
		return true, nil
	}
	cards, err := s.ListPaymentMethods(userCode)
	if err != nil {
		return false, err
	}
	log.Info("stripe payable result", zap.Int("cards num", len(cards)))
	return len(cards) > 0, nil
}

func (s *StripePaymentHandlerImpl) getLocalCurrencyComment(price decimal.Decimal, taxRate *forms.TaxRate, fx *forms.InvoiceAppliedFXRate) (string, error) {
	tmplRaw := `Due to regulatory requirements, we will show you the invoice price in {{ .TargetCurrency }}*: 
Total excluding tax: ${{ .Price }} 
{{ .TaxType }}: ${{ .Tax }} ({{ .TaxRate }}% on ${{ .Price }}) 
Total: ${{ .Total}} 

* The {{ .TargetCurrency }} price shown on this invoice is for your reference only. Your final bill will still be paid in {{ .BaseCurrency }}. Currency converted at the average exchange rate on {{ .FXRateDate }}: 1 {{ .BaseCurrency }} = {{ .FXRate }} {{ .TargetCurrency }}.`
	tmpl, err := template.New("tmpl").Parse(tmplRaw)
	if err != nil {
		return "", err
	}
	output := bytes.NewBufferString("")
	localPrice := price.Mul(fx.Value).Truncate(config.Get().GetInt32(config.BillingCostsPrecisionDigits))
	taxRateDisplayName := "Tax"
	taxRateVal := decimal.Zero
	taxRatePercent := "0"
	if taxRate != nil && !s.isUnsupportedTaxRate(taxRate.TaxAbilityReason) {
		taxRateDisplayName = taxRate.DisplayName
		taxRateVal = taxRate.Value.Div(decimal.NewFromInt(100))
		taxRatePercent = taxRate.Value.String()
	}
	tax := localPrice.Mul(taxRateVal).Round(config.Get().GetInt32(config.BillingCostsPrecisionDigits))
	total := localPrice.Add(tax)
	param := struct {
		Price          string
		Tax            string
		TaxType        string
		TaxRate        string
		Total          string
		TargetCurrency string
		BaseCurrency   string
		FXRate         string
		FXRateDate     string
	}{
		Price:          localPrice.String(),
		Tax:            tax.String(),
		TaxType:        taxRateDisplayName,
		TargetCurrency: strings.ToUpper(string(fx.Target)),
		BaseCurrency:   strings.ToUpper(string(fx.Base)),
		TaxRate:        taxRatePercent,
		FXRateDate:     fx.Date.Format("January 02, 2006"),
		FXRate:         fx.Value.String(),
		Total:          total.String(),
	}
	err = tmpl.Execute(output, param)
	if err != nil {
		return "", err
	}
	return output.String(), nil
}

func (s *StripePaymentHandlerImpl) Pay(paymentInfo *forms.PaymentMethod, billingCompany *forms.BillingCompany, invoice *forms.Invoice) (*forms.Invoice, error) {
	if !stripeEnabled() {
		return invoice, nil
	}
	applyTax := func(params *stripe.InvoiceParams) {
		if invoice.TaxRate.TaxAbilityReason == string(stripe.TaxCalculationTaxBreakdownTaxabilityReasonReverseCharge) {
			params.Description = stripe.String(config.Get().GetString(config.TaxReverseChargeDescription))
		}
		if s.isUnsupportedTaxRate(invoice.TaxRate.TaxAbilityReason) {
			params.DefaultTaxRates = []*string{stripe.String(config.Get().GetString(config.TaxNotCollectingTaxRateID))}
			params.Description = stripe.String(config.Get().GetString(config.TaxNotCollectingDescription))
		} else {
			params.DefaultTaxRates = []*string{&invoice.TaxRate.StripeTaxRateID}
		}
	}
	draft, err := s.buildDraftInvoice(paymentInfo, billingCompany, invoice, invoicePushMeta([]forms.Invoice{*invoice}, ChargeModeAuto), applyTax, "")
	if err != nil {
		return nil, err
	}
	if err := s.addInvoiceLineItems(paymentInfo, draft.ID, invoice); err != nil {
		return nil, err
	}
	finalized, err := s.finalizeInvoice(draft.ID, true)
	if err != nil {
		return nil, err
	}
	if finalized != nil && finalized.ID != "" {
		invoice.StripeInvoiceID = finalized.ID
	} else {
		invoice.StripeInvoiceID = draft.ID
	}
	invoice.ChargeMode = "auto"
	return invoice, nil
}

// buildDraftInvoice creates a Stripe draft invoice with the common fields
// (customer, metadata, custom fields, on-behalf-of, tax, FX comment).
// The caller supplies extra metadata and a tax-application callback;
// the idempotency key, tax-rate suffix, and default tax rates are
// handled internally.
func (s *StripePaymentHandlerImpl) buildDraftInvoice(
	paymentInfo *forms.PaymentMethod,
	billingCompany *forms.BillingCompany,
	invoice *forms.Invoice,
	extraMeta map[string]string,
	applyTax func(params *stripe.InvoiceParams),
	idempotencySuffix string,
) (*stripe.Invoice, error) {
	client := getStripeClient()
	// The idempotency key must be stable per logical request. A merged
	// batch's composition (merged_from_invoice_ids) is part of the
	// request, so a different batch for the same first invoice MUST get
	// a different key — otherwise Stripe rejects the retry with
	// idempotency_key_reused_with_different_request.
	key := getIdempotencyKey(InvoiceIdempotencyKey, paymentInfo.PaymentCode, invoice.InvoiceID)
	if idempotencySuffix != "" {
		key = key + "-" + idempotencySuffix
	}
	params := &stripe.InvoiceParams{
		Customer: &paymentInfo.PaymentCode,
		Metadata: extraMeta,
		Params: stripe.Params{
			IdempotencyKey: stripe.String(key),
		},
		CustomFields: []*stripe.InvoiceCustomFieldParams{
			{Name: stripe.String("Bill start"), Value: stripe.String(invoice.StartDate.Format("2006-01-02 15:04:05"))},
			{Name: stripe.String("Bill end"), Value: stripe.String(invoice.EndDate.Add(-time.Second).Format("2006-01-02 15:04:05"))},
		},
	}
	if billingCompany.PaymentSubCode != "" {
		params.OnBehalfOf = stripe.String(billingCompany.PaymentSubCode)
		params.TransferData = &stripe.InvoiceTransferDataParams{Destination: stripe.String(billingCompany.PaymentSubCode)}
	}
	if billingCompany.StripeTaxID != "" {
		params.AccountTaxIDs = []*string{stripe.String(billingCompany.StripeTaxID)}
	}
	if billingCompany.DisplayLocalCurrencyCommentOnInvoice && invoice.Meta.AppliedFXRate != nil {
		comment, err := s.getLocalCurrencyComment(invoice.CostPrice, invoice.TaxRate, invoice.Meta.AppliedFXRate)
		if err != nil {
			return nil, err
		}
		params.Footer = &comment
	}
	if invoice.TaxRate != nil {
		applyTax(params)
		// Bind the idempotency key to the tax rate so a retry with a
		// changed rate creates a fresh draft instead of reusing a
		// stale cached one carrying the old rate.
		suffixedKey := *params.IdempotencyKey + invoice.TaxRate.StripeTaxRateID
		params.IdempotencyKey = &suffixedKey
	}
	in, err := client.Invoices.New(params)
	if err != nil {
		log.Error("failed to create stripe draft invoice", zap.Error(err))
		return nil, billing_errors.NewSystemError(err)
	}
	return in, nil
}

// addInvoiceLineItems adds the per-category line items (and discount items)
// for a single local invoice onto a Stripe draft invoice.
func (s *StripePaymentHandlerImpl) addInvoiceLineItems(
	paymentInfo *forms.PaymentMethod,
	stripeInvoiceID string,
	invoice *forms.Invoice,
) error {
	client := getStripeClient()
	categoryUnitsCosts := invoice.CategoryUnitsCosts
	if invoice.Type == entity.InvoiceType_COMMITMENT {
		categoryUnitsCosts = map[string]forms.Price{config.Get().GetString(config.CommitmentInvoiceItem): invoice.Price}
	}
	for category, categoryPrice := range categoryUnitsCosts {
		rawIntPrice := categoryPrice.RawPrice.Mul(decimal.NewFromInt(StripeCurrencyFactor)).IntPart()
		actualIntPrice := categoryPrice.CostPrice.Mul(decimal.NewFromInt(StripeCurrencyFactor)).IntPart()
		discountIntPrice := rawIntPrice - actualIntPrice
		item := &stripe.InvoiceItemParams{
			Invoice:     stripe.String(stripeInvoiceID),
			Params:      stripe.Params{IdempotencyKey: stripe.String(getIdempotencyKey(category, paymentInfo.PaymentCode, invoice.InvoiceID))},
			Amount:      stripe.Int64(rawIntPrice),
			Currency:    stripe.String(string(stripe.CurrencyUSD)),
			Customer:    stripe.String(paymentInfo.PaymentCode),
			Description: stripe.String(category),
		}
		if _, err := client.InvoiceItems.New(item); err != nil {
			return billing_errors.NewSystemError(err)
		}
		if discountIntPrice != 0 {
			discountItem := &stripe.InvoiceItemParams{
				Invoice:     stripe.String(stripeInvoiceID),
				Params:      stripe.Params{IdempotencyKey: stripe.String(getIdempotencyKey(fmt.Sprintf("%s Discount", category), paymentInfo.PaymentCode, invoice.InvoiceID))},
				Amount:      stripe.Int64(-discountIntPrice),
				Currency:    stripe.String(string(stripe.CurrencyUSD)),
				Customer:    stripe.String(paymentInfo.PaymentCode),
				Description: stripe.String(fmt.Sprintf("%s Discount", category)),
			}
			if _, err := client.InvoiceItems.New(discountItem); err != nil {
				return billing_errors.NewSystemError(err)
			}
		}
	}
	return nil
}

// finalizeInvoice finalizes a Stripe draft invoice with the given auto-advance setting.
func (s *StripePaymentHandlerImpl) finalizeInvoice(invoiceID string, autoAdvance bool) (*stripe.Invoice, error) {
	client := getStripeClient()
	finalized, err := client.Invoices.FinalizeInvoice(invoiceID, &stripe.InvoiceFinalizeInvoiceParams{
		AutoAdvance: stripe.Bool(autoAdvance),
	})
	if err != nil {
		return nil, billing_errors.NewSystemError(err)
	}
	return finalized, nil
}

// InvoiceChargeMode describes how Stripe should attempt collection after the
// invoice has been pushed. Auto lets Stripe auto-charge the customer's default
// payment method; Manual leaves the invoice in "open" state for Finance to
// handle separately.
type InvoiceChargeMode string

const (
	// ChargeModeAuto means the pushed invoice should auto-charge the customer.
	ChargeModeAuto InvoiceChargeMode = "auto"
	// ChargeModeManual means the pushed invoice should NOT auto-charge and
	// Finance will collect payment through a separate path.
	ChargeModeManual InvoiceChargeMode = "manual"
)

// Metadata keys written onto the Stripe draft invoice so Finance can trace a
// Stripe invoice back to the local invoices that produced it. Every push path
// (Pay / PushInvoice) writes the full canonical set via invoicePushMeta —
// invoice_start / invoice_end carry the covered billing period, merged_*
// carry the originating local invoices.
const (
	PushMetadataMergedInvoiceIDs = "merged_from_invoice_ids"
	PushMetadataMergedMonths     = "merged_months"
	PushMetadataChargeMode       = "charge_mode"
)

// invoicePushMeta builds the canonical invoice metadata for a push batch.
// All keys are written unconditionally — a single-invoice push (via Pay or
// PushInvoice) produces the same shape as a merged batch, so downstream
// consumers (AR table month attribution, ops dashboards) never have to guess
// which keys a given Stripe invoice carries.
//
// Semantics:
//   - invoice_start / invoice_end: unix seconds of the batch's covered
//     billing period (min StartDate .. max EndDate). For a single invoice
//     this is exactly its own period, matching the historical Pay() behavior.
//     Zero-value times (invoices without a period) are ignored; keys are
//     omitted when no invoice in the batch carries a period.
//   - merged_from_invoice_ids: local invoice ids in batch order, comma joined.
//   - merged_months: distinct billing months ("yyyy-mm") of the batch, sorted
//     ascending. A carryover merge covering June and August therefore reads
//     "2026-06,2026-08" and the newest month is what AR attributes the
//     invoice to.
//   - charge_mode: auto / manual.
func invoicePushMeta(invoices []forms.Invoice, mode InvoiceChargeMode) map[string]string {
	meta := map[string]string{
		PushMetadataChargeMode: string(mode),
	}
	if len(invoices) == 0 {
		return meta
	}
	ids := make([]string, 0, len(invoices))
	monthSet := make(map[string]struct{}, len(invoices))
	var start, end time.Time
	for _, inv := range invoices {
		ids = append(ids, strconv.FormatUint(uint64(inv.InvoiceID), 10))
		monthSet[inv.StartDate.Format("2006-01")] = struct{}{}
		if !inv.StartDate.IsZero() && (start.IsZero() || inv.StartDate.Before(start)) {
			start = inv.StartDate
		}
		if !inv.EndDate.IsZero() && (end.IsZero() || inv.EndDate.After(end)) {
			end = inv.EndDate
		}
	}
	months := make([]string, 0, len(monthSet))
	for m := range monthSet {
		if m != "" && m != "0001-01" {
			months = append(months, m)
		}
	}
	sort.Strings(months)
	meta[PushMetadataMergedInvoiceIDs] = strings.Join(ids, ",")
	meta[PushMetadataMergedMonths] = strings.Join(months, ",")
	if !start.IsZero() {
		meta[Start] = strconv.FormatInt(start.Unix(), 10)
	}
	if !end.IsZero() {
		meta[End] = strconv.FormatInt(end.Unix(), 10)
	}
	return meta
}

// PushInvoice wraps Stripe Pay (create draft invoice + add invoice items +
// finalize + send) for the auto-paying flow. It may merge several local
// invoices into a single Stripe invoice. The chargeMode decides whether Stripe
// should auto-charge the customer (single-month normal push) or leave the
// invoice open for manual collection (multi-month accumulated push).
//
// When stripe is disabled, PushInvoice is a no-op and returns the first input
// invoice unchanged.
func (s *StripePaymentHandlerImpl) PushInvoice(
	paymentInfo *forms.PaymentMethod,
	billingCompany *forms.BillingCompany,
	invoices []forms.Invoice,
	chargeMode InvoiceChargeMode,
) (*forms.Invoice, error) {
	if !stripeEnabled() {
		if len(invoices) == 0 {
			return nil, nil
		}
		first := invoices[0]
		return &first, nil
	}
	if paymentInfo == nil || len(invoices) == 0 {
		return nil, billing_errors.NewInvalidInputError(errors.New("paymentInfo and invoices are required"))
	}
	first := invoices[0]
	// Metadata is derived from the batch itself (invoicePushMeta) — no
	// caller-supplied overrides: merged_months always reflects the months the
	// line items actually cover, never the paying run's month.
	metadata := invoicePushMeta(invoices, chargeMode)

	// Use the first invoice that carries a tax rate for tax-related
	// draft fields. When the batch includes carryover invoices (which
	// have TaxRate == nil — it's written at SENT time), invoices[0]
	// might be a carried invoice with no rate, silently skipping
	// DefaultTaxRates on the entire Stripe draft.
	taxInvoice := &first
	for i := range invoices {
		if invoices[i].TaxRate != nil {
			taxInvoice = &invoices[i]
			break
		}
	}
	applyTax := func(params *stripe.InvoiceParams) {
		if taxInvoice.TaxRate.TaxAbilityReason == string(stripe.TaxCalculationTaxBreakdownTaxabilityReasonReverseCharge) {
			params.Description = stripe.String(config.Get().GetString(config.TaxReverseChargeDescription))
		}
		if s.isUnsupportedTaxRate(taxInvoice.TaxRate.TaxAbilityReason) {
			params.DefaultTaxRates = []*string{stripe.String(config.Get().GetString(config.TaxNotCollectingTaxRateID))}
			params.Description = stripe.String(config.Get().GetString(config.TaxNotCollectingDescription))
		} else {
			params.DefaultTaxRates = []*string{&taxInvoice.TaxRate.StripeTaxRateID}
		}
	}
	// When first is a carried invoice (TaxRate == nil, written at SENT
	// time), borrow the tax rate from an in-month invoice so applyTax
	// runs and the draft gets DefaultTaxRates. taxInvoice is guaranteed
	// non-nil with TaxRate set if any invoice in the batch has a rate.
	if first.TaxRate == nil && taxInvoice != nil && taxInvoice.TaxRate != nil {
		first.TaxRate = taxInvoice.TaxRate
	}
	draft, err := s.buildDraftInvoice(paymentInfo, billingCompany, &first, metadata, applyTax, metadata[PushMetadataMergedInvoiceIDs])
	if err != nil {
		return nil, err
	}

	// Add line items for every invoice in the batch.
	for _, invoice := range invoices {
		if err := s.addInvoiceLineItems(paymentInfo, draft.ID, &invoice); err != nil {
			return nil, err
		}
	}

	// When keep-draft mode is enabled for carryover-inclusive pushes,
	// leave the Stripe invoice as a draft so ops can manually review
	// and adjust line items before finalizing. The invoice is NOT
	// finalized or charged — it sits in Stripe as a draft.
	if config.Get().GetBool(config.StripePushKeepDraftOnChargeModeManual) && chargeMode == ChargeModeManual {
		out := first
		out.StripeInvoiceID = draft.ID
		out.ChargeMode = string(chargeMode)
		return &out, nil
	}

	autoAdvance := chargeMode == ChargeModeAuto
	finalized, err := s.finalizeInvoice(draft.ID, autoAdvance)
	if err != nil {
		log.Error("failed to finalize stripe invoice", zap.Error(err), zap.String("stripe_invoice_id", draft.ID))
		return nil, billing_errors.NewSystemError(err)
	}
	stripeInvoiceID := draft.ID
	if finalized != nil && finalized.ID != "" {
		stripeInvoiceID = finalized.ID
	}
	out := first
	out.StripeInvoiceID = stripeInvoiceID
	out.ChargeMode = string(chargeMode)
	return &out, nil
}

func (*StripePaymentHandlerImpl) SetupIntent(userCode string) (*forms.SetupIntent, error) {
	if !stripeEnabled() {
		return &forms.SetupIntent{}, nil
	}
	params := &stripe.SetupIntentParams{
		Customer: stripe.String(userCode),
	}
	client := getStripeClient()
	intent, err := client.SetupIntents.New(params)
	if err != nil {
		return nil, billing_errors.NewSystemError(err)
	}
	return &forms.SetupIntent{
		ClientSecret: intent.ClientSecret,
		PublicKey:    config.Get().GetString(config.StripePublicKey),
	}, nil
}

func newPaymentMethod(method *stripe.PaymentMethod) *forms.StripePaymentMethod {
	var card *forms.Card
	if method.Card != nil {
		card = &forms.Card{
			Brand:       string(method.Card.Brand),
			Country:     method.Card.Country,
			ExpMonth:    method.Card.ExpMonth,
			ExpYear:     method.Card.ExpYear,
			Fingerprint: method.Card.Fingerprint,
			Funding:     string(method.Card.Funding),
			Last4:       method.Card.Last4,
		}
	}
	var detail *forms.BillingDetails
	if method.BillingDetails != nil {
		var addr *forms.Address
		if method.BillingDetails.Address != nil {
			addr = &forms.Address{
				City:       method.BillingDetails.Address.City,
				Country:    method.BillingDetails.Address.Country,
				Line1:      method.BillingDetails.Address.Line1,
				Line2:      method.BillingDetails.Address.Line2,
				PostalCode: method.BillingDetails.Address.PostalCode,
				State:      method.BillingDetails.Address.State,
			}
		}
		detail = &forms.BillingDetails{
			Address: addr,
			Email:   &method.BillingDetails.Email,
			Name:    method.BillingDetails.Name,
			Phone:   &method.BillingDetails.Phone,
		}
	}
	return &forms.StripePaymentMethod{
		ID:             method.ID,
		Card:           card,
		BillingDetails: detail,
	}
}

func (*StripePaymentHandlerImpl) ListPaymentMethods(userCode string) ([]forms.StripePaymentMethod, error) {
	if !stripeEnabled() {
		return []forms.StripePaymentMethod{}, nil
	}
	params := &stripe.PaymentMethodListParams{
		Customer: stripe.String(userCode),
		Type:     stripe.String("card"),
	}
	it := getStripeClient().PaymentMethods.List(params)
	var items = make([]forms.StripePaymentMethod, 0)
	for it.Next() {
		items = append(items, *newPaymentMethod(it.PaymentMethod()))
	}
	err := it.Err()
	if err != nil {
		return nil, billing_errors.NewSystemError(err)
	}
	return items, nil
}

func newBillingDetailParams(method *forms.StripePaymentMethod) *stripe.PaymentMethodBillingDetailsParams {
	var detail *stripe.PaymentMethodBillingDetailsParams
	if method.BillingDetails != nil {
		var addr *stripe.AddressParams
		if method.BillingDetails.Address != nil {
			addr = &stripe.AddressParams{
				City:       stripe.String(method.BillingDetails.Address.City),
				Country:    stripe.String(method.BillingDetails.Address.Country),
				Line1:      stripe.String(method.BillingDetails.Address.Line1),
				Line2:      stripe.String(method.BillingDetails.Address.Line2),
				PostalCode: stripe.String(method.BillingDetails.Address.PostalCode),
				State:      stripe.String(method.BillingDetails.Address.State),
			}
		}
		detail = &stripe.PaymentMethodBillingDetailsParams{
			Address: addr,
			Email:   method.BillingDetails.Email,
			Name:    stripe.String(method.BillingDetails.Name),
			Phone:   method.BillingDetails.Phone,
		}
	}
	return detail
}

func (s *StripePaymentHandlerImpl) UpdatePaymentMethod(userCode, methodId string, method *forms.StripePaymentMethod) (*forms.StripePaymentMethod, error) {
	if !stripeEnabled() {
		return nil, nil
	}

	_, err := s.GetCustomerPaymentMethod(userCode, methodId)
	if err != nil {
		return nil, err
	}
	params := &stripe.PaymentMethodParams{
		BillingDetails: newBillingDetailParams(method),
	}
	paymentMethod, err := getStripeClient().PaymentMethods.Update(methodId, params)
	if err != nil {
		return nil, billing_errors.NewSystemError(err)
	}

	log.Info("update a payment method from customer", zap.String("method id", methodId))
	return newPaymentMethod(paymentMethod), nil
}

func (s *StripePaymentHandlerImpl) DeletePaymentMethod(userCode, methodId string) error {
	if !stripeEnabled() {
		return nil
	}
	// check whether PaymentMethod exist
	_, err := s.GetCustomerPaymentMethod(userCode, methodId)
	if err != nil {
		return err
	}
	// check card nums
	cards, err := s.ListPaymentMethods(userCode)
	if err != nil {
		return err
	}
	if len(cards) <= 1 {
		log.Error("you must keep at least one credit card", zap.String("methodId", methodId), zap.String("userCode", userCode))
		return billing_errors.NewInvalidInputError(errors.New("you must keep at least one credit card"))
	}
	// check whether delete default PaymentMethod
	customer, err := s.GetCustomerInfo(userCode)
	if err != nil {
		return err
	}
	if customer.DefaultSource == methodId {
		log.Error("the default PaymentMethod is forbidden to delete", zap.String("methodId", methodId), zap.String("userCode", userCode))
		return billing_errors.NewInvalidInputError(fmt.Errorf("the default PaymentMethod is forbidden to delete"))
	}
	pm, err := getStripeClient().PaymentMethods.Detach(methodId, nil)
	if err != nil {
		return billing_errors.NewSystemError(err)
	}
	log.Info("delete a payment method from customer", zap.String("method id", pm.ID))
	return nil
}

func (s *StripePaymentHandlerImpl) GetPaymentMethod(methodId string) (*forms.StripePaymentMethod, error) {
	if !stripeEnabled() {
		return &forms.StripePaymentMethod{}, nil
	}
	paymentMethod, err := getStripeClient().PaymentMethods.Get(methodId, nil)
	if err != nil {
		return nil, billing_errors.NewSystemError(err)
	}
	return newPaymentMethod(paymentMethod), nil
}

func (s *StripePaymentHandlerImpl) GetCustomerPaymentMethod(userCode, methodId string) (*forms.StripePaymentMethod, error) {
	if !stripeEnabled() {
		return &forms.StripePaymentMethod{}, nil
	}
	paymentMethod, err := getStripeClient().PaymentMethods.Get(methodId, nil)
	if err != nil {
		return nil, billing_errors.NewSystemError(err)
	}

	if paymentMethod.Customer.ID != userCode {
		return nil, fmt.Errorf("payment method %s is not belongs to customer %s", methodId, userCode)
	}
	return newPaymentMethod(paymentMethod), nil
}

func (s *StripePaymentHandlerImpl) SetPaymentMethodDefault(userCode, methodId string) (*forms.StripePaymentMethod, error) {
	if !stripeEnabled() {
		return &forms.StripePaymentMethod{}, nil
	}
	log.Info("set default paymentMethod with methodId", zap.String("userCode", userCode), zap.String("methodId", methodId))
	stripePaymentMethod, err := s.GetCustomerPaymentMethod(userCode, methodId)
	if err != nil {
		return nil, err
	}
	_, err = getStripeClient().Customers.Update(userCode, &stripe.CustomerParams{InvoiceSettings: &stripe.CustomerInvoiceSettingsParams{DefaultPaymentMethod: &methodId}})
	if err != nil {
		return nil, billing_errors.NewSystemError(err)
	}
	return stripePaymentMethod, nil
}

func newStripeCustomerInfo(c *stripe.Customer) *forms.StripeCustomerInfo {
	var sourceID string
	invoiceSetting := c.InvoiceSettings
	if invoiceSetting != nil && invoiceSetting.DefaultPaymentMethod != nil {
		sourceID = invoiceSetting.DefaultPaymentMethod.ID
	}
	ret := &forms.StripeCustomerInfo{
		ID:            c.ID,
		TenantID:      c.Metadata[StripeDBaaSIDLabel],
		Email:         c.Email,
		Name:          c.Name,
		Description:   c.Description,
		DefaultSource: sourceID,
	}
	address := &forms.Address{}
	if c.Address != nil {
		address = &forms.Address{
			City:       c.Address.City,
			Country:    c.Address.Country,
			Line1:      c.Address.Line1,
			Line2:      c.Address.Line2,
			PostalCode: c.Address.PostalCode,
			State:      c.Address.State,
		}
	}
	ret.Address = address
	return ret
}

func (s *StripePaymentHandlerImpl) GetCustomerInfo(userCode string) (*forms.StripeCustomerInfo, error) {
	if !stripeEnabled() {
		return nil, nil
	}
	sc, err := getStripeClient().Customers.Get(userCode, nil)
	if err != nil {
		return nil, billing_errors.NewSystemError(err)
	}
	return newStripeCustomerInfo(sc), nil
}

func (s *StripePaymentHandlerImpl) UpdateCustomerBillingInfo(userCode string, billingInfo *forms.BillingInformation) (*forms.StripeCustomerInfo, error) {
	if !stripeEnabled() {
		return nil, nil
	}
	customer := getStripeClient().Customers
	params := &stripe.CustomerParams{
		Name:  stripe.String(billingInfo.CompanyName),
		Email: stripe.String(billingInfo.BillingEmail),
		Address: &stripe.AddressParams{
			Line1:      stripe.String(billingInfo.Line1),
			Line2:      stripe.String(billingInfo.Line2),
			City:       stripe.String(billingInfo.City),
			State:      stripe.String(billingInfo.State),
			Country:    stripe.String(billingInfo.CountryCode),
			PostalCode: stripe.String(billingInfo.PostalCode),
		},
	}
	sc, err := customer.Update(userCode, params)
	if err != nil {
		return nil, billing_errors.NewSystemError(err)
	}
	err = s.TryUpdateCustomerTaxIDs(userCode, &billingInfo.TaxInfo)
	if err != nil {
		log.Error("failed to update tax id", zap.Error(err), zap.String("userCode", userCode))
	}
	return newStripeCustomerInfo(sc), nil
}

func (s *StripePaymentHandlerImpl) TryUpdateCustomerTaxIDs(userCode string, taxInfo *forms.TaxInfo) error {
	taxids := getStripeClient().TaxIDs
	iter := taxids.List(&stripe.TaxIDListParams{Customer: stripe.String(userCode)})
	if err := iter.Err(); err != nil {
		return err
	}
	taxIDs := iter.TaxIDList().Data
	needDelTaxIDs := len(taxIDs) != 0 && (taxInfo == nil || taxInfo.TaxID == "" || (string(taxIDs[0].Type) != taxInfo.TaxIDType || taxIDs[0].Value != taxInfo.TaxID))
	needNewTaxID := taxInfo != nil && taxInfo.TaxID != "" && (len(taxIDs) == 0 || (len(taxIDs) != 0 && string(taxIDs[0].Type) != taxInfo.TaxIDType || taxIDs[0].Value != taxInfo.TaxID))
	if needDelTaxIDs {
		for _, oldTaxID := range taxIDs {
			_, err := taxids.Del(oldTaxID.ID, &stripe.TaxIDParams{Customer: stripe.String(userCode)})
			if err != nil {
				log.Error("failed to delete old tax id", zap.Error(err), zap.String("userCode", userCode), zap.String("taxIDStripeID", oldTaxID.ID))
				continue
			}
		}
	}
	if needNewTaxID {
		_, err := taxids.New(&stripe.TaxIDParams{
			Customer: stripe.String(userCode),
			Type:     stripe.String(taxInfo.TaxIDType),
			Value:    stripe.String(taxInfo.TaxID),
		})
		if err != nil {
			return billing_errors.NewSystemError(err)
		}
	}
	return nil
}

func newStripeInvoice(invoice *stripe.Invoice) *forms.StripeInvoice {
	dueDate := invoice.DueDate
	if dueDate == 0 {
		dueDate = invoice.StatusTransitions.PaidAt
	}
	var startDate, endDate time.Time
	if t, ok := invoice.Metadata[Start]; ok {
		ts, err := strconv.ParseInt(t, 10, 64)
		if err != nil {
			log.Error("failed to parse start", zap.String("start", t))
		} else {
			startDate = time.Unix(ts, 0)
		}
	}
	if t, ok := invoice.Metadata[End]; ok {
		ts, err := strconv.ParseInt(t, 10, 64)
		if err != nil {
			log.Error("failed to parse end", zap.String("end", t))
		} else {
			endDate = time.Unix(ts, 0)
		}
	}
	ret := &forms.StripeInvoice{
		AmountDue:         decimal.NewFromInt(invoice.AmountDue).Div(decimal.NewFromInt(100)),
		AmountPaid:        decimal.NewFromInt(invoice.AmountPaid).Div(decimal.NewFromInt(100)),
		Created:           invoice.Created,
		Currency:          string(invoice.Currency),
		DueDate:           dueDate,
		CreditUsed:        decimal.NewFromInt(invoice.EndingBalance - invoice.StartingBalance).Div(decimal.NewFromInt(100)),
		ID:                invoice.ID,
		Number:            invoice.Number,
		InvoicePDF:        invoice.InvoicePDF,
		Total:             decimal.NewFromInt(invoice.Total).Div(decimal.NewFromInt(100)),
		Paid:              invoice.Paid,
		Status:            string(invoice.Status),
		Tax:               decimal.NewFromInt(invoice.Tax).Div(decimal.NewFromInt(100)),
		SubTotal:          decimal.NewFromInt(invoice.Subtotal).Div(decimal.NewFromInt(100)),
		CustomerID:        invoice.Customer.ID,
		StartDate:         startDate,
		EndDate:           endDate,
		Metadata:          invoice.Metadata,
		StatusTransitions: invoice.StatusTransitions,
	}
	if len(invoice.DefaultTaxRates) != 0 {
		ret.TaxRate = decimal.NewFromFloat(invoice.DefaultTaxRates[0].Percentage)
		if len(invoice.DefaultTaxRates) > 1 {
			log.Error("too many tax rates detected in stripe invoice", zap.String("number", invoice.Number), zap.String("id", invoice.ID))
		}
	}
	return ret
}

func (s *StripePaymentHandlerImpl) listStripeInvoices(params *stripe.InvoiceListParams) ([]*forms.StripeInvoice, error) {
	if params.Limit == nil {
		limit := int64(100)
		params.Limit = &limit
	}
	it := getStripeClient().Invoices.List(params)
	if it.Err() != nil {
		log.Error("failed to list invoices", zap.Error(it.Err()))
		return nil, it.Err()
	}
	var items []*forms.StripeInvoice
	for it.Next() {
		invoice := it.Invoice()
		if invoice.Status == stripe.InvoiceStatusDraft {
			continue
		}
		items = append(items, newStripeInvoice(invoice))
	}
	return items, nil
}

func (s *StripePaymentHandlerImpl) ListStripeInvoices(userCode string) ([]*forms.StripeInvoice, error) {
	statuses := []string{string(stripe.InvoiceStatusOpen), string(stripe.InvoiceStatusPaid)}
	var ret []*forms.StripeInvoice
	for _, st := range statuses {
		params := &stripe.InvoiceListParams{Customer: stripe.String(userCode), Status: &st}
		res, err := s.listStripeInvoices(params)
		if err != nil {
			return nil, err
		}
		ret = append(ret, res...)
	}
	return ret, nil
}

func (s *StripePaymentHandlerImpl) ListStripeInvoicesWithCache(userCode string) ([]*forms.StripeInvoice, error) {
	const cacheKey = "ListStripeInvoices"
	var result []*forms.StripeInvoice
	hit := cachehandler.Get().Get(cacheKey, &result, userCode)
	if hit {
		params := &stripe.InvoiceListParams{Customer: stripe.String(userCode), Status: stripe.String(string(stripe.InvoiceStatusOpen))}
		openInvoices, err := s.listStripeInvoices(params)
		if err != nil {
			return nil, err
		}
		count := 0
		for _, invoice := range result {
			if invoice.Status == string(stripe.InvoiceStatusOpen) {
				count++
			}
		}
		if count == len(openInvoices) {
			return result, nil
		}
		log.Info("open invoices changed, do not use cache", zap.Int("cacheCount", count), zap.Int("realCount", len(openInvoices)))
	}
	result, err := s.ListStripeInvoices(userCode)
	if err != nil {
		return nil, err
	}
	cachehandler.Get().Set(cacheKey, result, userCode)
	return result, nil
}

func (s *StripePaymentHandlerImpl) GetStripeInvoice(userCode string, invoiceID string) (*forms.StripeInvoiceDetail, error) {
	if !stripeEnabled() {
		return nil, billing_errors.NewSystemError(fmt.Errorf("stripe is not enable"))
	}
	inv, err := getStripeClient().Invoices.Get(invoiceID, nil)
	if err != nil {
		return nil, err
	}
	if inv.Customer.ID != userCode {
		return nil, billing_errors.NewSystemError(fmt.Errorf("stripe customer ID not allowed"))
	}
	res := &forms.StripeInvoiceDetail{
		StripeInvoice: *newStripeInvoice(inv),
		PubicKey:      config.Get().GetString(config.StripePublicKey),
	}
	if inv.PaymentIntent != nil {
		res.PaymentIntentID = inv.PaymentIntent.ID
		intent, err := getStripeClient().PaymentIntents.Get(inv.PaymentIntent.ID, nil)
		if err != nil {
			return nil, err
		}
		res.PaymentIntentStatus = string(intent.Status)
		res.ClientSecret = intent.ClientSecret
	}

	return res, nil
}

func (*StripePaymentHandlerImpl) NeedMultiInvoiceSendOnce() bool {
	return false
}

func (s *StripePaymentHandlerImpl) PayMultiInvoice(paymentInfo *forms.PaymentMethod, billingCompany *forms.BillingCompany, invoices []forms.Invoice) ([]forms.Invoice, error) {
	return invoices, billing_errors.NewNotImplementedError(errors.New("can not send stripe multi invoice"))
}

func (s *StripePaymentHandlerImpl) ConfirmRequiredConfirmedPaymentIntent(paymentIntentID string) error {
	if !stripeEnabled() {
		return billing_errors.NewSystemError(fmt.Errorf("stripe is not enable"))
	}
	_, err := getStripeClient().PaymentIntents.Confirm(paymentIntentID, nil)
	return err
}

// ConfirmPaymentIntent :create and confirm PaymentIntent, if success, attach PaymentMethod to Customer
func (s *StripePaymentHandlerImpl) ConfirmPaymentIntent(cent decimal.Decimal, paymentMethodID string, customerID string) (string, error) {
	if !stripeEnabled() {
		return "", billing_errors.NewSystemError(fmt.Errorf("stripe is not enable"))
	}
	paymentIntent, err := getStripeClient().PaymentIntents.New(&stripe.PaymentIntentParams{
		Amount:           stripe.Int64(cent.IntPart()),
		Currency:         stripe.String(string(stripe.CurrencyUSD)),
		PaymentMethod:    stripe.String(paymentMethodID),
		Customer:         stripe.String(customerID),
		SetupFutureUsage: stripe.String("off_session"),
		AutomaticPaymentMethods: &stripe.PaymentIntentAutomaticPaymentMethodsParams{
			AllowRedirects: stripe.String("never"),
			Enabled:        stripe.Bool(true),
		},
		PaymentMethodOptions: &stripe.PaymentIntentPaymentMethodOptionsParams{
			Card: &stripe.PaymentIntentPaymentMethodOptionsCardParams{
				RequestThreeDSecure: stripe.String("any"),
			},
		},
	})
	// deal with error: https://stripe.com/docs/error-handling#payment-errors
	// expiredTime、cardNumber、cvc
	if err != nil {
		if stripeErr, ok := err.(*stripe.Error); ok {
			if stripeErr.Type == stripe.ErrorTypeCard {
				if stripeErr.Code == stripe.ErrorCodeInvalidNumber ||
					stripeErr.Code == stripe.ErrorCodeExpiredCard ||
					stripeErr.Code == stripe.ErrorCodeIncorrectCVC ||
					stripeErr.Code == stripe.ErrorCodeInvalidCVC ||
					stripeErr.Code == stripe.ErrorCodeIncorrectNumber ||
					stripeErr.Code == stripe.ErrorCodeCardDeclined {
					return "", billing_errors.NewCardVerificationError(errors.New(stripeErr.Msg), string(stripeErr.Code))
				}
			}
		}
		return "", billing_errors.NewConfirmPaymentIntentError(err)
	}
	// status of PaymentIntent is not success
	if paymentIntent.Status != stripe.PaymentIntentStatusSucceeded {
		return paymentIntent.ID, billing_errors.NewConfirmPaymentIntentFail(fmt.Errorf("confirm PaymentMethod fail, message is: %+v", paymentIntent))
	}
	return paymentIntent.ID, nil
}

// Init3DSPaymentIntent
// create PaymentIntent to start 3DS check, if success, attach PaymentMethod to Customer
func (s *StripePaymentHandlerImpl) Init3DSPaymentIntent(cent decimal.Decimal, paymentMethodID string, customerID string) (*stripe.PaymentIntent, error) {
	if !stripeEnabled() {
		return nil, billing_errors.NewSystemError(fmt.Errorf("stripe is not enable"))
	}
	paymentIntent, err := getStripeClient().PaymentIntents.New(&stripe.PaymentIntentParams{
		Amount:           stripe.Int64(cent.IntPart()),
		Currency:         stripe.String(string(stripe.CurrencyUSD)),
		PaymentMethod:    stripe.String(paymentMethodID),
		Confirm:          stripe.Bool(true),
		Customer:         stripe.String(customerID),
		SetupFutureUsage: stripe.String("off_session"),
		AutomaticPaymentMethods: &stripe.PaymentIntentAutomaticPaymentMethodsParams{
			AllowRedirects: stripe.String("never"),
			Enabled:        stripe.Bool(true),
		},
		PaymentMethodOptions: &stripe.PaymentIntentPaymentMethodOptionsParams{
			Card: &stripe.PaymentIntentPaymentMethodOptionsCardParams{
				RequestThreeDSecure: stripe.String("any"),
			},
		},
	})
	// deal with error: https://stripe.com/docs/error-handling#payment-errors
	// expiredTime、cardNumber、cvc
	if err != nil {
		var stripeErr *stripe.Error
		if errors.As(err, &stripeErr) {
			if stripeErr.Type == stripe.ErrorTypeCard {
				if stripeErr.Code == stripe.ErrorCodeInvalidNumber ||
					stripeErr.Code == stripe.ErrorCodeExpiredCard ||
					stripeErr.Code == stripe.ErrorCodeIncorrectCVC ||
					stripeErr.Code == stripe.ErrorCodeInvalidCVC ||
					stripeErr.Code == stripe.ErrorCodeIncorrectNumber ||
					stripeErr.Code == stripe.ErrorCodeCardDeclined {
					return nil, billing_errors.NewCardVerificationError(errors.New(stripeErr.Msg), string(stripeErr.Code))
				}
			}
		}
		return nil, billing_errors.NewConfirmPaymentIntentError(err)
	}
	switch paymentIntent.Status {
	case stripe.PaymentIntentStatusSucceeded, stripe.PaymentIntentStatusRequiresAction:
		return paymentIntent, nil
	case stripe.PaymentIntentStatusCanceled, stripe.PaymentIntentStatusRequiresPaymentMethod:
		return paymentIntent, billing_errors.NewConfirmPaymentIntentFail(fmt.Errorf("failed to verify card"))
	default:
		log.Error("failed to verify card", zap.Any("payment_intent", paymentIntent))
		return paymentIntent, billing_errors.NewConfirmPaymentIntentFail(fmt.Errorf("failed to verify card"))
	}
}

// RefundBackground :create refund background
func (s *StripePaymentHandlerImpl) RefundBackground(paymentIntentID string, refundPrice decimal.Decimal) (string, error) {
	if !stripeEnabled() {
		return "", billing_errors.NewSystemError(fmt.Errorf("stripe is not enable"))
	}
	refund, err := getStripeClient().Refunds.New(&stripe.RefundParams{
		PaymentIntent: stripe.String(paymentIntentID),
		Amount:        stripe.Int64(refundPrice.IntPart()),
	})
	if err != nil {
		RefundFailCount.Inc()
		return "", billing_errors.NewSystemError(err)
	}
	if refund.Status == stripe.RefundStatusFailed {
		RefundFailCount.Inc()
		return "", billing_errors.NewSystemError(fmt.Errorf("create refund fail, message is: %+v", refund))
	}
	return refund.ID, nil
}

func (s *StripePaymentHandlerImpl) CancelPaymentIntent(paymentIntentID string) error {
	if !stripeEnabled() {
		return billing_errors.NewSystemError(fmt.Errorf("stripe is not enable"))
	}
	_, err := getStripeClient().PaymentIntents.Cancel(paymentIntentID, nil)
	if err != nil {
		CancelPaymentFailCount.Inc()
		return billing_errors.NewSystemError(err)
	}
	return nil
}

func (s *StripePaymentHandlerImpl) QueryRefundStatus(refundId string) (string, error) {
	if !stripeEnabled() {
		return "", billing_errors.NewSystemError(fmt.Errorf("stripe is not enable"))
	}
	refund, err := getStripeClient().Refunds.Get(refundId, nil)
	if err != nil {
		return "", billing_errors.NewSystemError(err)
	}
	return string(refund.Status), nil
}

func (s *StripePaymentHandlerImpl) GetThirdPartyName() string {
	return entity.PaymentType_STRIPE
}

func (s *StripePaymentHandlerImpl) EnableReconciliation() bool {
	return false
}

func (s *StripePaymentHandlerImpl) GetDailyRevenue(date time.Time) ([]*TenantRevenue, error) {
	return nil, billing_errors.NewNotImplementedError(errors.New("don't need"))
}

func (s *StripePaymentHandlerImpl) GetMonthlyRevenue(date time.Time) ([]*TenantRevenue, error) {
	return nil, billing_errors.NewNotImplementedError(errors.New("don't need"))
}

func (s *StripePaymentHandlerImpl) GetMonthlyCreatedInvoices(date time.Time) ([]*forms.StripeInvoice, error) {
	thisMonthFirstDay := timeutils.GetMonthDate(date)
	NextMonthFirstDay := thisMonthFirstDay.AddDate(0, 1, 0)
	MonthAfterNextFirstDay := NextMonthFirstDay.AddDate(0, 1, 0)
	params := &stripe.InvoiceListParams{
		CreatedRange: &stripe.RangeQueryParams{
			GreaterThanOrEqual: thisMonthFirstDay.Unix(),
			LesserThan:         MonthAfterNextFirstDay.Unix(),
		},
	}
	invoices, err := s.listStripeInvoices(params)
	if err != nil {
		return nil, err
	}
	var ret []*forms.StripeInvoice
	for _, in := range invoices {
		if !stripeInvoiceBelongsToMonth(in, thisMonthFirstDay, NextMonthFirstDay) {
			continue
		}
		ret = append(ret, in)
	}
	return ret, nil
}

// stripeInvoiceBelongsToMonth reports whether a Stripe invoice should be
// attributed to the bill month starting at billMonthStart.
//
// Two signals, in order:
//   - invoice_start / invoice_end metadata (written by Pay and PushInvoice):
//     the invoice's StartDate parsing to a date inside the target month. This
//     is the historical path (#781).
//   - merged_months metadata: the invoice belongs to the month of its newest
//     covered billing month (max "yyyy-mm"). The auto-push paying worker
//     stamps every batch with merged_months regardless of whether it merges
//     several local invoices; a single-month push carries its own month and a
//     carryover merge carries e.g. "2026-06,2026-08", so max month == the
//     paying-run month is what lands it in that month's AR table.
func stripeInvoiceBelongsToMonth(in *forms.StripeInvoice, billMonthStart, nextMonthStart time.Time) bool {
	if !in.StartDate.IsZero() && !in.StartDate.Before(billMonthStart) && in.StartDate.Before(nextMonthStart) {
		return true
	}
	months, ok := in.Metadata[PushMetadataMergedMonths]
	if !ok || months == "" {
		return false
	}
	// "yyyy-mm" sorts lexicographically in chronological order.
	maxMonth := ""
	for _, m := range strings.Split(months, ",") {
		if m != "" && m > maxMonth {
			maxMonth = m
		}
	}
	return maxMonth == billMonthStart.Format("2006-01")
}

func (s *StripePaymentHandlerImpl) CalculateTax(billingInfo *forms.BillingInformation, subCode string, price decimal.Decimal) (*forms.StripeCalculateTaxResult, error) {
	if !stripeEnabled() {
		return nil, billing_errors.NewSystemError(fmt.Errorf("stripe is not enable"))
	}
	if billingInfo.CountryCode == "" || (billingInfo.CountryCode == "US" && billingInfo.PostalCode == "") {
		return nil, billing_errors.NewInvalidBillingProfileInputError(errors.New("empty country or postal code"))
	}
	cent := price.Mul(decimal.NewFromInt(StripeCurrencyFactor)).IntPart()
	params := &stripe.TaxCalculationParams{
		Currency: stripe.String(string(stripe.CurrencyUSD)),
		CustomerDetails: &stripe.TaxCalculationCustomerDetailsParams{
			Address: &stripe.AddressParams{
				Line1:      stripe.String(billingInfo.Line1),
				Line2:      stripe.String(billingInfo.Line2),
				City:       stripe.String(billingInfo.City),
				State:      stripe.String(billingInfo.State),
				Country:    stripe.String(billingInfo.CountryCode),
				PostalCode: stripe.String(billingInfo.PostalCode),
			},
			AddressSource: stripe.String(string(stripe.TaxCalculationCustomerDetailsAddressSourceBilling)),
		},
		LineItems: []*stripe.TaxCalculationLineItemParams{
			{
				Amount:    stripe.Int64(cent),
				TaxCode:   stripe.String(config.Get().GetString(config.TiDBCloudStripeProductTaxCode)),
				Reference: stripe.String(config.Get().GetString(config.TiDBCloudStripeProductReference)),
			},
		},
	}
	if billingInfo.TaxID != "" && billingInfo.TaxIDType != "" {
		params.CustomerDetails.TaxIDs = []*stripe.TaxCalculationCustomerDetailsTaxIDParams{{
			Type:  stripe.String(billingInfo.TaxIDType),
			Value: stripe.String(billingInfo.TaxID),
		}}
	}
	params.AddExpand("line_items")
	var sc = getStripeClient()
	var err error
	if subCode != "" {
		sc, err = getSubStripeClient(subCode)
		if err != nil {
			log.Error("failed to get sub account stripe client", zap.Error(err), zap.String("subCode", subCode))
			return nil, billing_errors.NewSystemError(nil)
		}
	}
	calculation := sc.TaxCalculations
	result, err := calculation.New(params)
	if err != nil {
		log.Info("err resp", zap.Error(err))
		if stripeErr, ok := err.(*stripe.Error); ok {
			switch stripeErr.Code {
			case stripe.ErrorCodeTaxIDInvalid, stripe.ErrorCodeInvalidTaxLocation, stripe.ErrorCodeCustomerTaxLocationInvalid:
				return nil, billing_errors.NewInvalidBillingProfileInputError(errors.New(stripeErr.Msg))
			}
		}
		return nil, billing_errors.NewSystemError(err)
	} else if len(result.TaxBreakdown) == 0 {
		// No tax breakdown means no tax is applicable
		ret := &forms.StripeCalculateTaxResult{
			Amount:            decimal.Zero,
			Inclusive:         false,
			TaxAbilityReason:  string(stripe.TaxCalculationTaxBreakdownTaxabilityReasonNotSubjectToTax),
			Country:           billingInfo.CountryCode,
			State:             billingInfo.State,
			PercentageDecimal: "0",
			TaxType:           "",
		}
		return ret, nil
	} else if len(result.TaxBreakdown) == 1 && result.TaxBreakdown[0].TaxRateDetails == nil {
		// Single tax breakdown but no details - treat as no tax
		ret := &forms.StripeCalculateTaxResult{
			Amount:            decimal.NewFromInt(result.TaxBreakdown[0].Amount),
			Inclusive:         result.TaxBreakdown[0].Inclusive,
			TaxAbilityReason:  string(result.TaxBreakdown[0].TaxabilityReason),
			Country:           billingInfo.CountryCode,
			State:             billingInfo.State,
			PercentageDecimal: "0",
			TaxType:           "",
		}
		if ret.TaxType == "" && s.isUnsupportedTaxRate(string(result.TaxBreakdown[0].TaxabilityReason)) {
			ret.TaxType = string(stripe.TaxCalculationTaxBreakdownTaxRateDetailsTaxTypeGST)
		}
		return ret, nil
	} else {
		// Multiple tax breakdowns or single with details
		totalTaxAmount := int64(0)
		var taxAbilityReason string
		var taxType string
		var country, state string

		// Process all tax breakdowns
		for _, breakdown := range result.TaxBreakdown {
			totalTaxAmount += breakdown.Amount
			taxAbilityReason = string(breakdown.TaxabilityReason)

			if breakdown.TaxRateDetails != nil {
				country = breakdown.TaxRateDetails.Country
				if breakdown.TaxRateDetails.State != "" {
					state = breakdown.TaxRateDetails.State
				}
				if breakdown.TaxRateDetails.TaxType != "" {
					taxType = string(breakdown.TaxRateDetails.TaxType)
				}
			}
		}

		ret := &forms.StripeCalculateTaxResult{
			Amount:            decimal.NewFromInt(totalTaxAmount),
			Inclusive:         result.TaxBreakdown[0].Inclusive,
			TaxAbilityReason:  taxAbilityReason,
			Country:           country,
			State:             state,
			PercentageDecimal: result.TaxBreakdown[0].TaxRateDetails.PercentageDecimal,
			TaxType:           taxType,
		}
		if ret.TaxType == "" && s.isUnsupportedTaxRate(taxAbilityReason) {
			ret.TaxType = string(stripe.TaxCalculationTaxBreakdownTaxRateDetailsTaxTypeGST)
		}
		return ret, nil
	}
}

const (
	TaxValue         = "tax_value"
	TenantID         = "tenant_id"
	TaxAbilityReason = "tax_ability_reason"
	TaxCode          = "tax_code"
)

func (s *StripePaymentHandlerImpl) CreateTaxRate(rate *forms.TaxRate) (*forms.TaxRate, error) {
	if !stripeEnabled() {
		return nil, billing_errors.NewSystemError(fmt.Errorf("stripe is not enable"))
	}
	if s.isUnsupportedTaxRate(rate.TaxAbilityReason) {
		return s.initUnsupportedTaxRate(rate.TaxAbilityReason), nil
	}
	percent, _ := rate.Value.Float64()
	taxRateParams := &stripe.TaxRateParams{
		DisplayName: stripe.String(rate.DisplayName),
		Inclusive:   stripe.Bool(rate.Inclusive),
		Percentage:  stripe.Float64(percent),
		Country:     stripe.String(rate.Country),
		TaxType:     stripe.String(rate.TaxType),
		Metadata: map[string]string{
			TaxValue:         rate.Value.String(),
			TenantID:         fmt.Sprint(rate.TenantID),
			TaxAbilityReason: rate.TaxAbilityReason,
			TaxCode:          rate.TaxCode,
		},
	}
	if rate.State != "" {
		taxRateParams.State = stripe.String(rate.State)
	}
	taxRate := getStripeClient().TaxRates
	result, err := taxRate.New(taxRateParams)
	if err != nil {
		return nil, billing_errors.NewSystemError(err)
	}
	rate.StripeTaxRateID = result.ID
	return rate, nil
}

func (s *StripePaymentHandlerImpl) GetTaxRate(rateID string) (*forms.TaxRate, error) {
	if !stripeEnabled() {
		return nil, billing_errors.NewSystemError(fmt.Errorf("stripe is not enable"))
	}
	if s.isUnsupportedTaxRate(rateID) {
		return s.initUnsupportedTaxRate(rateID), nil
	}
	taxRate := getStripeClient().TaxRates
	result, err := taxRate.Get(rateID, nil)
	if err != nil {
		return nil, billing_errors.NewSystemError(err)
	}
	var tenantID uint64
	if _, ok := result.Metadata[TenantID]; ok {
		tenantID, err = strconv.ParseUint(result.Metadata[TenantID], 10, 64)
	}
	val := decimal.NewFromFloat(result.EffectivePercentage)
	if _, ok := result.Metadata[TaxValue]; ok {
		val = decimal.RequireFromString(result.Metadata[TaxValue])
	} else {
		log.Error("failed to get tax rate value from meta data", zap.String("rateID", rateID))
	}
	return &forms.TaxRate{
		StripeTaxRateID:  result.ID,
		Value:            val,
		DisplayName:      result.DisplayName,
		Inclusive:        result.Inclusive,
		Country:          result.Country,
		State:            result.State,
		TaxType:          string(result.TaxType),
		TenantID:         uint(tenantID),
		TaxAbilityReason: result.Metadata[TaxAbilityReason],
		TaxCode:          result.Metadata[TaxCode],
	}, nil
}

func (s *StripePaymentHandlerImpl) EnableTaxCalculation() bool {
	return config.Get().GetBool(config.EnableTaxCalculation)
}

func (s *StripePaymentHandlerImpl) InactiveTaxRate(rateID string) error {
	if !stripeEnabled() {
		return billing_errors.NewSystemError(fmt.Errorf("stripe is not enable"))
	}
	if s.isUnsupportedTaxRate(rateID) {
		return nil
	}
	taxRate := getStripeClient().TaxRates
	_, err := taxRate.Update(rateID, &stripe.TaxRateParams{Active: stripe.Bool(false)})
	if err != nil {
		return billing_errors.NewSystemError(err)
	}
	return nil
}

func (s *StripePaymentHandlerImpl) isUnsupportedTaxRate(taxAbilityReason string) bool {
	switch taxAbilityReason {
	case string(stripe.TaxCalculationTaxBreakdownTaxabilityReasonNotCollecting), string(stripe.TaxCalculationLineItemTaxBreakdownTaxabilityReasonNotSupported), string(stripe.TaxCalculationShippingCostTaxBreakdownTaxabilityReasonNotSubjectToTax):
		return true
	default:
		return false
	}
}

func (s *StripePaymentHandlerImpl) initUnsupportedTaxRate(taxAbilityReason string) *forms.TaxRate {
	return &forms.TaxRate{
		StripeTaxRateID:  taxAbilityReason,
		Value:            decimal.Zero,
		TaxAbilityReason: taxAbilityReason,
	}
}

func (s *StripePaymentHandlerImpl) EnableHourlySending(tenantID, billingConfigID uint) bool {
	return false
}

func (s *StripePaymentHandlerImpl) EnableDailySending(tenantID, billingConfigID uint) (bool, int) {
	return false, 0
}

func (s *StripePaymentHandlerImpl) NeedInMonthInvoice() bool {
	return false
}

func (s *StripePaymentHandlerImpl) NeedSetInvoiceAutoConfirm() bool {
	return false
}

func (s *StripePaymentHandlerImpl) GetPaymentIntent(paymentIntentID string) (*stripe.PaymentIntent, error) {
	if !stripeEnabled() {
		return nil, billing_errors.NewSystemError(fmt.Errorf("stripe is not enable"))
	}
	paymentIntent, err := getStripeClient().PaymentIntents.Get(paymentIntentID, nil)
	if err != nil {
		return nil, billing_errors.NewSystemError(err)
	}
	return paymentIntent, nil
}
