package facade_imp

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"sync"
	"time"

	"github.com/go-resty/resty/v2"
	"github.com/shopspring/decimal"
	"github.com/tidbcloud/pkgv2/bizerr"
	"github.com/tidbcloud/pkgv2/goroutine"
	"github.com/tidbcloud/pkgv2/log"
	"github.com/tidbcloud/pkgv2/tracing"
	"github.com/tidbcloud/serverless-service/internal/config"
	"github.com/tidbcloud/serverless-service/internal/domain/constant"
	"github.com/tidbcloud/serverless-service/internal/domain/facade"
	"github.com/tidbcloud/serverless-service/internal/metric"
	"go.uber.org/zap"
)

type billingMeasureParam struct {
	CostID        string            `json:"cost_id"`
	Cost          int64             `json:"cost"`
	CostTimestamp int64             `json:"cost_time_stamp"`
	Labels        map[string]string `json:"labels"`
	Unit          string            `json:"unit"`
}

type billingMeasureResult struct {
	BindedPackageID uint64 `json:"binded_package_id"`
	ConsumedSource  string `json:"consumed_source"`
	Cost            int    `json:"cost"`
	CostID          string `json:"cost_id"`
	CostTimestamp   int64  `json:"cost_time_stamp"`
	InvoiceID       int    `json:"invoice_id"`
	PackageID       uint64 `json:"package_id"`
	PackageVersion  int64  `json:"package_version"`
	Price           string `json:"price"`
	RawPrice        string `json:"raw_price"`
	Unit            string `json:"unit"`
}

func callBillingMeasures(
	ctx context.Context,
	client *resty.Client,
	method, apiPath string,
	pathParams map[string]string,
	body any,
	output any,
) (*resty.Response, bizerr.BizError) {
	url := fmt.Sprintf("http://%s%s", config.Global().SvcAddrConfig.BillingMeasuresAddr, apiPath)
	return callBilling(ctx, client, url, method, pathParams, body, output)
}

func callBillingAPI(
	ctx context.Context,
	client *resty.Client,
	method, apiPath string,
	pathParams map[string]string,
	body any,
	output any,
) (*resty.Response, bizerr.BizError) {
	url := fmt.Sprintf("http://%s%s", config.Global().SvcAddrConfig.BillingAPIAddr, apiPath)
	return callBilling(ctx, client, url, method, pathParams, body, output)
}

func callBilling(
	ctx context.Context,
	client *resty.Client,
	url, method string,
	pathParams map[string]string,
	body any,
	output any,
) (*resty.Response, bizerr.BizError) {
	logger := log.WithContextS(ctx)

	request := client.R().EnableTrace().SetPathParams(pathParams).SetContext(ctx)
	if body != nil {
		request = request.SetBody(body)
	}
	resp, err := request.Execute(method, url)
	if err != nil {
		return nil, bizerr.New(bizerr.ErrCodeSvcBilling, "http request failed: %s", err)
	}
	bodyBytes, _ := json.Marshal(body)
	logger.Debugf(
		"call %s on %s,status=%d,req=%s,resp=%s",
		method,
		request.URL,
		resp.StatusCode(),
		string(bodyBytes),
		string(resp.Body()),
	)
	if resp.StatusCode() != http.StatusOK {
		return resp, bizerr.New(bizerr.ErrCodeSvcBilling, " http status code != 200, resp: %s", string(resp.Body()))
	}
	if output != nil {
		err = json.Unmarshal(resp.Body(), output)
		if err != nil {
			return resp, bizerr.New(bizerr.ErrCodeSvcBilling, "unmarshal response failed: %s", err)
		}
	}
	return resp, nil
}

type billing struct {
	client *resty.Client
}

func NewBillingFacade() facade.BillingFacade {
	return billing{client: resty.New()}
}

func (f billing) GetPackage(ctx context.Context, id uint64) (_ *facade.BillingPackage, err bizerr.BizError) {
	const (
		path = "/api/v1/billing/packages/{pkgId}"
	)
	start := time.Now()
	ctx, span := Tracer.Start(ctx, "Billing2.GetPackage")
	defer func() {
		tracing.EndSpan(span, err)
		status := metric.StatusOK
		if err != nil {
			status = metric.StatusErr
		}
		metric.FacadeRequestedCounter.WithLabelValues("billing", "GetPackage", status).Inc()
		metric.FacadeRequestedHistogram.WithLabelValues("billing", "GetPackage", status).
			Observe(time.Since(start).Seconds())
	}()

	pkg := &facade.BillingPackage{}
	if _, err := callBillingAPI(ctx, f.client, resty.MethodGet, path, map[string]string{"pkgId": fmt.Sprint(id)}, nil, pkg); err != nil {
		return nil, err.Chain("GetPackage")
	}
	return pkg, nil
}

func (f billing) UpdatePackage(
	ctx context.Context,
	id uint64,
	pkg *facade.BillingPackage4Update,
) (err bizerr.BizError) {
	const (
		path = "/api/v1/billing/packages/{pkgId}"
	)
	start := time.Now()
	ctx, span := Tracer.Start(ctx, "Billing2.UpdatePackage")
	defer func() {
		tracing.EndSpan(span, err)
		status := metric.StatusOK
		if err != nil {
			status = metric.StatusErr
		}
		metric.FacadeRequestedCounter.WithLabelValues("billing", "UpdatePackage", status).Inc()
		metric.FacadeRequestedHistogram.WithLabelValues("billing", "UpdatePackage", status).
			Observe(time.Since(start).Seconds())
	}()

	logger := log.WithContextL(ctx)
	logger.Sugar().Infof("try to update package, id=%d, pkg=%+v", id, pkg)
	if _, err := callBillingAPI(ctx, f.client, resty.MethodPatch, path, map[string]string{"pkgId": fmt.Sprint(id)}, pkg, nil); err != nil {
		return err.Chain("UpdatePackage")
	}
	return nil
}

func (f billing) CreateOrUpdateUnits(ctx context.Context, units []*facade.BillingUnit) (err bizerr.BizError) {
	const (
		path = "/api/v1/billing/units/_bulk"
	)
	start := time.Now()
	ctx, span := Tracer.Start(ctx, "Billing2.CreateOrUpdateUnits")
	defer func() {
		tracing.EndSpan(span, err)
		status := metric.StatusOK
		if err != nil {
			status = metric.StatusErr
		}
		metric.FacadeRequestedCounter.WithLabelValues("billing", "CreateOrUpdateUnits", status).Inc()
		metric.FacadeRequestedHistogram.WithLabelValues("billing", "CreateOrUpdateUnits", status).
			Observe(time.Since(start).Seconds())
	}()

	logger := log.WithContextL(ctx).Sugar()
	unitBytes, _ := json.Marshal(units)
	logger.Infof("try to update units, units=%s", string(unitBytes))
	if _, err := callBillingAPI(ctx, f.client, resty.MethodPost, path, nil, units, nil); err != nil {
		return err.Chain("CreateOrUpdateUnits")
	}
	return nil
}

func (f billing) CalcFee(
	ctx context.Context,
	records []*facade.BillingReport,
) (map[string]*facade.BillingCharge, bizerr.BizError) {
	ctx, span := Tracer.Start(ctx, "Billing2.CalcFee")
	defer span.End()

	out := make(map[string]*facade.BillingCharge)
	var mu sync.Mutex
	var fns []goroutine.Handle
	for _, each := range records {
		record := each
		fns = append(fns, func() error {
			price, err := f.calcFee(ctx, record)
			if err != nil {
				return err
			}

			mu.Lock()
			defer mu.Unlock()
			out[record.Sku] = price
			return nil
		})
	}
	if err := goroutine.Concurrent(ctx, fns...); err != nil {
		return nil, bizerr.WrapError(err, "calc fee from billing")
	}
	return out, nil
}

func (f billing) calcFee(
	ctx context.Context,
	record *facade.BillingReport,
) (_ *facade.BillingCharge, err bizerr.BizError) {
	const (
		path = "/api/v1/billing/tenants/{tenant_id}/measures/dryrun"
	)
	start := time.Now()
	ctx, span := Tracer.Start(ctx, "Billing2.calcFee")
	defer func() {
		tracing.EndSpan(span, err)
		status := metric.StatusOK
		if err != nil {
			status = metric.StatusErr
		}
		metric.FacadeRequestedCounter.WithLabelValues("billing", "calcFee", status).Inc()
		metric.FacadeRequestedHistogram.WithLabelValues("billing", "calcFee", status).
			Observe(time.Since(start).Seconds())
	}()

	body := billingMeasureParam{
		CostID:        record.CostID,
		Cost:          record.Cost,
		CostTimestamp: time.Now().Unix(),
		Labels:        record.Labels,
		Unit:          record.Sku,
	}
	log.WithContextS(ctx).With("tenantID", record.TenantID).Debugf("req=%+v", body)
	result := &billingMeasureResult{}
	if _, err := callBillingAPI(ctx, f.client, resty.MethodPost, path, map[string]string{"tenant_id": fmt.Sprint(record.TenantID)}, body, result); err != nil {
		return nil, err.Chain("calcFee")
	}

	rawPrice, e := decimal.NewFromString(result.RawPrice)
	if e != nil {
		return nil, bizerr.WrapError(e, "invalid raw_price: %s", result.RawPrice)
	}
	out := &facade.BillingCharge{
		RawPrice: rawPrice,
	}
	return out, nil
}

func (f billing) Measure(ctx context.Context, records []*facade.BillingReport) (err bizerr.BizError) {
	ctx, span := Tracer.Start(ctx, "Billing2.Measure")
	defer func() { tracing.EndSpan(span, err) }()

	logger := log.WithContextL(ctx)
	logger.Debug("start reporting usages to billing ", zap.Int("count", len(records)))

	var fns []goroutine.Handle
	for _, each := range records {
		record := each
		fns = append(fns, func() error {
			return f.measureOne(ctx, record)
		})
	}
	if e := goroutine.Concurrent(ctx, fns...); e != nil {
		return bizerr.New(bizerr.ErrCodeHTTP, "failed to report usages to billing")
	}
	return nil
}

func (f billing) measureOne(ctx context.Context, record *facade.BillingReport) (err bizerr.BizError) {
	const (
		path = "/api/v1/billing/tenants/{tenant_id}/measures"
	)
	start := time.Now()
	ctx, span := Tracer.Start(ctx, "Billing2.measureOne")
	defer func() {
		tracing.EndSpan(span, err)
		status := metric.StatusOK
		if err != nil {
			status = metric.StatusErr
		}
		metric.FacadeRequestedCounter.WithLabelValues("billing", "measureOne", status).Inc()
		metric.FacadeRequestedHistogram.WithLabelValues("billing", "measureOne", status).
			Observe(time.Since(start).Seconds())
	}()

	logger := log.WithContextL(ctx).With(
		zap.Uint64("tenant_id", record.TenantID), zap.Any("labels", record.Labels),
		zap.String("cost_id", record.CostID), zap.String("unit", record.Sku),
	)

	body := billingMeasureParam{
		CostID:        record.CostID,
		Cost:          record.Cost,
		CostTimestamp: record.CostTimeStamp,
		Labels:        record.Labels,
		Unit:          record.Sku,
	}

	logger.Debug("call billing", zap.Any("req", body))
	result := &billingMeasureResult{}
	resp, err := callBillingMeasures(
		ctx,
		f.client,
		resty.MethodPost,
		path,
		map[string]string{"tenant_id": fmt.Sprint(record.TenantID)},
		body,
		result,
	)
	if err != nil {
		// should ignore if the error is request error. such as: invalid input, can't consume for this cost
		// TODO(chenfei): should return err for 4xx if 200 have been returned from billing for internal error(such as: can't consume).
		if resp != nil && (400 <= resp.StatusCode() && resp.StatusCode() <= 499) {
			logger.Warn("measure usages to billing have some error, but ignore it",
				zap.Int("code", resp.StatusCode()), zap.String("body", string(resp.Body())), zap.Error(err))
			return nil
		}

		logger.Error("failed to report one record", zap.Error(err))
		return err.Chain("measureOne")
	}

	return nil
}

func (f billing) GetPaymentMethods(
	ctx context.Context,
	id uint64,
) (_ []*facade.StripePaymentMethod, err bizerr.BizError) {
	const (
		path = "/api/v1/billing/tenants/{tenant_id}/payments/stripe/payment_methods"
	)
	start := time.Now()
	ctx, span := Tracer.Start(ctx, "Billing2.GetPaymentMethods")
	defer func() {
		tracing.EndSpan(span, err)
		status := metric.StatusOK
		if err != nil {
			status = metric.StatusErr
		}
		metric.FacadeRequestedCounter.WithLabelValues("billing", "GetPaymentMethods", status).Inc()
		metric.FacadeRequestedHistogram.WithLabelValues("billing", "GetPaymentMethods", status).
			Observe(time.Since(start).Seconds())
	}()

	payments := make([]*facade.StripePaymentMethod, 0)
	if _, err := callBillingAPI(ctx, f.client, resty.MethodGet, path, map[string]string{"tenant_id": fmt.Sprint(id)}, nil, &payments); err != nil {
		return nil, err.Chain("Billing2.GetPaymentMethods")
	}
	return payments, nil
}

func (f billing) GetTenantPlan(ctx context.Context, id uint64) (_ *facade.TenantPlanInfo, err bizerr.BizError) {
	const (
		path = "/api/v1/billing/tenants/{tenant_id}/plans"
	)
	start := time.Now()
	ctx, span := Tracer.Start(ctx, "Billing2.GetPlans")
	defer func() {
		tracing.EndSpan(span, err)
		status := metric.StatusOK
		if err != nil {
			status = metric.StatusErr
		}
		metric.FacadeRequestedCounter.WithLabelValues("billing", "GetPlans", status).Inc()
		metric.FacadeRequestedHistogram.WithLabelValues("billing", "GetPlans", status).
			Observe(time.Since(start).Seconds())
	}()

	plan := &facade.TenantPlanInfo{}
	if _, err := callBillingAPI(ctx, f.client, resty.MethodGet, path, map[string]string{"tenant_id": fmt.Sprint(id)}, nil, plan); err != nil {
		return nil, err.Chain("Billing2.GetPlans")
	}
	return plan, nil
}

func (f billing) ModifySpendLimit(ctx context.Context, setting *facade.SpendLimitSetting) (err bizerr.BizError) {
	const (
		path = "/api/v1/billing/tenants/{tenant_id}/spend_limits/{spend_limit_setting_id}"
	)
	start := time.Now()
	ctx, span := Tracer.Start(ctx, "Billing2.ModifySpendLimit")
	defer func() {
		tracing.EndSpan(span, err)
		status := metric.StatusOK
		if err != nil {
			status = metric.StatusErr
		}
		metric.FacadeRequestedCounter.WithLabelValues("billing", "ModifySpendLimit", status).Inc()
		metric.FacadeRequestedHistogram.WithLabelValues("billing", "ModifySpendLimit", status).
			Observe(time.Since(start).Seconds())
	}()

	_, err = callBillingAPI(
		ctx,
		f.client,
		resty.MethodPut,
		path,
		map[string]string{
			"tenant_id":              fmt.Sprint(setting.TenantID),
			"spend_limit_setting_id": fmt.Sprint(setting.SpendLimitSettingID),
		},
		setting,
		nil,
	)
	return err
}

func (f billing) SetSpendLimit(ctx context.Context, setting *facade.SpendLimitSetting) (err bizerr.BizError) {
	const (
		path = "/api/v1/billing/tenants/{tenant_id}/spend_limits"
	)
	start := time.Now()
	ctx, span := Tracer.Start(ctx, "Billing2.SetSpendLimit")
	defer func() {
		tracing.EndSpan(span, err)
		status := metric.StatusOK
		if err != nil {
			status = metric.StatusErr
		}
		metric.FacadeRequestedCounter.WithLabelValues("billing", "SetSpendLimit", status).Inc()
		metric.FacadeRequestedHistogram.WithLabelValues("billing", "SetSpendLimit", status).
			Observe(time.Since(start).Seconds())
	}()

	_, err = callBillingAPI(
		ctx,
		f.client,
		resty.MethodPost,
		path,
		map[string]string{"tenant_id": fmt.Sprint(setting.TenantID)},
		setting,
		nil,
	)
	return err
}

func (f billing) GetSpendLimit(
	ctx context.Context,
	tenantId uint64,
	clusterId uint64,
) (_ *facade.SpendLimitSetting, err bizerr.BizError) {
	const (
		path = "/api/v1/billing/tenants/{tenant_id}/spend_limits?biz_type=serverless_cluster&range_type=cluster&range_id="
	)
	start := time.Now()
	ctx, span := Tracer.Start(ctx, "Billing2.GetSpendLimit")
	defer func() {
		tracing.EndSpan(span, err)
		status := metric.StatusOK
		if err != nil {
			status = metric.StatusErr
		}
		metric.FacadeRequestedCounter.WithLabelValues("billing", "GetSpendLimit", status).Inc()
		metric.FacadeRequestedHistogram.WithLabelValues("billing", "GetSpendLimit", status).
			Observe(time.Since(start).Seconds())
	}()

	settings := make([]*facade.SpendLimitSetting, 0)
	if _, err := callBillingAPI(ctx, f.client, resty.MethodGet, path+fmt.Sprint(clusterId),
		map[string]string{"tenant_id": fmt.Sprint(tenantId)}, nil, &settings); err != nil {
		return nil, err.Chain("Billing2.GetSpendLimit")
	}
	if len(settings) == 0 {
		return nil, bizerr.New(bizerr.ErrCodeSvcBilling, "not found")
	}
	return settings[0], nil
}

func (f billing) DeleteSpendLimit(
	ctx context.Context,
	tenantId uint64,
	spendLimitSettingId uint64,
) (err bizerr.BizError) {
	const (
		path = "/api/v1/billing/tenants/{tenant_id}/spend_limits/{spend_limit_setting_id}"
	)
	start := time.Now()
	ctx, span := Tracer.Start(ctx, "Billing2.DeleteSpendLimit")
	defer func() {
		tracing.EndSpan(span, err)
		status := metric.StatusOK
		if err != nil {
			status = metric.StatusErr
		}
		metric.FacadeRequestedCounter.WithLabelValues("billing", "DeleteSpendLimit", status).Inc()
		metric.FacadeRequestedHistogram.WithLabelValues("billing", "DeleteSpendLimit", status).
			Observe(time.Since(start).Seconds())
	}()

	if _, err := callBillingAPI(ctx, f.client, resty.MethodDelete, path,
		map[string]string{"tenant_id": fmt.Sprint(tenantId), "spend_limit_setting_id": fmt.Sprint(spendLimitSettingId)},
		nil, nil); err != nil {
		return err.Chain("Billing2.DeleteSpendLimit")
	}

	return nil
}

func (f billing) GetSpendLimitSettings(
	ctx context.Context,
	tenantId uint64,
) (_ []*facade.SpendLimitSetting, err bizerr.BizError) {
	const (
		// must include has_serverless_free_credits=true to avoid large tenant cause billing svc oom
		path = "/api/v1/billing/tenants/{tenant_id}/spend_limits?biz_type=serverless_cluster&has_serverless_free_credits=true"
	)
	start := time.Now()
	ctx, span := Tracer.Start(ctx, "Billing2.GetSpendLimitSettings")
	defer func() {
		tracing.EndSpan(span, err)
		status := metric.StatusOK
		if err != nil {
			status = metric.StatusErr
		}
		metric.FacadeRequestedCounter.WithLabelValues("billing", "GetSpendLimitSettings", status).Inc()
		metric.FacadeRequestedHistogram.WithLabelValues("billing", "GetSpendLimitSettings", status).
			Observe(time.Since(start).Seconds())
	}()

	settings := make([]*facade.SpendLimitSetting, 0)
	if _, err := callBillingAPI(ctx, f.client, resty.MethodGet, path,
		map[string]string{"tenant_id": fmt.Sprint(tenantId)}, nil, &settings); err != nil {
		return nil, err.Chain("Billing2.GetSpendLimitSettings")
	}
	return settings, nil
}

func (f billing) GetSpendLimitInfo(
	ctx context.Context,
	setting *facade.SpendLimitSetting,
) (_ *facade.SpendLimitInfo, err bizerr.BizError) {
	const (
		path = "/api/v1/billing/tenants/{tenant_id}/spend_limits/{spend_limit_setting_id}/info"
	)
	start := time.Now()
	ctx, span := Tracer.Start(ctx, "Billing2.GetSpendLimitInfo")
	defer func() {
		tracing.EndSpan(span, err)
		status := metric.StatusOK
		if err != nil {
			status = metric.StatusErr
		}
		metric.FacadeRequestedCounter.WithLabelValues("billing", "GetSpendLimitInfo", status).Inc()
		metric.FacadeRequestedHistogram.WithLabelValues("billing", "GetSpendLimitInfo", status).
			Observe(time.Since(start).Seconds())
	}()

	info := &facade.SpendLimitInfo{}
	if _, err := callBillingAPI(ctx, f.client, resty.MethodGet, path,
		map[string]string{"tenant_id": fmt.Sprint(setting.TenantID), "spend_limit_setting_id": fmt.Sprint(setting.SpendLimitSettingID)},
		nil, &info); err != nil {
		return info, err.Chain("Billing2.GetSpendLimitsInfo")
	}
	return info, nil
}

func (f billing) CalculateCost(
	ctx context.Context,
	tenantId uint64,
	costQuery *facade.CostExplorerInternalQuery,
) (_ *facade.CostExplorerResult, err bizerr.BizError) {
	const (
		path = "/api/v1/billing/tenants/{tenant_id}/internal/costexplorer"
	)
	start := time.Now()
	ctx, span := Tracer.Start(ctx, "Billing2.CalculateCost")
	defer func() {
		tracing.EndSpan(span, err)
		status := metric.StatusOK
		if err != nil {
			status = metric.StatusErr
		}
		metric.FacadeRequestedCounter.WithLabelValues("billing", "CalculateCost", status).Inc()
		metric.FacadeRequestedHistogram.WithLabelValues("billing", "CalculateCost", status).
			Observe(time.Since(start).Seconds())
	}()

	costQuery.IgnoreBeta = true
	result := &facade.CostExplorerResult{}
	if _, err := callBillingAPI(ctx, f.client, resty.MethodPost, path, map[string]string{"tenant_id": fmt.Sprint(tenantId)}, costQuery, result); err != nil {
		return nil, err.Chain("Billing2.CalculateCost")
	}

	return result, nil
}

func (f billing) CostExplorerV2(
	ctx context.Context,
	tenantId uint64,
	costQuery *facade.CostExplorerInternalQuery,
) (_ *facade.CompleteCostExplorerResult, err bizerr.BizError) {
	const (
		path = "/api/v1/billing/tenants/{tenant_id}/internal/costexplorer/v2"
	)
	start := time.Now()
	ctx, span := Tracer.Start(ctx, "Billing2.CostExplorerV2")
	defer func() {
		tracing.EndSpan(span, err)
		status := metric.StatusOK
		if err != nil {
			status = metric.StatusErr
		}
		metric.FacadeRequestedCounter.WithLabelValues("billing", "CostExplorerV2", status).Inc()
		metric.FacadeRequestedHistogram.WithLabelValues("billing", "CostExplorerV2", status).
			Observe(time.Since(start).Seconds())
	}()

	result := &facade.CompleteCostExplorerResult{}
	if _, err := callBillingAPI(ctx, f.client, resty.MethodPost, path, map[string]string{"tenant_id": fmt.Sprint(tenantId)}, costQuery, result); err != nil {
		return nil, err.Chain("Billing2.CostExplorerV2")
	}

	return result, nil
}

func (f billing) SendNotification(
	ctx context.Context,
	tenantId uint64,
	req *facade.BillingNotificationReq,
) (err bizerr.BizError) {
	const (
		path = "/api/v1/unc/tenants/{tenant_id}/bulk_trigger"
	)
	start := time.Now()
	ctx, span := Tracer.Start(ctx, "Billing2.SendNotification")
	defer func() {
		tracing.EndSpan(span, err)
		status := metric.StatusOK
		if err != nil {
			status = metric.StatusErr
		}
		metric.FacadeRequestedCounter.WithLabelValues("billing", "SendNotification", status).Inc()
		metric.FacadeRequestedHistogram.WithLabelValues("billing", "SendNotification", status).
			Observe(time.Since(start).Seconds())
	}()

	if _, err := callBillingAPI(ctx, f.client, resty.MethodPost, path, map[string]string{"tenant_id": fmt.Sprint(tenantId)}, req, nil); err != nil {
		return err.Chain("Billing2.SendNotification")
	}

	return nil
}

func (f billing) BillingActionCheck(ctx context.Context, tenantId uint64) (_ *facade.ActionCheckResp, err error) {
	const (
		path = "/api/v1/billing/tenants/{tenant_id}/billing_action_check"
	)

	start := time.Now()
	ctx, span := Tracer.Start(ctx, "Billing2.BillingActionCheck")
	defer func() {
		tracing.EndSpan(span, err)
		status := metric.StatusOK
		if err != nil {
			status = metric.StatusErr
		}
		metric.FacadeRequestedCounter.WithLabelValues("billing", "BillingActionCheck", status).Inc()
		metric.FacadeRequestedHistogram.WithLabelValues("billing", "BillingActionCheck", status).
			Observe(time.Since(start).Seconds())
	}()

	req := &facade.ActionCheckReq{
		Resource: facade.Resource{
			Type: "serverless_cluster",
		},
		Action: "add",
	}
	resp := &facade.ActionCheckResp{}

	if _, err := callBillingAPI(ctx, f.client, resty.MethodPost, path, map[string]string{"tenant_id": fmt.Sprint(tenantId)}, req, resp); err != nil {
		return nil, err.Chain("Billing2.ActionCheck")
	}
	return resp, nil
}

// GetInvoiceLimit get invoice limit in dollar cents
func (f billing) GetInvoiceLimit(
	ctx context.Context,
	tenantId uint64,
	clusterId uint64,
) (_ uint32, err bizerr.BizError) {
	settings, err := f.GetSpendLimit(ctx, tenantId, clusterId)
	if err != nil {
		return 0, err
	}
	bizData := &facade.ServerlessSpendLimitBizData{}
	if err := json.Unmarshal([]byte(settings.BizData), bizData); err != nil {
		return 0, bizerr.New(bizerr.ErrCodeSvcBilling, "unmarshal biz data failed")
	}
	invoiceLimit := uint32(bizData.InvoiceLimit.Mul(constant.DolloarToCents).IntPart())
	return invoiceLimit, nil
}
