package billing

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"strconv"
	"strings"
	"time"

	"github.com/pingcap/errors"
	"github.com/pingcap/log"
	"github.com/shopspring/decimal"
	"go.uber.org/zap"

	"github.com/tidbcloud/dataflow-service-ng/internal/config"
)

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"`
}

type BillingHandlerImpl struct{}

type BillingReqError struct {
	Code int
	Msg  string
}

func (b *BillingReqError) Error() string {
	return b.Msg
}

func (b *BillingReqError) IsRetryable() bool {
	return b.Code >= 500 && b.Code < 600
}

var globalClient http.Client = http.Client{
	Timeout: 60 * time.Second,
}

func call(url, method string, input, output interface{}) error {
	var inputMarshalled io.Reader
	if input == nil {
		inputMarshalled = nil
	} else {
		inputBytes, err := json.Marshal(input)
		if err != nil {
			return errors.Trace(err)
		}
		inputMarshalled = bytes.NewReader(inputBytes)
	}

	req, err := http.NewRequest(method, url, inputMarshalled)
	if err != nil {
		return errors.Trace(err)
	}
	resp, err := globalClient.Do(req)
	if err != nil {
		return errors.Trace(err)
	}
	bodyBytes, err := io.ReadAll(resp.Body)
	if err != nil {
		return errors.Trace(err)
	}
	resp.Body.Close() // nolint: errcheck

	if resp.StatusCode != 200 {
		return &BillingReqError{
			Code: resp.StatusCode,
			Msg:  errors.Trace(fmt.Errorf("call billing2 service resp code err %d, resp: %s", resp.StatusCode, string(bodyBytes))).Error(),
		}
	}
	if output != nil {
		err = json.Unmarshal(bodyBytes, output)
		if err != nil {
			return errors.Trace(err)
		}
	}
	return nil
}

func doRequest(apiPath string, method string, input interface{}, output interface{}) error {
	baseUrl := config.Global().BillingConfig.Billing2BaseURL
	apiPrefix := config.Global().BillingConfig.Billing2URLPrefix
	apiPath = strings.TrimLeft(apiPath, "/")
	url := fmt.Sprintf("%s/%s/%s", strings.TrimRight(baseUrl, "/"), strings.Trim(apiPrefix, "/"), strings.TrimLeft(apiPath, "/"))
	log.L().Info("start call billing2 service", zap.String("method", method), zap.String("url", url))
	err := call(url, method, input, output)
	if err != nil {
		log.L().Error("failed call billing2 service", zap.String("method", method), zap.String("url", url), zap.Any("input", input), zap.Error(err))
		return err
	}
	log.L().Info("success call billing2 service", zap.String("method", method), zap.String("url", url))
	return nil
}

func (*BillingHandlerImpl) ChangeUserDiscount(tenantID, bindedPackageID uint64, input *ChangeBindedDiscountInput) error {
	path := fmt.Sprintf("/tenants/%d/binded_packages/%d/discount", tenantID, bindedPackageID)
	return doRequest(path, "PUT", input, nil)
}

func (*BillingHandlerImpl) ChargeCredits(tenantID, bindedPackageID uint64, input *CreditsChargeInfoInput) error {
	path := fmt.Sprintf("/tenants/%d/binded_packages/%d/charge_credits", tenantID, bindedPackageID)
	return doRequest(path, "POST", input, nil)
}

type remainingCreditsInfo struct {
	RemainingCredits decimal.Decimal `json:"remaining_credits"`
}

func (*BillingHandlerImpl) GetCredits(tenantID, bindedPackageID uint64) (*decimal.Decimal, error) {
	path := fmt.Sprintf("/tenants/%d/binded_packages/%d", tenantID, bindedPackageID)
	output := &remainingCreditsInfo{}
	err := doRequest(path, "GET", nil, output)
	if err != nil {
		return nil, err
	}
	return &output.RemainingCredits, nil
}

func (*BillingHandlerImpl) GetCreditsHistory(tenantID, bindedPackageID uint64) (*CreditsChargeHistory, error) {
	path := fmt.Sprintf("/tenants/%d/binded_packages/%d/charge_credits", tenantID, bindedPackageID)
	output := &CreditsChargeHistory{}
	err := doRequest(path, "GET", nil, output)
	if err != nil {
		return nil, err
	}
	return output, nil
}

type packageDiscountInfo struct {
	Discount decimal.Decimal `json:"discount"`
}

type bindedPackageDiscountInfo struct {
	Discount *decimal.Decimal `json:"binded_discount"`
}

func (*BillingHandlerImpl) GetPackageDiscount(packageID uint64) (*decimal.Decimal, error) {
	path := fmt.Sprintf("/packages/%d", packageID)
	output := &packageDiscountInfo{}
	err := doRequest(path, "GET", nil, output)
	if err != nil {
		return nil, err
	}
	return &output.Discount, nil
}

func (*BillingHandlerImpl) UpdatePackageDiscount(packageID uint64, discount decimal.Decimal) error {
	path := fmt.Sprintf("/packages/%d", packageID)
	input := &packageDiscountInfo{
		Discount: discount,
	}
	return doRequest(path, "PATCH", input, nil)
}

func (*BillingHandlerImpl) GetBindedPackageDiscount(tenantID, bindedPackageID uint64) (*decimal.Decimal, error) {
	path := fmt.Sprintf("/tenants/%d/binded_packages/%d", tenantID, bindedPackageID)

	output := &bindedPackageDiscountInfo{}
	err := doRequest(path, "GET", nil, output)
	if err != nil {
		return nil, err
	}
	return output.Discount, nil
}

func (*BillingHandlerImpl) UpdateBindedPackageDiscount(tenantID, bindedPackageID uint64, discount *decimal.Decimal) error {
	path := fmt.Sprintf("/tenants/%d/binded_packages/%d/discount", tenantID, bindedPackageID)

	input := &bindedPackageDiscountInfo{
		Discount: discount,
	}
	return doRequest(path, "PUT", input, nil)
}

type bindedPackageInfoInput struct {
	PackageID uint64 `json:"package_id"`
}

type bindedPackageInfoOutput struct {
	BindedPackageID uint64 `json:"binded_package_id"`
}

func (*BillingHandlerImpl) BindPackage(tenantID, packageID uint64) (uint64, error) {
	path := fmt.Sprintf("/tenants/%d/binded_packages", tenantID)
	input := &bindedPackageInfoInput{
		PackageID: packageID,
	}
	output := &bindedPackageInfoOutput{}
	err := doRequest(path, "POST", input, output)
	if err != nil {
		return 0, err
	}
	return output.BindedPackageID, nil
}

func (*BillingHandlerImpl) UnBindPackage(tenantID, bindedPackageID uint64) error {
	path := fmt.Sprintf("/tenants/%d/binded_packages/%d", tenantID, bindedPackageID)

	return doRequest(path, "DELETE", nil, nil)
}

type payableStruct struct {
	Payable bool `json:"payable"`
}

func (*BillingHandlerImpl) Payable(tenantID uint64) (bool, error) {
	output := []payableStruct{}
	path := fmt.Sprintf("/tenants/%d/payments", tenantID)
	err := doRequest(path, "GET", nil, &output)
	if err != nil {
		return false, err
	}
	if len(output) == 0 {
		return false, nil
	}
	return output[0].Payable, nil
}

func (*BillingHandlerImpl) ConfirmStripeSetupIntent(tenantID uint64) error {
	path := fmt.Sprintf("/tenants/%d/payments/stripe/setup_intent/confirm", tenantID)
	return doRequest(path, "PUT", nil, nil)
}

func (*BillingHandlerImpl) CreatePayment(tenantID uint64, paymentType string) error {
	input := map[string]string{
		"payment_type": paymentType,
	}

	path := fmt.Sprintf("/tenants/%d/payments", tenantID)
	return doRequest(path, "POST", input, nil)
}

func (*BillingHandlerImpl) ListPayments(tenantID uint64) ([]PaymentMethod, error) {
	output := []PaymentMethod{}
	path := fmt.Sprintf("/tenants/%d/payments", tenantID)
	err := doRequest(path, "GET", nil, &output)
	if err != nil {
		return nil, err
	}
	return output, nil
}

func (*BillingHandlerImpl) MigratePayments() error {
	path := "/migrates/payments"
	return doRequest(path, "POST", nil, nil)
}

func (*BillingHandlerImpl) MigrateInvoices() error {
	path := "/migrates/invoices"
	return doRequest(path, "POST", nil, nil)
}

func (*BillingHandlerImpl) MigrateChargeCredits(bindedPackageID uint64, records []CreditsChargeInfoMigrateInput) error {
	path := fmt.Sprintf("/migrates/binded_packages/%d/charge_credits", bindedPackageID)
	return doRequest(path, "POST", records, nil)
}

func (*BillingHandlerImpl) MeasureCosts(tenantID uint64, cost *UnitCostsInput) error {
	path := fmt.Sprintf("/tenants/%d/measures", tenantID)
	return doRequest(path, "POST", cost, nil)
}

func (f *BillingHandlerImpl) CalFee(ctx context.Context, tenantID uint64, record *UnitCostsInput) (*BillingCharge, error) {
	path := fmt.Sprintf("/tenants/%d/measures/dryrun", tenantID)
	result := &billingMeasureResult{}

	if err := doRequest(path, "POST", record, result); err != nil {
		return nil, err
	}
	price, err := strconv.ParseFloat(result.Price, 64)
	if err != nil {
		return nil, errors.Annotatef(err, "invalid price: %s", result.Price)
	}
	rawPrice, err := strconv.ParseFloat(result.RawPrice, 64)
	if err != nil {
		return nil, errors.Annotatef(err, "invalid raw_price: %s", result.RawPrice)
	}
	out := &BillingCharge{
		RawFee: rawPrice,
		Fee:    price,
	}
	return out, nil
}

func (f *BillingHandlerImpl) ActionCheck(ctx context.Context, tenantID uint64, action ActionInfo) (*ActionCheckResult, error) {
	action.Resource.Provider = strings.ToUpper(action.Resource.Provider)

	path := fmt.Sprintf("/tenants/%d/billing_action_check", tenantID)
	result := &ActionCheckResult{}

	if err := doRequest(path, "POST", action, result); err != nil {
		return nil, err
	}
	return result, nil
}

func (*BillingHandlerImpl) ListSKU(ctx context.Context) ([]*SKU, error) {
	path := "/units"
	output := []*SKU{}
	return output, doRequest(path, "GET", nil, &output)
}

func (*BillingHandlerImpl) CreateOrUpdateSKU(ctx context.Context, sku *SKU) (*SKU, error) {
	path := "/units"
	output := &SKU{}
	return output, doRequest(path, "POST", sku, output)
}

func (*BillingHandlerImpl) DeleteSKU(ctx context.Context, sku *SKU) error {
	path := fmt.Sprintf("/units/%s", sku.UnitKey)
	return doRequest(path, "DELETE", nil, nil)
}
