// Package resourcepoolcapacity projects the public TiKV and TiFlash Compute
// capacity view from regional shared-pool components.
package resourcepoolcapacity

import (
	"strings"

	clusterngv1beta "github.com/tidbcloud/cluster-service-ng/api/cluster-service-ng/v1beta"

	globalngv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/nextgen/v1"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
)

const (
	publicDataStorageName = "data"
	hiddenStorageName     = ""
)

// Project returns the public capacity represented by regional shared-pool
// component groups. Components outside TiKV and TiFlash Compute are ignored.
func Project(
	components []*clusterngv1beta.Component,
) (*globalngv1.ResourcePoolCapacity, bizerr.BizError) {
	var tikv *clusterngv1beta.Component
	var tiflashCompute *clusterngv1beta.Component

	for _, component := range components {
		if component == nil {
			return nil, bizerr.New(bizerr.ErrCodeRegionalAPI, "shared pool component is nil")
		}

		switch component.GetComponentType() {
		case clusterngv1beta.ComponentType_TIKV:
			if tikv != nil {
				return nil, bizerr.New(bizerr.ErrCodeRegionalAPI, "duplicate TIKV component")
			}
			tikv = component
		case clusterngv1beta.ComponentType_TIFLASH_COMPUTE:
			if tiflashCompute != nil {
				return nil, bizerr.New(bizerr.ErrCodeRegionalAPI, "duplicate TIFLASH_COMPUTE component")
			}
			tiflashCompute = component
		default:
			continue
		}
	}

	if tikv == nil {
		return nil, bizerr.New(bizerr.ErrCodeRegionalAPI, "exactly one TIKV component is required")
	}

	tikvCapacity, err := projectComponent(
		tikv,
		map[clusterngv1beta.Component_Storage_Name]string{
			clusterngv1beta.Component_Storage_DATA:    publicDataStorageName,
			clusterngv1beta.Component_Storage_RAFTLOG: hiddenStorageName,
		},
		true,
	)
	if err != nil {
		return nil, err
	}

	capacity := &globalngv1.ResourcePoolCapacity{
		Tikv: tikvCapacity,
	}
	if tiflashCompute == nil {
		return capacity, nil
	}
	if tiflashCompute.GetNodeQuota().GetNodeCount() == 0 {
		return capacity, nil
	}

	tiflashCapacity, err := projectComponent(
		tiflashCompute,
		map[clusterngv1beta.Component_Storage_Name]string{
			clusterngv1beta.Component_Storage_DATA: publicDataStorageName,
		},
		false,
	)
	if err != nil {
		return nil, err
	}
	capacity.Tiflash = tiflashCapacity

	return capacity, nil
}

// ProjectScaleTargets projects the same TiKV/TiFlash Compute capacity shape
// from the normalized targets returned by the existing scale preview.
func ProjectScaleTargets(
	tikv *globalngv1.ByocSharedComponentScaleTarget,
	tiflashCompute *globalngv1.ByocSharedComponentScaleTarget,
	includeZeroCompute bool,
) (*globalngv1.ResourcePoolCapacity, bizerr.BizError) {
	tikvCapacity, err := projectScaleTarget(
		"TIKV",
		tikv,
		map[string]string{
			"DATA":    publicDataStorageName,
			"RAFTLOG": hiddenStorageName,
		},
		true,
	)
	if err != nil {
		return nil, err
	}

	capacity := &globalngv1.ResourcePoolCapacity{Tikv: tikvCapacity}
	if tiflashCompute == nil || (!includeZeroCompute && tiflashCompute.GetNodeCount() == 0) {
		return capacity, nil
	}
	tiflashCapacity, err := projectScaleTarget(
		"TIFLASH_COMPUTE",
		tiflashCompute,
		map[string]string{"DATA": publicDataStorageName},
		false,
	)
	if err != nil {
		return nil, err
	}
	capacity.Tiflash = tiflashCapacity
	return capacity, nil
}

func projectScaleTarget(
	componentType string,
	target *globalngv1.ByocSharedComponentScaleTarget,
	storageNames map[string]string,
	requirePositiveNodeCount bool,
) (*globalngv1.ComponentCapacity, bizerr.BizError) {
	if target == nil || target.NodeCount == nil || target.Cpu == nil || target.MemoryGi == nil {
		return nil, bizerr.New(bizerr.ErrCodeRegionalAPI, "%s normalized target is incomplete", componentType)
	}
	if target.GetCpu() == 0 || target.GetMemoryGi() == 0 {
		return nil, bizerr.New(bizerr.ErrCodeRegionalAPI, "%s normalized node spec must be positive", componentType)
	}
	if requirePositiveNodeCount && target.GetNodeCount() == 0 {
		return nil, bizerr.New(bizerr.ErrCodeRegionalAPI, "%s normalized node count must be positive", componentType)
	}

	nodeCount := int64(target.GetNodeCount())
	component := &globalngv1.ComponentCapacity{
		NodeSpec: &globalngv1.NodeSpecSelection{
			Cpu:      int64(target.GetCpu()),
			MemoryGi: int64(target.GetMemoryGi()),
		},
		NodeCount: &nodeCount,
	}
	seen := make(map[string]struct{}, len(target.GetStorages()))
	for _, storage := range target.GetStorages() {
		if storage == nil || storage.SizeGi == nil {
			return nil, bizerr.New(bizerr.ErrCodeRegionalAPI, "%s normalized storage is incomplete", componentType)
		}
		name := strings.ToUpper(strings.TrimSpace(storage.GetStorageName()))
		publicName, ok := storageNames[name]
		if !ok {
			return nil, bizerr.New(bizerr.ErrCodeRegionalAPI, "unsupported %s storage %s", componentType, name)
		}
		if _, ok := seen[name]; ok {
			return nil, bizerr.New(bizerr.ErrCodeRegionalAPI, "duplicate %s storage %s", componentType, name)
		}
		seen[name] = struct{}{}
		if publicName == hiddenStorageName {
			continue
		}
		component.Storages = append(component.Storages, &globalngv1.Storage{
			Name:   publicName,
			SizeGi: int64(storage.GetSizeGi()),
		})
	}
	return component, nil
}

func projectComponent(
	component *clusterngv1beta.Component,
	storageNames map[clusterngv1beta.Component_Storage_Name]string,
	requirePositiveNodeCount bool,
) (*globalngv1.ComponentCapacity, bizerr.BizError) {
	componentType := component.GetComponentType().String()
	nodeQuota := component.GetNodeQuota()
	if nodeQuota == nil {
		return nil, bizerr.New(bizerr.ErrCodeRegionalAPI, "%s node quota is required", componentType)
	}
	nodeSpec := nodeQuota.GetNodeSpec()
	if nodeSpec == nil {
		return nil, bizerr.New(bizerr.ErrCodeRegionalAPI, "%s node spec is required", componentType)
	}
	if nodeSpec.GetCpu() == 0 || nodeSpec.GetMemoryGi() == 0 {
		return nil, bizerr.New(
			bizerr.ErrCodeRegionalAPI,
			"%s node spec CPU and memory must be positive",
			componentType,
		)
	}
	if requirePositiveNodeCount && nodeQuota.GetNodeCount() == 0 {
		return nil, bizerr.New(bizerr.ErrCodeRegionalAPI, "%s node count must be positive", componentType)
	}

	storages := make([]*globalngv1.Storage, 0, len(component.GetStorages()))
	seenStorageNames := make(map[clusterngv1beta.Component_Storage_Name]struct{}, len(component.GetStorages()))
	for _, storage := range component.GetStorages() {
		if storage == nil {
			return nil, bizerr.New(bizerr.ErrCodeRegionalAPI, "%s storage is nil", componentType)
		}

		storageName := storage.GetName()
		publicName, ok := storageNames[storageName]
		if !ok {
			return nil, bizerr.New(
				bizerr.ErrCodeRegionalAPI,
				"unsupported %s storage %s",
				componentType,
				storageName.String(),
			)
		}
		if _, exists := seenStorageNames[storageName]; exists {
			return nil, bizerr.New(
				bizerr.ErrCodeRegionalAPI,
				"duplicate %s storage %s",
				componentType,
				storageName.String(),
			)
		}
		seenStorageNames[storageName] = struct{}{}
		if publicName == hiddenStorageName {
			continue
		}
		storages = append(storages, &globalngv1.Storage{
			Name:   publicName,
			SizeGi: int64(storage.GetSizeGi()),
		})
	}

	nodeCount := int64(nodeQuota.GetNodeCount())
	return &globalngv1.ComponentCapacity{
		NodeSpec: &globalngv1.NodeSpecSelection{
			Cpu:      int64(nodeSpec.GetCpu()),
			MemoryGi: int64(nodeSpec.GetMemoryGi()),
		},
		NodeCount: &nodeCount,
		Storages:  storages,
	}, nil
}
