package billing2

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

	"github.com/tidbcloud/account/commons/metrics"

	"github.com/hashicorp/go-cleanhttp"
	"github.com/hashicorp/go-retryablehttp"
	"github.com/pingcap/errors"
	"github.com/tidbcloud/account/commons/config"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
	"go.uber.org/zap"
)

type BillingHandlerImpl struct {
}

var globalClient = retryablehttp.NewClient()

func init() {
	globalClient.RetryMax = 2
}

func updatePrometheus(statusCode int) {
	metrics.ThirdPartyResponse.WithLabelValues("billing2", fmt.Sprintf("%d", 200)).Add(0)
	metrics.ThirdPartyResponse.WithLabelValues("billing2", fmt.Sprintf("%d", 400)).Add(0)
	metrics.ThirdPartyResponse.WithLabelValues("billing2", fmt.Sprintf("%d", 401)).Add(0)
	metrics.ThirdPartyResponse.WithLabelValues("billing2", fmt.Sprintf("%d", 500)).Add(0)
	metrics.ThirdPartyResponse.WithLabelValues("billing2", fmt.Sprintf("%d", 501)).Add(0)
	metrics.ThirdPartyResponse.WithLabelValues("billing2", fmt.Sprintf("%d", statusCode)).Inc()
}

func call(ctx context.Context, url, method string, input, output interface{}, headers map[string][]string) 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 := retryablehttp.NewRequestWithContext(ctx, method, url, inputMarshalled)
	if err != nil {
		return errors.Trace(err)
	}
	for k, v := range headers {
		req.Header[k] = v
	}
	client := retryablehttp.NewClient()
	client.RetryMax = 2
	client.HTTPClient = &http.Client{Transport: otelhttp.NewTransport(cleanhttp.DefaultPooledTransport())}
	resp, err := client.Do(req)
	if err != nil {
		return errors.Trace(err)
	}
	respBodyBytes, err := io.ReadAll(resp.Body)
	if err != nil {
		return errors.Trace(err)
	}
	resp.Body.Close() // nolint: errcheck

	updatePrometheus(resp.StatusCode)
	tilog.Info(ctx, "billing2 sync output", zap.String("url", url), zap.Int("status", resp.StatusCode), zap.String("response", string(respBodyBytes)))

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

func doRequestWithHeaders(ctx context.Context, apiPath string, method string, input interface{}, output interface{}, headers map[string][]string) 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, "/"))
	//ctx := threadlocal.GetContext()
	tilog.Info(ctx, "start call billing2 service", zap.String("method", method), zap.String("url", url))
	err := call(ctx, url, method, input, output, headers)
	if err != nil {
		tilog.Info(ctx, "failed call billing2 service", zap.String("method", method), zap.String("url", url), zap.Error(err))
		return err
	}
	tilog.Info(ctx, "success call billing2 service", zap.String("method", method), zap.String("url", url))
	return nil
}

func doInnerRequestWithHeaders(ctx context.Context, apiPath string, method string, input interface{}, output interface{}, headers map[string][]string) error {
	baseUrl := config.Global().BillingConfig.Billing2BaseURL
	apiPath = strings.TrimLeft(apiPath, "/")
	url := fmt.Sprintf("%s/inner/billing/%s", strings.TrimRight(baseUrl, "/"), strings.TrimLeft(apiPath, "/"))
	//ctx := threadlocal.GetContext()
	tilog.Info(ctx, "start call billing2 service", zap.String("method", method), zap.String("url", url))
	err := call(ctx, url, method, input, output, headers)
	if err != nil {
		tilog.Info(ctx, "failed call billing2 service", zap.String("method", method), zap.String("url", url), zap.Error(err))
		return err
	}
	tilog.Info(ctx, "success call billing2 service", zap.String("method", method), zap.String("url", url))
	return nil
}

func doRequest(ctx context.Context, apiPath string, method string, input interface{}, output interface{}) error {
	return doRequestWithHeaders(ctx, apiPath, method, input, output, nil)
}

func (*BillingHandlerImpl) GetTenantPlanInfo(ctx context.Context, tenantID uint64) (*TenantPlanInfo, error) {
	path := fmt.Sprintf("/tenants/%d/plans", tenantID)
	output := &TenantPlanInfo{}
	err := doRequest(ctx, path, "GET", nil, output)
	if err != nil {
		tilog.Info(ctx, "call billing GetTenantPlanInfo err", zap.Any("path", path), zap.Error(err))
		return nil, err
	}
	return output, nil
}

func (*BillingHandlerImpl) GetMonthlyBills(ctx context.Context, tenantID uint64, year int, month time.Month) (*CostExplorerResult, error) {
	path := fmt.Sprintf("/tenants/%d/costexplorer/monthly_cost/year/%d/month/%d", tenantID, year, month)
	output := &CostExplorerResult{}
	err := doRequest(ctx, path, "GET", nil, output)
	if err != nil {
		tilog.Error(ctx, "call billing GetMonthlyBills err", zap.Any("path", path), zap.Error(err))
		return nil, err
	}
	return output, nil
}

func (*BillingHandlerImpl) CreateAliyunSubscriptionRecord(ctx context.Context, aliUid string, bizId string, inTrial bool, productCode string) (*AliyunSubscriptionRecord, error) {
	path := fmt.Sprintf("/aliyun_subscription_record")
	input := &AliyunSubscriptionRecord{
		AliUid:       aliUid,
		BizId:        bizId,
		InTrial:      inTrial,
		IsSubscribed: true,
		ProductCode:  productCode,
	}
	output := &AliyunSubscriptionRecord{}
	err := doRequest(ctx, path, "POST", input, output)
	if err != nil {
		tilog.Error(ctx, "call billing CreateAliyunSubscriptionRecord err", zap.Any("path", path), zap.Error(err))
		return nil, err
	}
	return output, nil
}

func (*BillingHandlerImpl) InactiveAliyunSubscriptionRecord(ctx context.Context, bizId string) (*AliyunSubscriptionRecord, error) {
	path := fmt.Sprintf("/biz_id/%s/aliyun_subscription_record", bizId)
	output := &AliyunSubscriptionRecord{}
	err := doRequest(ctx, path, "PUT", nil, output)
	if err != nil {
		tilog.Error(ctx, "call billing InactiveAliyunSubscriptionRecord err", zap.Any("path", path), zap.Error(err))
		return nil, err
	}
	return output, nil
}

func (*BillingHandlerImpl) GetAliyunSubscriptionRecordByBizId(ctx context.Context, bizId string) (*AliyunSubscriptionRecord, error) {
	path := fmt.Sprintf("/biz_id/%s/aliyun_subscription_record", bizId)
	output := &AliyunSubscriptionRecord{}
	err := doRequest(ctx, path, "GET", nil, output)
	if err != nil {
		tilog.Info(ctx, "call billing GetAliyunSubscriptionRecordByBizId err", zap.Any("path", path), zap.Error(err))
		return nil, err
	}
	return output, nil
}

func (*BillingHandlerImpl) GetActiveSubscriptionRecordByAliUid(ctx context.Context, aliUid string) (*AliyunSubscriptionRecord, error) {
	path := fmt.Sprintf("/ali_uid/%s/aliyun_subscription_record", aliUid)
	output := &AliyunSubscriptionRecord{}
	err := doRequest(ctx, path, "GET", nil, output)
	if err != nil {
		tilog.Info(ctx, "call billing GetActiveSubscriptionRecordByAliUid err", zap.Any("path", path), zap.Error(err))
		return nil, err
	}
	return output, nil
}

func (*BillingHandlerImpl) GetAzureAccessToken(ctx context.Context) (*AzureAccessToken, error) {
	baseUrl := config.Global().BillingConfig.Billing2BaseURL
	path := "/api/v1/billing/marketplace/azure/token"
	url := baseUrl + path
	tilog.Info(ctx, "call billing2 service to get azure access token start", zap.String("url", url))
	var output string
	err := call(ctx, url, "GET", nil, &output, nil)
	if err != nil {
		tilog.Info(ctx, "call billing2 service to get azure access token error", zap.String("url", url), zap.Error(err))
		return nil, err
	}
	tilog.Info(ctx, "call billing2 service to get azure access token success", zap.String("url", url), zap.Any("output", output))

	accessToken := &AzureAccessToken{
		AccessToken: output,
	}
	return accessToken, nil
}

func (*BillingHandlerImpl) CreateOrActiveAWSSubscriptionRecord(ctx context.Context, productCode string, customerIdentifier string, inTrial bool) (*AWSSubscriptionRecord, error) {
	path := "/aws_subscription_record"
	input := &AWSSubscriptionRecord{
		ProductCode:        productCode,
		CustomerIdentifier: customerIdentifier,
		InTrial:            inTrial,
		IsSubscribed:       true,
	}
	output := &AWSSubscriptionRecord{}
	err := doRequest(ctx, path, "POST", input, output)
	if err != nil {
		tilog.Error(ctx, "call billing CreateOrActiveSubscriptionRecord err", zap.Any("path", path), zap.Error(err))
		return nil, err
	}
	return output, nil
}

func (*BillingHandlerImpl) InactiveAWSSubscriptionRecord(ctx context.Context, productCode string, customerIdentifier string) (*AWSSubscriptionRecord, error) {
	path := fmt.Sprintf("/identifier_id/%s/product_code/%s/aws_subscription_record", customerIdentifier, productCode)
	output := &AWSSubscriptionRecord{}
	err := doRequest(ctx, path, "PUT", nil, output)
	if err != nil {
		tilog.Error(ctx, "call billing InActiveSubscriptionRecord err", zap.Any("path", path), zap.Error(err))
		return nil, err
	}
	return output, nil
}

func (*BillingHandlerImpl) GetAWSSubscriptionFreeTrialStatus(ctx context.Context, accountCode string) (bool, error) {
	// default setting
	isInFreeTrial := true
	productCode := config.Global().MarketPlaceConfig.AWSProductCode
	customerIdentifier := accountCode

	if strList := strings.Split(accountCode, "-"); len(strList) != 1 {
		productCode = strList[0]
		customerIdentifier = strList[1]
		tilog.Info(context.Background(), "it's aws subscription from cppo", zap.String("customerIdentifier", customerIdentifier), zap.String("productCode", productCode))
	}
	path := fmt.Sprintf("/identifier_id/%s/product_code/%s/aws_subscription_record", customerIdentifier, productCode)
	output := &AWSSubscriptionRecord{}
	err := doRequest(ctx, path, "GET", nil, output)
	if err != nil {
		tilog.Error(ctx, "call billing GetSubscriptionRecord err", zap.Any("path", path), zap.Error(err))
		return isInFreeTrial, err
	}
	tilog.Info(context.Background(), "call billing GetSubscriptionRecord result", zap.Any("result", output))
	// subscription record is not empty
	if output.CustomerIdentifier == customerIdentifier && output.ProductCode == productCode {
		isInFreeTrial = output.InTrial
	}
	return isInFreeTrial, nil
}

func (*BillingHandlerImpl) NotifyRegistrationSource(ctx context.Context, req *NotifyRegistrationSourceReq) error {
	path := "/registration/source"
	err := doInnerRequestWithHeaders(ctx, path, "POST", req, nil, nil)
	if err != nil {
		tilog.Info(ctx, "call billing NotifyRegistrationSource err", zap.Any("path", path), zap.Any("req", req), zap.Error(err))
		return err
	}
	return nil
}

func (*BillingHandlerImpl) GetTenantPromotionStatus(ctx context.Context, tenantID uint64) (*TenantPromotionStatus, error) {
	path := fmt.Sprintf("/tenants/%d/campaigns", tenantID)
	output := &TenantPromotionStatus{}
	err := doInnerRequestWithHeaders(ctx, path, "GET", nil, output, nil)
	if err != nil {
		tilog.Info(ctx, "call billing GetTenantPromotionStatus err", zap.Any("path", path), zap.Error(err))
		return nil, err
	}
	return output, nil
}

func (*BillingHandlerImpl) BillingActionCheck(ctx context.Context, tenantID uint64, actionInfo *BillingActionInfo) (*BillingActionCheckResult, error) {
	path := fmt.Sprintf("/tenants/%d/billing_action_check", tenantID)
	output := &BillingActionCheckResult{}
	err := doRequest(ctx, path, "POST", actionInfo, output)
	if err != nil {
		tilog.Info(ctx, "call billing BillingActionCheck err", zap.Any("path", path), zap.Error(err))
		return nil, err
	}
	return output, nil
}
