package billing2

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

	"github.com/pingcap/dbaas/central/config"
	"github.com/pingcap/errors"
	"github.com/pingcap/log"
	"github.com/shopspring/decimal"
	"go.uber.org/zap"
)

type BillingHandlerImpl struct {
}

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 errors.Trace(fmt.Errorf("call billing2 service resp code err %d, resp: %s", resp.StatusCode, string(bodyBytes)))
	}
	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.Info("start call billing2 service", zap.String("method", method), zap.String("url", url))
	err := call(url, method, input, output)
	if err != nil {
		log.Error("failed call billing2 service", zap.String("method", method), zap.String("url", url), zap.Error(err))
		return err
	}
	log.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)
}
