package metering

import (
	"fmt"
	"maps"
	"strconv"
	"time"

	"github.com/samber/lo"
	"github.com/shopspring/decimal"

	globalcommonv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/common/v1"
	globaldedicatedv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/dedicated/v1"
	openapidedicatedv1beta1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_cloud_open_api/dedicated/v1beta1"
	"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"
	"github.com/tidbcloud/tidb-management-service/utilities/idgenerator"
	"github.com/tidbcloud/tidb-management-service/utilities/spu/storagetype"
)

func buildBillingReportForComponent(
	idGeneratorFacade idgenerator.IDGenerator,
	tenantID uint64,
	regionName string,
	replicas int,
	state globaldedicatedv1.Cluster_State,
	duration time.Duration,
	comp *globaldedicatedv1.Cluster_ClusterComponent,
	labels map[string]string,
	separateDiskNewSkuAllowed bool,
	componentType globaldedicatedv1.ComponentType,
	provider globalcommonv1.CloudProvider_Name,
) []*billing.Report {
	// don't charge for pd
	if componentType == globaldedicatedv1.ComponentType_PD {
		return nil
	}

	// all prices are labeled by per minute
	minutes_ := decimal.NewFromFloat(duration.Minutes())
	replicas_ := decimal.NewFromInt(int64(replicas))

	var records []*billing.Report

	// Add compute node SKU. Pausing, paused, resuming cluster don't charge for compute
	if !pauseOrResumeClusterStatusSet[state] {
		switch comp.GetComponentType() {
		case globaldedicatedv1.ComponentType_TIPROXY:
			records = append(records, &billing.Report{
				CostID:   strconv.FormatUint(idGeneratorFacade.MustNew(), 10),
				TenantID: tenantID,
				Sku: entity.GenerateSkuNameForSku(
					provider.String(),
					regionName,
					comp.GetInstanceType(),
					"",
					"",
					globaldedicatedv1.SKUType_SKU_TYPE_TIPROXY,
				),
				Cost:   minutes_.Mul(replicas_).Round(0).IntPart(),
				Labels: labels,
			})
		default:
			records = append(records, &billing.Report{
				CostID:   strconv.FormatUint(idGeneratorFacade.MustNew(), 10),
				TenantID: tenantID,
				Sku: entity.GenerateSkuNameForSku(
					provider.String(),
					regionName,
					comp.GetInstanceType(),
					"",
					"",
					globaldedicatedv1.SKUType_SKU_TYPE_NODE,
				),
				Cost:   minutes_.Mul(replicas_).Round(0).IntPart(),
				Labels: labels,
			})
		}
	}

	if _, ok := entity.ComponentsHaveStorage[comp.GetComponentType()]; !ok {
		return records
	}

	dataVt := comp.GetStorages()[globaldedicatedv1.Sku_DATA.String()].GetVolumeType()
	raftVt := comp.GetStorages()[globaldedicatedv1.Sku_RAFT_LOG.String()].GetVolumeType()

	switch {
	case !separateDiskNewSkuAllowed:
		// Old storage sku logic. Only metering size/iops/throughput for data storage.
		storageSizeGiDe := decimal.NewFromInt(
			int64(comp.GetStorages()[globaldedicatedv1.Sku_DATA.String()].GetSizeGi()),
		)
		basicStorageUsages := []*billing.Report{
			{
				CostID:   strconv.FormatUint(idGeneratorFacade.MustNew(), 10),
				TenantID: tenantID,
				Sku: entity.GenerateSkuNameForSku(
					provider.String(),
					regionName,
					"",
					comp.GetStorages()[globaldedicatedv1.Sku_DATA.String()].GetVolumeType().String(),
					comp.GetComponentType().String(),
					globaldedicatedv1.SKUType_SKU_TYPE_DISK_STORAGE,
				),
				Cost:   minutes_.Mul(replicas_).Mul(storageSizeGiDe).Round(0).IntPart(),
				Labels: labels,
			},
		}
		records = append(records, basicStorageUsages...)

		if dataVt == globaldedicatedv1.StorageMu_GP3 || dataVt == globaldedicatedv1.StorageMu_IO2 {
			extraStorageUsages := []*billing.Report{
				{
					CostID:   strconv.FormatUint(idGeneratorFacade.MustNew(), 10),
					TenantID: tenantID,
					Sku: entity.GenerateSkuNameForSku(
						provider.String(),
						regionName,
						"",
						dataVt.String(),
						"",
						globaldedicatedv1.SKUType_SKU_TYPE_DISK_IOPS,
					),
					Cost:   minutes_.Mul(replicas_).Round(0).IntPart(),
					Labels: labels,
				},
				{
					CostID:   strconv.FormatUint(idGeneratorFacade.MustNew(), 10),
					TenantID: tenantID,
					Sku: entity.GenerateSkuNameForSku(
						provider.String(),
						regionName,
						"",
						dataVt.String(),
						"",
						globaldedicatedv1.SKUType_SKU_TYPE_DISK_THROUGHPUT,
					),
					Cost:   minutes_.Mul(replicas_).Round(0).IntPart(),
					Labels: labels,
				},
			}

			records = append(records, extraStorageUsages...)
		}

	default:
		// New storage sku logic.
		// Refer to https://pingcap.feishu.cn/wiki/XkTuwZ7XgivWczkxOFEcHUhHnKe and https://pingcap.feishu.cn/wiki/KRPuwbY79iKEoXk0ZYGcKJv1nmb?renamingWikiNode=true.
		for diskType, storage := range comp.GetStorages() {
			if diskType == globaldedicatedv1.Sku_RAFT_LOG.String() &&
				storage.GetVolumeType() == globaldedicatedv1.StorageMu_VOLUME_TYPE_UNSPECIFIED {
				// raft log storage type is not set, skip it.
				continue
			}

			ls := maps.Clone(labels)
			// Delete misleading labels.
			delete(ls, BillingLabelIops)
			delete(ls, BillingLabelThroughput)

			var storageType openapidedicatedv1beta1.Cluster_StorageNodeSetting_StorageType
			switch comp.GetComponentType() {
			case globaldedicatedv1.ComponentType_TIKV:
				storageType = storagetype.BuildCurrentTikvStorageType(dataVt, raftVt)
			case globaldedicatedv1.ComponentType_TIFLASH:
				storageType = storagetype.BuildCurrentTiflashStorageType(dataVt, raftVt)
			}

			storageSizeGiDe := decimal.NewFromInt(int64(storage.GetSizeGi()))
			records = append(records, &billing.Report{
				CostID:   strconv.FormatUint(idGeneratorFacade.MustNew(), 10),
				TenantID: tenantID,
				Sku: entity.GenerateSkuNameForStorageNew(
					provider.String(),
					regionName,
					storageType.String(),
					diskType,
					storage.GetVolumeType().String(),
					globaldedicatedv1.SKUType_SKU_TYPE_DISK_STORAGE,
				),
				Cost:   minutes_.Mul(replicas_).Mul(storageSizeGiDe).Round(0).IntPart(),
				Labels: ls,
			}, &billing.Report{
				CostID:   strconv.FormatUint(idGeneratorFacade.MustNew(), 10),
				TenantID: tenantID,
				Sku: entity.GenerateSkuNameForStorageNew(
					provider.String(),
					regionName,
					storageType.String(),
					diskType,
					storage.GetVolumeType().String(),
					globaldedicatedv1.SKUType_SKU_TYPE_DISK_ADDITIONAL_IOPS,
				),
				Cost: minutes_.Mul(replicas_).
					Mul(decimal.NewFromInt(int64(storage.GetAdditionalIops()))).
					Round(0).
					IntPart(),
				Labels: ls,
			})

			lsIops := maps.Clone(ls)
			lsIops[BillingLabelIops] = strconv.FormatUint(uint64(storage.GetIops()), 10)
			records = append(records, &billing.Report{
				CostID:   strconv.FormatUint(idGeneratorFacade.MustNew(), 10),
				TenantID: tenantID,
				Sku: entity.GenerateSkuNameForStorageNew(
					provider.String(),
					regionName,
					storageType.String(),
					diskType,
					storage.GetVolumeType().String(),
					globaldedicatedv1.SKUType_SKU_TYPE_DISK_IOPS,
				),
				Cost:   minutes_.Mul(replicas_).Round(0).IntPart(),
				Labels: lsIops,
			})

			lsThroughput := maps.Clone(ls)
			lsThroughput[BillingLabelThroughput] = lo.Ternary(diskType == globaldedicatedv1.Sku_DATA.String(),
				convertBillingReportThroughputMi(comp.GetInstanceCpuArch(), storage.GetThroughputMibps()),
				strconv.FormatUint(uint64(storage.GetThroughputMibps()), 10),
			)
			records = append(records, &billing.Report{
				CostID:   strconv.FormatUint(idGeneratorFacade.MustNew(), 10),
				TenantID: tenantID,
				Sku: entity.GenerateSkuNameForStorageNew(
					provider.String(),
					regionName,
					storageType.String(),
					diskType,
					storage.GetVolumeType().String(),
					globaldedicatedv1.SKUType_SKU_TYPE_DISK_THROUGHPUT,
				),
				Cost:   minutes_.Mul(replicas_).Round(0).IntPart(),
				Labels: lsThroughput,
			})
		}
	}

	return records
}

func buildBillingMeasureLabel(
	tenant *account.TenantInfo,
	project *account.ProjectInfo,
	clusterId uint64,
	cpuArch globaldedicatedv1.InstanceMu_CpuArch,
	cpuCore, memoryGi, effectiveIops, effectiveThroughput uint32,
	compType globaldedicatedv1.ComponentType,
) map[string]string {
	isPoc := tenant.Plan == account.TenantPlanPOC
	labels := map[string]string{
		BillingLabelTenantID:     fmt.Sprint(tenant.ID),
		BillingLabelProjectID:    fmt.Sprint(project.ID),
		BillingLabelProjectName:  project.Name,
		BillingLabelPoc:          fmt.Sprint(isPoc),
		BillingLabelCrossAccount: fmt.Sprint(project.IsCrossAccount),
	}
	if clusterId != 0 {
		labels[BillingLabelClusterID] = fmt.Sprint(clusterId)
	}

	labels[BillingLabelDevTier] = "false"
	labels[BillingLabelIops] = strconv.FormatUint(uint64(effectiveIops), 10)
	labels[BillingLabelThroughput] = convertBillingReportThroughputMi(cpuArch, effectiveThroughput)
	labels[BillingLabelCpuNum] = fmt.Sprint(cpuCore)
	labels[BillingLabelMemoryGiB] = fmt.Sprint(memoryGi)
	labels[BillingLabelComponent] = cvtComponentType(compType)

	return labels
}

const (
	BillingLabelTenantID     = "tenantID"
	BillingLabelProjectID    = "projectID"
	BillingLabelProjectName  = "project"
	BillingLabelPoc          = "poc"
	BillingLabelCrossAccount = "crossAccount"
	BillingLabelClusterID    = "clusterID"
	BillingLabelDevTier      = "devTier"
	BillingLabelIops         = "iops"
	BillingLabelThroughput   = "throughput"
	BillingLabelComponent    = "component"
	BillingLabelCpuNum       = "cpuNum"
	BillingLabelMemoryGiB    = "memoryGiB"
)

var (
	skuTypeMap = map[string]globaldedicatedv1.SKUType{
		"node":                 globaldedicatedv1.SKUType_SKU_TYPE_NODE,
		"disk-storage":         globaldedicatedv1.SKUType_SKU_TYPE_DISK_STORAGE,
		"disk-iops":            globaldedicatedv1.SKUType_SKU_TYPE_DISK_IOPS,
		"disk-throughput":      globaldedicatedv1.SKUType_SKU_TYPE_DISK_THROUGHPUT,
		"disk-additional-iops": globaldedicatedv1.SKUType_SKU_TYPE_DISK_ADDITIONAL_IOPS,
		"tiproxy":              globaldedicatedv1.SKUType_SKU_TYPE_TIPROXY,
	}

	diskSkuTypeSet = map[globaldedicatedv1.SKUType]bool{
		globaldedicatedv1.SKUType_SKU_TYPE_DISK_STORAGE:         true,
		globaldedicatedv1.SKUType_SKU_TYPE_DISK_IOPS:            true,
		globaldedicatedv1.SKUType_SKU_TYPE_DISK_THROUGHPUT:      true,
		globaldedicatedv1.SKUType_SKU_TYPE_DISK_ADDITIONAL_IOPS: true,
	}

	pauseOrResumeClusterStatusSet = map[globaldedicatedv1.Cluster_State]bool{
		globaldedicatedv1.Cluster_PAUSING:  true,
		globaldedicatedv1.Cluster_PAUSED:   true,
		globaldedicatedv1.Cluster_RESUMING: true,
	}
)

func cvtComponentType(componentType globaldedicatedv1.ComponentType) string {
	switch componentType {
	case globaldedicatedv1.ComponentType_TIDB:
		return "COMPONENT_TYPE_TIDB"
	case globaldedicatedv1.ComponentType_TIKV:
		return "COMPONENT_TYPE_TIKV"
	case globaldedicatedv1.ComponentType_TIFLASH:
		return "COMPONENT_TYPE_TIFLASH"
	case globaldedicatedv1.ComponentType_PD:
		return "COMPONENT_TYPE_PD"
	case globaldedicatedv1.ComponentType_TIPROXY:
		return "COMPONENT_TYPE_TIPROXY"
	default:
		return "COMPONENT_TYPE_UNSPECIFIED"
	}
}

// We use different storage configurations for x86 and ARM ,
// while the list price on our website is only one for each node type,
// so we need to convert the storage configurations to the list price
// on the website.
// For ARM, we use the list price of x86
// It's hardcore, we should change it in the future on tidb-mgmt.
// We'll add a new billing report field to the storage configuration
//
//	and use this to indicate the data that really needs to be reported for metering
//
// refer: https://pingcap.feishu.cn/wiki/NFKewkwaTi9rmUkZ5QecZHcKnCe
func convertBillingReportThroughputMi(cpuArch globaldedicatedv1.InstanceMu_CpuArch, effectiveThroughput uint32) string {
	billingReportThroughputMi := map[uint32]uint32{
		149: 144,
		297: 288,
	}

	// Hard-coded throughput takes 1st precedence.
	if cpuArch == globaldedicatedv1.InstanceMu_ARM64 {
		if v, ok := billingReportThroughputMi[effectiveThroughput]; ok {
			return strconv.FormatUint(uint64(v), 10)
		}
	}

	// customized throughput takes the last precedence.
	return strconv.FormatUint(uint64(effectiveThroughput), 10)
}
