package billingcli

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

	"github.com/go-resty/resty/v2"
	"github.com/shopspring/decimal"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.opentelemetry.io/otel"
	"go.uber.org/zap"

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

var tracer = otel.Tracer("billing_imp")

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

func NewBillingClient(cfg *config.Config) billing.Client {
	var host string
	if cfg.Billing == nil {
		host = ""
	} else {
		host = cfg.Billing.Billing2Url
	}
	if cfg.Billing.Local {
		return &fakeBillingClient{}
	}
	return &billingClient{
		client: resty.New().SetTimeout(10 * time.Second),
		host:   host,
	}
}

type fakeBillingClient struct{}

var TestSpendLimitSetting = billing.SpendLimitSetting{
	SpendLimitSettingID: 1,
	TenantID:            0,
	Interval:            "minutes",
	CostType:            "serverless_cluster",
	RangeType:           "cluster",
	RangeID:             0, // fake test cluster id
	Limit:               decimal.New(100, 0),
	Status:              "active",
	BizType:             "serverless_cluster",
	BizData:             `{"free_credits":"100"}`,
}

func (f fakeBillingClient) GetSpendLimitSettings(
	ctx context.Context,
	tenantId uint64,
) ([]*billing.SpendLimitSetting, bizerr.BizError) {
	TestSpendLimitSetting.TenantID = tenantId
	return []*billing.SpendLimitSetting{&TestSpendLimitSetting}, nil
}

func (f fakeBillingClient) SendClusterEventToUNC(
	ctx context.Context,
	tenantID uint64,
	event *billing.UNCEvent,
) bizerr.BizError {
	// Fake implementation, just return nil to indicate success.
	return nil
}

func (f fakeBillingClient) BillingActionCheck(
	ctx context.Context,
	action billing.Action,
	tenantID uint64,
	resource *billing.Resource,
) (*billing.ActionCheckResult, bizerr.BizError) {
	return &billing.ActionCheckResult{Pass: true}, nil
}

func (c billingClient) SendClusterEventToUNC(
	ctx context.Context,
	tenantID uint64,
	event *billing.UNCEvent,
) bizerr.BizError {
	const path = "/api/v1/unc/tenants/{tenant_id}/bulk_trigger"
	ctx, span := tracer.Start(ctx, "billingClient.SendClusterEventToUNC")
	defer span.End()

	pathParams := map[string]string{
		"tenant_id": strconv.FormatUint(tenantID, 10),
	}

	output := struct {
		Status string `json:"status"`
	}{}

	err := c.callBilling(ctx, c.client, resty.MethodPost, path, pathParams, event, &output)
	if err != nil {
		return err.Chain("SendClusterEventToUNC failed")
	}
	if output.Status != "success" {
		return bizerr.New(bizerr.ErrCodeSvcBilling, "send UNC event failed: %s", output.Status)
	}

	return nil
}

func (c billingClient) GetSpendLimitSettings(
	ctx context.Context,
	tenantId uint64,
) ([]*billing.SpendLimitSetting, bizerr.BizError) {
	// must include has_serverless_free_credits=true to avoid large tenant cause billing svc oom
	const path = "/api/v1/billing/tenants/{tenant_id}/spend_limits?biz_type=serverless_cluster&range_type=cluster&has_serverless_free_credits=true"
	ctx, span := tracer.Start(ctx, "billingClient.GetSpendLimitSettings")
	defer span.End()

	settings := make([]*billing.SpendLimitSetting, 0)
	err := c.callBilling(
		ctx,
		c.client,
		resty.MethodGet,
		path,
		map[string]string{"tenant_id": fmt.Sprint(tenantId)},
		nil,
		&settings,
	)
	if err != nil {
		return nil, err.Chain("GetSpendLimitSettings failed")
	}
	return settings, 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"
	ctx, span := tracer.Start(ctx, "billingClient.BillingActionCheck")
	defer span.End()

	body := map[string]any{
		"action":   action,
		"resource": resource,
	}
	result := &billing.ActionCheckResult{}
	err := c.callBilling(
		ctx,
		c.client,
		resty.MethodPost,
		path,
		map[string]string{"tenant_id": fmt.Sprint(tenantID)},
		body,
		result,
	)
	if err != nil {
		return nil, err.Chain("BillingActionCheck failed")
	}
	return result, nil
}

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

	request := client.R().EnableTrace().SetPathParams(pathParams).SetContext(ctx)
	if body != nil {
		request = request.SetBody(body)
	}
	resp, err := request.Execute(method, url)
	if err != nil {
		return bizerr.WrapErrorWithCode(err, bizerr.ErrCodeSvcBilling, "callBilling failed")
	}
	bodyBytes, _ := json.Marshal(body)

	l := tilog.WithContextL(ctx).With(
		zap.String("method", request.Method),
		zap.String("url", request.URL),
		zap.Int("status", resp.StatusCode()),
		zap.String("req", string(bodyBytes)),
		zap.String("resp", string(resp.Body())),
	)

	l.Info("callBilling")

	if resp.StatusCode() != http.StatusOK {
		l.Error("callBilling http status code != 200")
		return bizerr.New(bizerr.ErrCodeSvcBilling, " http status code != 200")
	}
	if output != nil {
		if err := json.Unmarshal(resp.Body(), output); err != nil {
			l.Error("callBilling unmarshal response failed")
			return bizerr.WrapErrorWithCode(
				err,
				bizerr.ErrCodeSvcBilling,
				"callBilling unmarshal response failed",
			)
		}
	}
	return nil
}
