package networkcapacity

import (
	"fmt"

	"github.com/tidbcloud/infra-provider/regional-server/pkg/util"
)

const (
	// The initial BYOC resource-pool API contract accepts VPC prefixes from /16
	// through /23. This is a product boundary, not the full AWS VPC CIDR range.
	awsMinVPCPrefix = 16
	awsMaxVPCPrefix = 23

	// AWS reserves the first four and last IPv4 addresses in every subnet, and
	// does not allow an IPv4 subnet smaller than /28.
	awsMinSubnetPrefix          = 28
	awsReservedIPsPerNodeSubnet = 5

	// Each node needs one primary and one warm IP. Reserve the same amount again
	// so a rolling upgrade can run the old and replacement nodes concurrently.
	awsNodeIPAndUpgradeDivisor = 4
)

func evaluateAWS(input Input) (Result, error) {
	if input.PrefixLength < awsMinVPCPrefix || input.PrefixLength > awsMaxVPCPrefix {
		return unsupported(ResultCategoryPrefixRange), nil
	}

	var (
		layout util.AWSSubnetLayout
		err    error
	)
	switch input.HighAvailability {
	case HighAvailabilityRegional:
		layout, err = util.BuildAWSStandardSubnetLayout(input.PrefixLength, input.RegionZones)
	case HighAvailabilityZonal:
		if len(input.ConfiguredNodeZones) != 1 {
			return Result{}, fmt.Errorf("zonal evaluation requires exactly one configured node zone")
		}
		layout, err = util.BuildAWSOptimizedSubnetLayout(
			input.PrefixLength,
			input.ConfiguredNodeZones,
			input.RegionZones,
		)
	}
	if err != nil {
		return Result{}, err
	}
	if !layoutMeetsAWSMinimumSubnetSize(layout) {
		return unsupported(ResultCategorySubnetShape), nil
	}

	if input.EndpointSubnetsEnabled && input.HighAvailability == HighAvailabilityZonal {
		endpointLayout, endpointErr := util.BuildAWSEndpointSubnetLayout(
			input.PrefixLength,
			input.PrivateLinkEndpointZones,
		)
		if endpointErr != nil {
			return unsupported(ResultCategoryEndpointCapacity), nil
		}
		layout.EndpointSubnets = endpointLayout.EndpointSubnets
	}

	count, err := calculateAWSNodeCapacity(layout.NodeSubnets, input.ReservedAdminPodIPs)
	if err != nil {
		return Result{}, err
	}
	if count == 0 {
		return unsupported(ResultCategoryZeroNodeCapacity), nil
	}
	return supported(count), nil
}

func layoutMeetsAWSMinimumSubnetSize(layout util.AWSSubnetLayout) bool {
	groups := [][]util.AWSSubnetShape{
		layout.NodeSubnets,
		layout.PublicLBSubnets,
		layout.PrivateLBSubnets,
		layout.ControlPlanes,
	}
	for _, group := range groups {
		for _, subnet := range group {
			if subnet.PrefixLength > awsMinSubnetPrefix {
				return false
			}
		}
	}
	return true
}

func calculateAWSNodeCapacity(nodeSubnets []util.AWSSubnetShape, reservedAdminPodIPs int64) (int64, error) {
	if len(nodeSubnets) == 0 {
		return 0, fmt.Errorf("AWS layout has no node subnets")
	}
	var usable uint64
	for _, subnet := range nodeSubnets {
		if subnet.PrefixLength < 0 || subnet.PrefixLength > 32 {
			return 0, fmt.Errorf("invalid node subnet prefix /%d", subnet.PrefixLength)
		}
		addresses := uint64(1) << uint(32-subnet.PrefixLength)
		if addresses > awsReservedIPsPerNodeSubnet {
			usable += addresses - awsReservedIPsPerNodeSubnet
		}
	}
	reserve := uint64(reservedAdminPodIPs)
	if reserve >= usable {
		return 0, nil
	}
	return int64((usable - reserve) / awsNodeIPAndUpgradeDivisor), nil
}
