package cluster

import (
	"context"
	"strconv"
	"time"

	"github.com/samber/lo"

	bffclusterv1beta1 "github.com/tidbcloud/tidb-management-service/api/spec/bff/cluster_management/v1beta1"
	globaldedicatedv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/dedicated/v1"
	openapicommonv1beta1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_cloud_open_api/common/v1beta1"
	openapidedicatedv1beta1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_cloud_open_api/dedicated/v1beta1"
	appcommon "github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/app/common"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/pkg/common"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
	"github.com/tidbcloud/tidb-management-service/utilities/enum"
	"github.com/tidbcloud/tidb-management-service/utilities/grpc/metadata"
)

var defaultDuration = 1 * time.Hour

func (s *Server) EstimatePrice(
	ctx context.Context,
	req *bffclusterv1beta1.EstimatePriceRequest,
) (*bffclusterv1beta1.EstimatePriceResponse, error) {
	// Extract request meta.
	m, err := metadata.ExtractRequestMetaWithCheckPermission(ctx)
	if err != nil {
		return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "failed to extract request metadata")
	}

	// check if the request is valid
	if req.ProjectId == "" {
		return nil, bizerr.New(bizerr.ErrCodeParams, "project_id is required")
	}

	projectId, err := strconv.ParseUint(req.ProjectId, 10, 64)
	if err != nil {
		return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "project_id is invalid")
	}

	providerStr, regionStr, err := common.ParseOpenApiRegionNameOrId(req.GetRegionId())
	if err != nil {
		return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "invalid region, please check and try again")
	}

	provider, err := enum.FromString[openapicommonv1beta1.Region_CloudProvider](providerStr)
	if err != nil {
		return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "invalid cloud provider")
	}

	if len(req.GetTidbNodeSetting().GetTidbNodeGroups()) < 1 {
		return nil, bizerr.New(bizerr.ErrCodeParams, "at least one tidb node group is required")
	}

	// Build components for price estimation. Delegate the logic to creating cluster process.
	// Calculate the total number of TiDB and TiProxy nodes across all node groups. TiDB and TiProxy instances across different node groups share the same SPU spec.
	tidbNodeCountSum := lo.SumBy(
		req.GetTidbNodeSetting().GetTidbNodeGroups(),
		func(i *openapidedicatedv1beta1.TidbNodeGroup) int32 { return i.GetNodeCount() },
	)
	tiproxyNodeCountSum := lo.SumBy(
		req.GetTidbNodeSetting().GetTidbNodeGroups(),
		func(i *openapidedicatedv1beta1.TidbNodeGroup) int32 { return i.GetTiproxySetting().GetNodeCount() },
	)

	tikvNodeSpec, tiflashNodeSpec, err := appcommon.GetPriceNodeSpecs(
		ctx,
		s.openAPIDedicatedRegionServer,
		req.GetRegionId(),
		req.GetTikvNodeSetting().GetNodeSpecKey(),
		req.GetTiflashNodeSetting().GetNodeSpecKey(),
		req.GetProjectId(),
	)
	if err != nil {
		return nil, err
	}

	comps, err := appcommon.BuildPriceClusterComponents(
		req.GetTidbNodeSetting().GetNodeSpecKey(),
		req.GetTidbNodeSetting().GetTidbNodeGroups()[0].GetTiproxySetting().GetNodeSpecKey(),
		tidbNodeCountSum,
		req.GetTikvNodeSetting().GetNodeCount(),
		req.GetTiflashNodeSetting().GetNodeCount(),
		tiproxyNodeCountSum,
		req.GetTikvNodeSetting().GetStorageType(),
		req.GetTiflashNodeSetting().GetStorageType(),
		req.GetTikvNodeSetting().GetStorageSizeGi(),
		req.GetTiflashNodeSetting().GetStorageSizeGi(),
		req.GetTikvNodeSetting().GetRaftStoreIops(),
		req.GetTikvNodeSetting().GetDataStoreIops(),
		openapicommonv1beta1.Region_CloudProvider(provider),
		tikvNodeSpec,
		tiflashNodeSpec,
	)
	if err != nil {
		return nil, err
	}

	durationNano := req.GetDuration().AsDuration().Nanoseconds()

	resp, err := s.dedicatedCli.CalcClusterFee(ctx, &globaldedicatedv1.CalcClusterFeeRequest{
		TenantId:          m.OrgID,
		ProjectId:         projectId,
		CloudProviderName: providerStr,
		RegionName:        regionStr,
		Duration:          lo.Ternary(durationNano == 0, defaultDuration.Nanoseconds(), durationNano),
		Components:        comps,
		IsClusterPaused:   req.GetPaused(),
		Promoted:          req.Promoted,
	})
	if err != nil {
		return nil, bizerr.WrapError(err, "failed to calculate price")
	}

	res := lo.Map(
		resp.GetCosts(),
		func(cost *globaldedicatedv1.CalcClusterFeeResponse_ComponentCost, _ int) *bffclusterv1beta1.EstimatePriceResponse_Price {
			return &bffclusterv1beta1.EstimatePriceResponse_Price{
				ComponentType:          common.CvtComponentTypeToOpenapi(cost.GetComponentType()),
				ComputeOriginalPrice:   cost.GetRawCompute(),
				ComputeDiscountedPrice: cost.GetCompute(),
				StorageOriginalPrice:   cost.GetRawStorage(),
				StorageDiscountedPrice: cost.GetStorage(),
			}
		},
	)

	return &bffclusterv1beta1.EstimatePriceResponse{
		Prices: res,
	}, nil
}
