package paymenthandler

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

	servicecontrol "cloud.google.com/go/servicecontrol/apiv1"
	"github.com/google/uuid"
	"github.com/pingcap/dbaas/billing2/billing_errors"
	"github.com/pingcap/dbaas/billing2/config"
	"github.com/pingcap/dbaas/billing2/forms"
	"github.com/pingcap/dbaas/billing2/handler/gcphandler/auth"
	"github.com/pingcap/errors"
	"github.com/pingcap/log"
	"github.com/shopspring/decimal"
	"github.com/spf13/viper"
	"go.uber.org/zap"
	"golang.org/x/oauth2"
	"google.golang.org/api/option"
	servicecontrolpb "google.golang.org/genproto/googleapis/api/servicecontrol/v1"
)

func getClientTS() (oauth2.TokenSource, error) {
	ts, err := auth.DefaultTokenSource()
	if err != nil {
		return nil, err
	}
	marketPlaceSA := viper.GetString(config.GCPMarketPlaceSA)

	if marketPlaceSA == "" {
		return nil, billing_errors.NewSystemError(errors.New("invalid marketplace sa"))
	}
	return auth.Impersonate(marketPlaceSA, ts)
}

func getGCPClient() (*http.Client, error) {
	ts, err := getClientTS()
	if err != nil {
		return nil, err
	}
	client := oauth2.NewClient(context.Background(), ts)
	return client, nil
}

func getGcpMeteringClient() (*servicecontrol.QuotaControllerClient, error) {
	ts, err := getClientTS()
	if err != nil {
		return nil, err
	}

	ctx := context.Background()
	return servicecontrol.NewQuotaControllerClient(ctx, option.WithTokenSource(ts))
}

func call(url, method string, input, output interface{}, client *http.Client) 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 := client.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 gcp marketplace 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 {
	client, err := getGCPClient()
	if err != nil {
		return err
	}
	baseUrl := viper.GetString(config.GCPProcurementBaseUrl)
	apiPath = strings.TrimLeft(apiPath, "/")
	url := fmt.Sprintf("%s/%s", strings.TrimRight(baseUrl, "/"), strings.TrimLeft(apiPath, "/"))
	log.Info("start call gcp marketplace service", zap.String("method", method), zap.String("url", url))
	err = call(url, method, input, output, client)
	if err != nil {
		log.Error("failed call gcp marketplace service", zap.String("method", method), zap.String("url", url), zap.Error(err))
		return err
	}
	log.Info("success call gcp marketplace service", zap.String("method", method), zap.String("url", url))
	return nil
}

const GCPActiveState = "active"

type EntitlementInfo struct {
	Product          string `json:"product"`
	UsageReportingId string `json:"usageReportingId"`
}

type ListResult struct {
	Entitlements []*EntitlementInfo `json:"entitlements"`
}

func getGCPReportID(accountCode string) (string, error) {
	partnerID := viper.GetString(config.GCPMarketPlaceProject)
	productCode := viper.GetString(config.GCPProductCode)
	_ = productCode
	url := fmt.Sprintf("/v1/providers/%s/entitlements?filter=account=%s&filter=state=%s", partnerID, accountCode, GCPActiveState)
	output := ListResult{}
	err := doRequest(url, "GET", nil, &output)
	if err != nil {
		return "", err
	}
	res := []*EntitlementInfo{}
	for _, outputResult := range output.Entitlements {
		if outputResult.Product == productCode {
			res = append(res, outputResult)
		}
	}

	if len(res) == 0 {
		return "", billing_errors.NewSystemError(errors.New("no valid entitlements"))
	}

	if len(res) > 1 {
		return "", billing_errors.NewSystemError(errors.New("system error: multi valid entitlements"))
	}

	return res[0].UsageReportingId, nil
}

type GcpPaymentHandlerImpl struct {
}

func (gcp *GcpPaymentHandlerImpl) Payable(userCode string) (bool, error) {
	return true, nil
}

const (
	GCPAccountProvider = "gcp_account_provider"
)

func (gcp *GcpPaymentHandlerImpl) GetUserCode(tenant forms.Tenant) (string, error) {
	if !viper.GetBool(config.GCPPaymentEnable) {
		return fmt.Sprintf("mock_gcp_%v", tenant.TenantID), nil
	}
	if tenant.ThirdPartyAccountProvider != GCPAccountProvider {
		return "", billing_errors.NewSystemError(errors.New("invalid third_party_account_handler"))
	}
	return tenant.ThirdPartyAccountCode, nil
}

func (gcp *GcpPaymentHandlerImpl) Pay(accountCode string, invoice *forms.Invoice) error {
	if !viper.GetBool(config.GCPPaymentEnable) {
		return nil
	}
	productCode := viper.GetString(config.GCPProductCode)
	unitCode := viper.GetString(config.GCPProductUnit)
	productPriceFactor := viper.GetInt64(config.GCPProudctUnitPriceFactor)
	client, err := getGcpMeteringClient()
	if err != nil {
		log.Error("get gcp metering client failed", zap.Error(err))
		return err
	}

	reportID, err := getGCPReportID(accountCode)
	if err != nil {
		log.Error("get gcp report id failed", zap.Error(err))
		return err
	}

	req := &servicecontrolpb.AllocateQuotaRequest{
		ServiceName: productCode,
		AllocateOperation: &servicecontrolpb.QuotaOperation{
			OperationId: uuid.New().String(),
			ConsumerId:  reportID,
			MethodName:  "report",
			QuotaMode:   servicecontrolpb.QuotaOperation_NORMAL,
		},
	}
	quantity := invoice.CostPrice.Mul(decimal.NewFromInt(productPriceFactor)).IntPart()

	gcpRecords := []*servicecontrolpb.MetricValueSet{
		{
			MetricName: fmt.Sprintf("%s/%s", productCode, unitCode),
			MetricValues: []*servicecontrolpb.MetricValue{
				{
					Value: &servicecontrolpb.MetricValue_Int64Value{
						Int64Value: quantity,
					},
				},
			},
		},
	}
	req.AllocateOperation.QuotaMetrics = gcpRecords
	resp, err := client.AllocateQuota(context.Background(), req)

	if err != nil {
		err = errors.Trace(err)
		log.Error("failed call gcp metering", zap.String("input", req.String()), zap.Error(err))
		return err
	}
	log.Info("success call gcp metering", zap.String("input", req.String()), zap.String("output", resp.String()))
	return nil
}
