package metering

import (
	"context"
	"strings"
	"time"

	globalcommonv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/common/v1"
	globaldedicatedv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/dedicated/v1"
	"github.com/tidbcloud/tidb-management-service/dedicated-global-service/domain/adapter/account"
	"github.com/tidbcloud/tidb-management-service/dedicated-global-service/domain/adapter/billing"
	"github.com/tidbcloud/tidb-management-service/dedicated-global-service/domain/entity"
	clustersvc "github.com/tidbcloud/tidb-management-service/dedicated-global-service/domain/service/cluster"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
	"github.com/tidbcloud/tidb-management-service/utilities/enum"
	"github.com/tidbcloud/tidb-management-service/utilities/featuregate"
	"github.com/tidbcloud/tidb-management-service/utilities/idgenerator"
)

type CalcClusterFeeSvc struct {
	billingFacade billing.Client
	tenantFacade  account.Client
	uuidFacade    idgenerator.IDGenerator
	featGateCli   featuregate.Service
	clusterSvc    clustersvc.Service
}

func NewCalcClusterFeeSvc(
	billingFacade billing.Client,
	tenantFacade account.Client,
	uuidFacade idgenerator.IDGenerator,
	featGateCli featuregate.Service,
	clusterSvc clustersvc.Service,
) *CalcClusterFeeSvc {
	return &CalcClusterFeeSvc{
		billingFacade: billingFacade,
		tenantFacade:  tenantFacade,
		uuidFacade:    uuidFacade,
		featGateCli:   featGateCli,
		clusterSvc:    clusterSvc,
	}
}

func (svc *CalcClusterFeeSvc) Execute(
	ctx context.Context,
	req *globaldedicatedv1.CalcClusterFeeRequest,
) ([]*globaldedicatedv1.CalcClusterFeeResponse_ComponentCost, bizerr.BizError) {
	tenant, err := svc.tenantFacade.GetTenant(ctx, req.TenantId)
	if err != nil {
		return nil, err
	}
	project, err := svc.tenantFacade.GetProject(ctx, req.ProjectId)
	if err != nil {
		return nil, err
	}
	regionName, err_ := enum.FromString[globalcommonv1.Region_Name](req.RegionName)
	if err_ != nil {
		return nil, bizerr.WrapErrorWithCode(
			err_,
			bizerr.ErrCodeParams,
			"regionName is invalid, name=%s",
			req.RegionName,
		)
	}
	providerName, err_ := enum.FromString[globalcommonv1.CloudProvider_Name](req.CloudProviderName)
	if err_ != nil {
		return nil, bizerr.WrapErrorWithCode(
			err_,
			bizerr.ErrCodeParams,
			"cloudProviderName is invalid, name=%s",
			req.CloudProviderName,
		)
	}

	dryrunCluster, err := svc.clusterSvc.Create(ctx, &globaldedicatedv1.CreateClusterRequest{
		Cluster: &globaldedicatedv1.Cluster{
			OrganizationId:    req.GetTenantId(),
			ProjectId:         req.GetProjectId(),
			CloudProviderId:   globalcommonv1.CloudProvider_Name(providerName),
			RegionId:          globalcommonv1.Region_Name(regionName),
			Port:              4000, // dummy port.
			ClusterComponents: req.GetComponents(),
		},
		ValidateOnly: true,
	})
	if err != nil {
		return nil, bizerr.WrapError(err, "create cluster dry run failed")
	}

	var costs []*globaldedicatedv1.CalcClusterFeeResponse_ComponentCost
	for _, each := range dryrunCluster.GetClusterComponents() {
		if _, ok := entity.ComponentsUserEditable[each.ComponentType]; !ok {
			continue
		}
		cost, err := svc.calcComponentFee(
			ctx,
			req,
			tenant,
			project,
			time.Duration(req.Duration),
			each,
			globalcommonv1.CloudProvider_Name(providerName),
		)
		if err != nil {
			return nil, err
		}
		costs = append(costs, cost)
	}

	return costs, nil
}

func (svc *CalcClusterFeeSvc) calcComponentFee(
	ctx context.Context,
	req *globaldedicatedv1.CalcClusterFeeRequest,
	tenant *account.TenantInfo,
	project *account.ProjectInfo,
	duration time.Duration,
	component *globaldedicatedv1.Cluster_ClusterComponent,
	providerName globalcommonv1.CloudProvider_Name,
) (*globaldedicatedv1.CalcClusterFeeResponse_ComponentCost, bizerr.BizError) {
	labels := buildBillingMeasureLabel(
		tenant,
		project,
		0,
		component.GetInstanceCpuArch(),
		component.GetCpuNum(),
		component.GetMemorySizeGi(),
		component.GetStorages()[globaldedicatedv1.Sku_DATA.String()].GetIops(),            // legacy billing label need data disk specs.
		component.GetStorages()[globaldedicatedv1.Sku_DATA.String()].GetThroughputMibps(), // legacy billing label need data disk specs.
		component.GetComponentType(),
	)

	clusterStatus := globaldedicatedv1.Cluster_AVAILABLE
	if req.IsClusterPaused {
		clusterStatus = globaldedicatedv1.Cluster_PAUSING
	}

	reports := buildBillingReportForComponent(
		svc.uuidFacade,
		tenant.ID,
		req.RegionName,
		int(component.GetCount()),
		clusterStatus,
		duration,
		component,
		labels,
		!req.GetPromoted(), // if promoted, use old storage sku. [https://pingcap.feishu.cn/wiki/SWfnwtCBZid1bdk5a7FcamQcn5g#share-E8Uddcpw2onjx1xnUVuc9pcanSh](price promotion until 2025/May)
		component.GetComponentType(),
		providerName,
	)

	cost, err := svc.billingFacade.CalcFee(ctx, reports)
	if err != nil {
		// Compatibility: if billing service returns error and promote = false (new sku), retry with old sku.
		if req.GetPromoted() {
			return nil, err.Chain("calcTiDBFee")
		}
		reports := buildBillingReportForComponent(
			svc.uuidFacade,
			tenant.ID,
			req.RegionName,
			int(component.GetCount()),
			clusterStatus,
			duration,
			component,
			labels,
			false,
			component.GetComponentType(),
			providerName,
		)
		cost, err = svc.billingFacade.CalcFee(ctx, reports)
		if err != nil {
			return nil, err.Chain("calcTiDBFee")
		}
	}

	out := &globaldedicatedv1.CalcClusterFeeResponse_ComponentCost{
		ComponentType: component.ComponentType,
	}
	for k, v := range cost {
		if k == "" {
			return nil, bizerr.New(bizerr.ErrCodeUnknown, "billing returned sku key is empty")
		}
		if diskSkuTypeSet[skuTypeMap[strings.Split(k, ":")[0]]] {
			// storage
			out.Storage += v.Price
			out.RawStorage += v.RawPrice
		} else {
			// compute
			out.Compute += v.Price
			out.RawCompute += v.RawPrice
		}
	}

	return out, nil
}
