package resourcepool

import (
	"context"
	"math"
	"net/netip"
	"slices"
	"strconv"
	"strings"

	clusterngv1beta "github.com/tidbcloud/cluster-service-ng/api/cluster-service-ng/v1beta"
	"google.golang.org/grpc/codes"
	"google.golang.org/protobuf/proto"

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

const (
	bootstrapStorageData    = "data"
	bootstrapStorageRaftlog = "raftlog"

	bootstrapCapacityVcpusLimitError = "The selected TiKV and TiFlash capacity exceeds 50% of the Pool vCPU Limit. Increase the limit or reduce the initial capacity."
)

type bootstrapPreflightResult struct {
	capacity *globalngv1.ResourcePoolCapacity
}

func validateBootstrapCapacityVcpusLimit(
	bootstrap *globalngv1.ResourcePoolBootstrapCapacity,
	vcpusLimit uint64,
) bizerr.BizError {
	if bootstrap == nil || vcpusLimit == 0 {
		return nil
	}

	components := []*globalngv1.ComponentCapacity{
		bootstrap.GetTikv(),
		bootstrap.GetTiflash(),
	}
	var usedVcpus uint64
	for _, component := range components {
		if component == nil {
			continue
		}

		cpu := component.GetNodeSpec().GetCpu()
		nodeCount := component.GetNodeCount()
		if cpu <= 0 || nodeCount <= 0 {
			continue
		}

		componentVcpus := uint64(cpu)
		componentNodeCount := uint64(nodeCount)
		if componentVcpus > math.MaxUint64/componentNodeCount {
			return bootstrapCapacityVcpusLimitExceeded()
		}
		componentVcpus *= componentNodeCount
		if usedVcpus > math.MaxUint64-componentVcpus {
			return bootstrapCapacityVcpusLimitExceeded()
		}
		usedVcpus += componentVcpus
	}

	if usedVcpus > vcpusLimit/2 {
		return bootstrapCapacityVcpusLimitExceeded()
	}
	return nil
}

func bootstrapCapacityVcpusLimitExceeded() bizerr.BizError {
	return bizerr.New(bizerr.ErrCodeParams, "%s", bootstrapCapacityVcpusLimitError)
}

func (s *serviceImpl) preflightBootstrapCreate(
	ctx context.Context,
	request *globalngv1.CreateResourcePoolRequest,
	cloudRegion *entity.Region,
) (*bootstrapPreflightResult, bizerr.BizError) {
	bootstrap := request.GetBootstrapCapacity()
	if bootstrap == nil {
		return nil, nil //nolint:nilnil // No bootstrap is a valid legacy create request.
	}

	tikvSpecs, err := s.regionalCli.ListNodeSpecs(ctx, cloudRegion, &clusterngv1beta.ListNodeSpecsRequest{
		ComponentType: clusterngv1beta.ComponentType_TIKV,
		TenantId:      strconv.FormatUint(request.GetResourcePool().GetOwnerId(), 10),
	})
	if err != nil {
		return nil, err
	}
	if err := validateBootstrapComponent(
		bootstrap.GetTikv(),
		tikvSpecs,
		[]string{bootstrapStorageData},
		[]string{bootstrapStorageData, bootstrapStorageRaftlog},
	); err != nil {
		return nil, err
	}

	if bootstrap.GetTiflash() != nil {
		computeSpecs, err := s.regionalCli.ListNodeSpecs(ctx, cloudRegion, &clusterngv1beta.ListNodeSpecsRequest{
			ComponentType: clusterngv1beta.ComponentType_TIFLASH_COMPUTE,
			TenantId:      strconv.FormatUint(request.GetResourcePool().GetOwnerId(), 10),
		})
		if err != nil {
			return nil, err
		}
		if err := validateBootstrapComponent(
			bootstrap.GetTiflash(),
			computeSpecs,
			[]string{bootstrapStorageData},
			[]string{bootstrapStorageData},
		); err != nil {
			return nil, err
		}
	}

	applyBootstrapStorageSizes(request.GetDemand(), bootstrap)

	return &bootstrapPreflightResult{
		capacity: bootstrapCapacityForResponse(bootstrap),
	}, nil
}

func validateBootstrapComponent(
	component *globalngv1.ComponentCapacity,
	discovered *clusterngv1beta.ListNodeSpecsResponse,
	publicStorages []string,
	requiredRegionalStorages []string,
) bizerr.BizError {
	if component == nil || component.GetNodeSpec() == nil || component.NodeCount == nil {
		return bizerr.New(bizerr.ErrCodeParams, "bootstrap component node_spec and node_count are required")
	}
	if component.GetNodeSpec().GetCpu() <= 0 || component.GetNodeSpec().GetCpu() > math.MaxUint32 ||
		component.GetNodeSpec().GetMemoryGi() <= 0 || component.GetNodeSpec().GetMemoryGi() > math.MaxUint32 {
		return bizerr.New(bizerr.ErrCodeParams, "bootstrap component node spec is out of range")
	}
	if component.GetNodeCount() <= 0 || component.GetNodeCount() > math.MaxUint32 {
		return bizerr.New(bizerr.ErrCodeParams, "bootstrap component node count is out of range")
	}

	matchingOptions := make([]*clusterngv1beta.NodeSpecOptions, 0, 1)
	for _, candidate := range discovered.GetNodeSpecOptions() {
		nodeSpec := candidate.GetNodeSpec()
		if int64(nodeSpec.GetCpu()) == component.GetNodeSpec().GetCpu() &&
			int64(nodeSpec.GetMemoryGi()) == component.GetNodeSpec().GetMemoryGi() {
			matchingOptions = append(matchingOptions, candidate)
		}
	}
	if len(matchingOptions) != 1 {
		return failedBootstrapPrecondition("bootstrap component node spec must match exactly one regional node spec")
	}
	complete, err := hasCompleteBootstrapCloudStorageOptions(
		matchingOptions[0].GetStorageOptions(),
		requiredRegionalStorages,
	)
	if err != nil {
		return err
	}
	if !complete {
		return failedBootstrapPrecondition("bootstrap component node spec is missing required cloud storage options")
	}
	if err := validateBootstrapNodeCount(component.GetNodeCount(), discovered.GetNodeCountOptions()); err != nil {
		return err
	}
	return normalizeBootstrapStorages(component, publicStorages)
}

func hasCompleteBootstrapCloudStorageOptions(
	options []*clusterngv1beta.StorageSizeOptions,
	requiredNames []string,
) (bool, bizerr.BizError) {
	seen := make(map[string]struct{}, len(requiredNames))
	for _, option := range options {
		if option == nil {
			return false, invalidBootstrapStorageCatalog("storage option is missing")
		}
		if option.GetMedium() != clusterngv1beta.StorageMedium_STORAGE_MEDIUM_CLOUD {
			continue
		}

		name, recognized := bootstrapStorageName(option.GetName())
		if !recognized || !slices.Contains(requiredNames, name) {
			continue
		}
		if _, exists := seen[name]; exists {
			return false, invalidBootstrapStorageCatalog("cloud storage option %q is duplicated", name)
		}
		minimum := option.GetMinSizeGi()
		defaultSize := option.GetDefaultSizeGi()
		maximum := option.GetMaxSizeGi()
		if minimum == 0 || defaultSize < minimum || maximum < defaultSize {
			return false, invalidBootstrapStorageCatalog(
				"cloud storage option %q has inconsistent size bounds",
				name,
			)
		}
		seen[name] = struct{}{}
	}
	for _, name := range requiredNames {
		if _, exists := seen[name]; !exists {
			return false, nil
		}
	}
	return true, nil
}

func bootstrapStorageName(name clusterngv1beta.Component_Storage_Name) (string, bool) {
	switch name {
	case clusterngv1beta.Component_Storage_DATA:
		return bootstrapStorageData, true
	case clusterngv1beta.Component_Storage_RAFTLOG:
		return bootstrapStorageRaftlog, true
	default:
		return "", false
	}
}

func invalidBootstrapStorageCatalog(format string, args ...any) bizerr.BizError {
	return bizerr.New(
		bizerr.ErrCode(codes.Unavailable),
		"invalid regional node spec response: "+format,
		args...,
	)
}

func validateBootstrapNodeCount(count int64, options *clusterngv1beta.NodeCountOptions) bizerr.BizError {
	if options == nil || options.GetRule() == nil {
		return failedBootstrapPrecondition("regional node count options are missing")
	}
	if count < int64(options.GetMinNodeCount()) || count > int64(options.GetMaxNodeCount()) {
		return failedBootstrapPrecondition("bootstrap component node count is outside the regional range")
	}

	rule := options.GetRule()
	switch rule.GetType() {
	case clusterngv1beta.NodeCountRuleType_NODE_COUNT_RULE_TYPE_NONE:
		return nil
	case clusterngv1beta.NodeCountRuleType_NODE_COUNT_RULE_TYPE_MULTIPLE_OF:
		if rule.GetValue() == 0 || count%int64(rule.GetValue()) != 0 {
			return failedBootstrapPrecondition("bootstrap component node count violates the regional multiple-of rule")
		}
		return nil
	case clusterngv1beta.NodeCountRuleType_NODE_COUNT_RULE_TYPE_FIXED:
		if rule.GetValue() == 0 || count != int64(rule.GetValue()) {
			return failedBootstrapPrecondition("bootstrap component node count violates the regional fixed rule")
		}
		return nil
	default:
		return failedBootstrapPrecondition("regional node count rule is unsupported")
	}
}

func normalizeBootstrapStorages(
	component *globalngv1.ComponentCapacity,
	requiredNames []string,
) bizerr.BizError {
	storages := make(map[string]*globalngv1.Storage, len(component.GetStorages()))
	for _, storage := range component.GetStorages() {
		if storage == nil {
			return bizerr.New(bizerr.ErrCodeParams, "bootstrap component storage is required")
		}
		name := strings.ToLower(strings.TrimSpace(storage.GetName()))
		if _, exists := storages[name]; exists {
			return failedBootstrapPrecondition("bootstrap component storage name %q is duplicated", name)
		}
		if storage.GetSizeGi() <= 0 || storage.GetSizeGi() > math.MaxUint32 {
			return bizerr.New(bizerr.ErrCodeParams, "bootstrap component storage %q size is out of range", name)
		}
		storages[name] = storage
	}

	normalized := make([]*globalngv1.Storage, 0, len(requiredNames))
	for _, name := range requiredNames {
		storage, ok := storages[name]
		if !ok {
			return failedBootstrapPrecondition("bootstrap component storage %q is required", name)
		}
		storage.Name = name
		normalized = append(normalized, storage)
		delete(storages, name)
	}
	if len(storages) != 0 {
		return failedBootstrapPrecondition("bootstrap component contains an unknown storage")
	}
	component.Storages = normalized
	return nil
}

func failedBootstrapPrecondition(format string, args ...any) bizerr.BizError {
	return bizerr.New(bizerr.ErrCode(codes.FailedPrecondition), format, args...)
}

// parseCanonicalIPv4Prefix remains shared with focused compatibility tests;
// create-time CIDR validation itself is owned by cidr.go.
func parseCanonicalIPv4Prefix(value, fieldName string) (netip.Prefix, bizerr.BizError) {
	trimmed := strings.TrimSpace(value)
	prefix, err := netip.ParsePrefix(trimmed)
	if err != nil || !prefix.Addr().Is4() || prefix != prefix.Masked() {
		return netip.Prefix{}, bizerr.New(bizerr.ErrCodeParams, "%s must be a canonical IPv4 CIDR", fieldName)
	}
	return prefix, nil
}

func validateBootstrapNetworkCIDRs(resourcePoolCIDR, o11yClusterCIDR string) (string, bizerr.BizError) {
	poolCIDR, err := parseCanonicalIPv4Prefix(resourcePoolCIDR, "resource pool CIDR")
	if err != nil {
		return "", err
	}
	o11yCIDR, o11yErr := parseCanonicalIPv4Prefix(o11yClusterCIDR, "O11Y cluster CIDR")
	if o11yErr != nil {
		return "", bizerr.New(bizerr.ErrCode(codes.FailedPrecondition), "%s", o11yErr.ErrMsg())
	}
	if poolCIDR.Overlaps(o11yCIDR) {
		return "", failedBootstrapPrecondition("resource pool CIDR overlaps O11Y cluster CIDR")
	}
	return poolCIDR.String(), nil
}

func applyBootstrapStorageSizes(
	demand *globalngv1.CreateResourcePoolRequest_Demand,
	bootstrap *globalngv1.ResourcePoolBootstrapCapacity,
) {
	if demand == nil || bootstrap == nil {
		return
	}
	for _, storage := range bootstrap.GetTikv().GetStorages() {
		if storage.GetName() == bootstrapStorageData {
			demand.TikvData.DiskSizeGi = uint32(storage.GetSizeGi())
		}
	}
	for _, storage := range bootstrap.GetTiflash().GetStorages() {
		if storage.GetName() == bootstrapStorageData {
			demand.TiflashCn.DiskSizeGi = uint32(storage.GetSizeGi())
		}
	}
}

func bootstrapCapacityForResponse(
	bootstrap *globalngv1.ResourcePoolBootstrapCapacity,
) *globalngv1.ResourcePoolCapacity {
	if bootstrap == nil {
		return nil
	}
	return &globalngv1.ResourcePoolCapacity{
		Tikv:    cloneComponentCapacity(bootstrap.GetTikv()),
		Tiflash: cloneComponentCapacity(bootstrap.GetTiflash()),
	}
}

func cloneComponentCapacity(component *globalngv1.ComponentCapacity) *globalngv1.ComponentCapacity {
	if component == nil {
		return nil
	}
	cloned, ok := proto.Clone(component).(*globalngv1.ComponentCapacity)
	if !ok {
		return nil
	}
	return cloned
}

func bootstrapCapacityToRegional(
	bootstrap *globalngv1.ResourcePoolBootstrapCapacity,
) *clusterngv1beta.SharedPoolBootstrapCapacity {
	if bootstrap == nil {
		return nil
	}
	return &clusterngv1beta.SharedPoolBootstrapCapacity{
		Tikv:           bootstrapComponentToRegional(bootstrap.GetTikv()),
		TiflashCompute: bootstrapComponentToRegional(bootstrap.GetTiflash()),
	}
}

func bootstrapComponentToRegional(
	component *globalngv1.ComponentCapacity,
) *clusterngv1beta.BootstrapComponentCapacity {
	if component == nil {
		return nil
	}
	return &clusterngv1beta.BootstrapComponentCapacity{
		NodeSpec: &clusterngv1beta.NodeSpec{
			Cpu:      uint32(component.GetNodeSpec().GetCpu()),
			MemoryGi: uint32(component.GetNodeSpec().GetMemoryGi()),
		},
		NodeCount: uint32(component.GetNodeCount()),
	}
}
