package billing

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

	"github.com/go-resty/resty/v2"
	"github.com/tidbcloud/pkgv2/goroutine"

	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/adapter/billing"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/pkg/config"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
	"github.com/tidbcloud/tidb-management-service/utilities/httputil"
)

type billingClient struct {
	host   string
	client *resty.Client
}

func NewBillingClient(cfg *config.Config) billing.Client {
	return &billingClient{
		client: resty.New().SetTimeout(10 * time.Second),
		host:   cfg.ServerConfig.DependentSvcAddresses.Billing2,
	}
}

func (c billingClient) callBilling(
	ctx context.Context,
	method, apiPath string,
	pathParams map[string]string,
	body, output any,
) (*resty.Response, bizerr.BizError) {
	baseUrl := fmt.Sprintf("http://%s", c.host)

	resp, err := httputil.DoRequest(ctx, c.client, method, baseUrl, apiPath, func(req *resty.Request) {
		req.SetPathParams(pathParams)
		req.ForceContentType("application/json")
		req.SetBody(body)
	})
	if err != nil {
		return nil, bizerr.New(bizerr.ErrCodeSvcBilling, "http request failed: %s", err)
	}

	if resp.StatusCode() != http.StatusOK {
		return resp, bizerr.New(bizerr.ErrCodeSvcBilling, " http status code != 200")
	}

	if output != nil {
		if err := json.Unmarshal(resp.Body(), output); err != nil {
			return resp, bizerr.New(bizerr.ErrCodeSvcBilling, "unmarshal response failed: %s", err)
		}
	}

	return resp, nil
}

func (c billingClient) BillingActionCheck(
	ctx context.Context,
	action billing.Action,
	tenantID uint64,
	resource *billing.Resource,
) (*billing.ActionCheckResult, bizerr.BizError) {
	const (
		path = "/api/v1/billing/tenants/{tenant_id}/billing_action_check"
	)

	body := map[string]any{
		"action":   action,
		"resource": resource,
	}
	result := &billing.ActionCheckResult{}
	resp, err := c.callBilling(
		ctx,
		resty.MethodPost,
		path,
		map[string]string{"tenant_id": fmt.Sprint(tenantID)},
		body,
		result,
	)
	if err != nil {
		return nil, err.Chain("BillingActionCheck")
	}
	if resp.StatusCode() != http.StatusOK {
		return nil, bizerr.New(bizerr.ErrCodeSvcBilling, " http status code != 200")
	}
	return result, nil
}

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 {
	Price    string `json:"price"`
	RawPrice string `json:"raw_price"`
}

func (c billingClient) CalcFee(
	ctx context.Context,
	records []*billing.Report,
) (map[string]*billing.Charge, bizerr.BizError) {
	calcFee := func(ctx context.Context, record *billing.Report) (*billing.Charge, bizerr.BizError) {
		const path = "/api/v1/billing/tenants/{tenant_id}/measures/dryrun"

		body := billingMeasureParam{
			CostID:        record.CostID,
			Cost:          record.Cost,
			CostTimestamp: time.Now().Unix(),
			Labels:        record.Labels,
			Unit:          record.Sku,
		}
		result := &billingMeasureResult{}
		if _, err := c.callBilling(
			ctx,
			resty.MethodPost,
			path,
			map[string]string{"tenant_id": fmt.Sprint(record.TenantID)},
			body,
			result,
		); err != nil {
			return nil, err.Chain("calcFee")
		}

		price, err := strconv.ParseFloat(result.Price, 64)
		if err != nil {
			return nil, bizerr.WrapError(err, "invalid price: %s", result.Price)
		}
		rawPrice, err := strconv.ParseFloat(result.RawPrice, 64)
		if err != nil {
			return nil, bizerr.WrapError(err, "invalid raw_price: %s", result.RawPrice)
		}
		return &billing.Charge{RawPrice: rawPrice, Price: price}, nil
	}

	out := make(map[string]*billing.Charge)
	var mu sync.Mutex
	var fns []goroutine.Handle
	for _, each := range records {
		record := each
		fns = append(fns, func() error {
			price, err := 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
}
