package resourcepool

import (
	"cmp"
	"context"
	"encoding/binary"
	"errors"
	"fmt"
	"net/netip"
	"slices"
	"strings"

	clusterngv1beta "github.com/tidbcloud/cluster-service-ng/api/cluster-service-ng/v1beta"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"google.golang.org/grpc/codes"
	"google.golang.org/protobuf/proto"

	globalcommonv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/common/v1"
	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/nextgen-global-service/domain/repository/region"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/pkg/config"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
)

const defaultNonAWSRecommendationPrefixLength = 16

var privateCIDRPrefixes = []netip.Prefix{
	netip.MustParsePrefix("10.0.0.0/8"),
	netip.MustParsePrefix("172.16.0.0/12"),
	netip.MustParsePrefix("192.168.0.0/16"),
}

type cidrInterval struct {
	start uint32
	end   uint32
}

type parsedCIDR struct {
	prefix    netip.Prefix
	canonical string
	interval  cidrInterval
}

type cidrValidationResult struct {
	responseCIDR       string
	canonicalCIDR      string
	o11yClusterCIDR    string
	findings           []*globalngv1.ResourcePoolCidrValidationFinding
	supportedNodeCount *int64
}

// CIDRValidationFailure is preserved across the Global Create boundary so the
// public API can return all deterministic validation findings.
type CIDRValidationFailure struct {
	bizerr.BizError

	Findings []*globalngv1.ResourcePoolCidrValidationFinding
}

func newCIDRValidationFailure(findings []*globalngv1.ResourcePoolCidrValidationFinding) *CIDRValidationFailure {
	return &CIDRValidationFailure{
		BizError: bizerr.New(bizerr.ErrCodeParams, "resource pool VPC CIDR is invalid"),
		Findings: findings,
	}
}

func (s *serviceImpl) PlanResourcePoolCidr(
	ctx context.Context,
	request *globalngv1.PlanResourcePoolCidrRequest,
) (*globalngv1.PlanResourcePoolCidrResponse, bizerr.BizError) {
	ctx, span := tracer.Start(ctx, "Domain.PlanResourcePoolCidr")
	defer span.End()

	highAvailabilityType, err := normalizeResourcePoolCIDRHighAvailability(request.GetHighAvailabilityType())
	if err != nil {
		return nil, err
	}
	cloudRegion, err := s.selectResourcePoolCIDRRegion(ctx, request.GetOrganizationId(), request.GetCloudProviderId(), request.GetRegionId())
	if err != nil {
		return nil, err
	}
	exclusions, intervals, _, err := s.loadResourcePoolCIDRExclusions(ctx, request.GetOrganizationId(), cloudRegion)
	if err != nil {
		return nil, err
	}

	response := &globalngv1.PlanResourcePoolCidrResponse{
		CloudProviderId: cloudRegion.GetCloudProviderId(),
		RegionId:        cloudRegion.GetId(),
		Policy:          s.resourcePoolCIDRPolicy(cloudRegion.GetCloudProviderId()),
		Exclusions:      exclusions,
	}
	recommendation, available, err := s.recommendResourcePoolCIDR(ctx, cloudRegion, intervals, highAvailabilityType)
	if err != nil {
		return nil, err
	}
	if !available {
		response.RecommendationStatus = globalngv1.PlanResourcePoolCidrResponse_EXHAUSTED
		return response, nil
	}
	response.RecommendationStatus = globalngv1.PlanResourcePoolCidrResponse_AVAILABLE
	response.Recommendation = recommendation
	return response, nil
}

func (s *serviceImpl) ValidateResourcePoolCidr(
	ctx context.Context,
	request *globalngv1.ValidateResourcePoolCidrRequest,
) (*globalngv1.ValidateResourcePoolCidrResponse, bizerr.BizError) {
	ctx, span := tracer.Start(ctx, "Domain.ValidateResourcePoolCidr")
	defer span.End()

	if request == nil || strings.TrimSpace(request.GetCidr()) == "" {
		return nil, bizerr.New(bizerr.ErrCodeParams, "cidr is required")
	}
	highAvailabilityType, err := normalizeResourcePoolCIDRHighAvailability(request.GetHighAvailabilityType())
	if err != nil {
		return nil, err
	}
	cloudRegion, err := s.selectResourcePoolCIDRRegion(ctx, request.GetOrganizationId(), request.GetCloudProviderId(), request.GetRegionId())
	if err != nil {
		return nil, err
	}
	result, err := s.validateResourcePoolCIDR(
		ctx,
		request.GetOrganizationId(),
		cloudRegion,
		request.GetCidr(),
		highAvailabilityType,
	)
	if err != nil {
		return nil, err
	}
	return &globalngv1.ValidateResourcePoolCidrResponse{
		CloudProviderId:    cloudRegion.GetCloudProviderId(),
		RegionId:           cloudRegion.GetId(),
		Cidr:               result.responseCIDR,
		SupportedNodeCount: result.supportedNodeCount,
		BlockingFindings:   result.findings,
	}, nil
}

func normalizeResourcePoolCIDRHighAvailability(
	highAvailabilityType globalngv1.ResourcePool_HighAvailabilityType,
) (globalngv1.ResourcePool_HighAvailabilityType, bizerr.BizError) {
	switch highAvailabilityType {
	case globalngv1.ResourcePool_HIGH_AVAILABILITY_TYPE_UNSPECIFIED:
		return globalngv1.ResourcePool_REGIONAL, nil
	case globalngv1.ResourcePool_REGIONAL, globalngv1.ResourcePool_ZONAL:
		return highAvailabilityType, nil
	default:
		return 0, bizerr.New(bizerr.ErrCodeParams, "high availability type is invalid")
	}
}

func (s *serviceImpl) selectResourcePoolCIDRRegion(
	ctx context.Context,
	organizationID uint64,
	providerID globalcommonv1.CloudProvider_Name,
	regionID globalcommonv1.Region_Name,
) (*entity.Region, bizerr.BizError) {
	if organizationID == 0 || providerID == globalcommonv1.CloudProvider_NAME_UNSPECIFIED || regionID == globalcommonv1.Region_NAME_UNSPECIFIED {
		return nil, bizerr.New(bizerr.ErrCodeParams, "organization, cloud provider, and region are required")
	}
	cloudRegion, err := s.regionRepo.SelectRegion(ctx, region.Filter{
		CloudProvider: providerID,
		RegionId:      regionID,
		TenantId:      organizationID,
		ServicePlan:   globalcommonv1.ServicePlan_BYOC,
	})
	if err != nil {
		return nil, err
	}
	if cloudRegion == nil {
		return nil, bizerr.New(bizerr.ErrCodeRecordNotFound, "region not found")
	}
	return cloudRegion, nil
}

func (s *serviceImpl) validateResourcePoolCIDR(
	ctx context.Context,
	organizationID uint64,
	cloudRegion *entity.Region,
	cidr string,
	ha globalngv1.ResourcePool_HighAvailabilityType,
) (*cidrValidationResult, bizerr.BizError) {
	result := &cidrValidationResult{responseCIDR: strings.TrimSpace(cidr)}
	parsed, parseErr := parseCanonicalIPv4CIDR(result.responseCIDR)
	if parseErr != nil {
		result.findings = append(result.findings, cidrFinding(
			globalngv1.ResourcePoolCidrValidationFinding_INVALID_CIDR,
			"The CIDR must be a canonical IPv4 network.",
			nil,
		))
		return result, nil
	}
	result.canonicalCIDR = parsed.canonical
	result.responseCIDR = parsed.canonical
	if !isPrivateCIDR(parsed) {
		result.findings = append(result.findings, cidrFinding(
			globalngv1.ResourcePoolCidrValidationFinding_CIDR_NOT_PRIVATE,
			"The CIDR must be fully contained within a private IPv4 address range.",
			nil,
		))
		return result, nil
	}

	exclusions, _, o11yCIDR, err := s.loadResourcePoolCIDRExclusions(ctx, organizationID, cloudRegion)
	if err != nil {
		return nil, err
	}
	result.o11yClusterCIDR = o11yCIDR
	for _, exclusion := range exclusions {
		exclusionCIDR, parseErr := parseCanonicalIPv4CIDR(exclusion.GetCidr())
		if parseErr != nil {
			return nil, bizerr.New(bizerr.ErrCode(codes.Internal), "stored CIDR exclusion is invalid")
		}
		if !intervalsOverlap(parsed.interval, exclusionCIDR.interval) {
			continue
		}
		switch exclusion.GetType() {
		case globalngv1.ResourcePoolCidrExclusion_RESOURCE_POOL:
			result.findings = append(result.findings, cidrFinding(
				globalngv1.ResourcePoolCidrValidationFinding_CIDR_OVERLAPS_RESOURCE_POOL,
				"The CIDR overlaps an existing resource pool.",
				exclusion,
			))
		case globalngv1.ResourcePoolCidrExclusion_O11Y_CLUSTER:
			result.findings = append(result.findings, cidrFinding(
				globalngv1.ResourcePoolCidrValidationFinding_CIDR_OVERLAPS_O11Y,
				"The CIDR overlaps the regional observability network.",
				exclusion,
			))
		case globalngv1.ResourcePoolCidrExclusion_TIDB_CLOUD_RESERVED:
			result.findings = append(result.findings, cidrFinding(
				globalngv1.ResourcePoolCidrValidationFinding_CIDR_OVERLAPS_RESERVED_NETWORK,
				"The CIDR overlaps a TiDB Cloud reserved network.",
				exclusion,
			))
		default:
			return nil, bizerr.New(bizerr.ErrCode(codes.Internal), "unknown CIDR exclusion type")
		}
	}

	supported, count, err := s.evaluateResourcePoolNetworkCapacity(ctx, cloudRegion, parsed.prefix.Bits(), ha)
	if err != nil {
		return nil, err
	}
	if !supported {
		result.findings = append(result.findings, cidrFinding(
			globalngv1.ResourcePoolCidrValidationFinding_CIDR_PREFIX_UNSUPPORTED,
			"The CIDR prefix is not supported by the selected region.",
			nil,
		))
		return result, nil
	}
	result.supportedNodeCount = proto.Int64(count)
	return result, nil
}

func (s *serviceImpl) loadResourcePoolCIDRExclusions(
	ctx context.Context,
	organizationID uint64,
	cloudRegion *entity.Region,
) ([]*globalngv1.ResourcePoolCidrExclusion, []cidrInterval, string, bizerr.BizError) {
	pools, err := s.resourcePoolRepo.ListLiveBYOCOrganizationResourcePoolsInRegion(
		ctx,
		organizationID,
		cloudRegion.GetCloudProviderId(),
		cloudRegion.GetId(),
	)
	if err != nil {
		return nil, nil, "", err
	}
	reservedCIDRs := []string(nil)
	if s.cfg != nil {
		reservedCIDRs = s.cfg.ResourcePoolNetwork.ReservedCIDRs
	}
	exclusions := make([]*globalngv1.ResourcePoolCidrExclusion, 0, len(pools)+1+len(reservedCIDRs))
	for _, pool := range pools {
		parsed, parseErr := parseCanonicalIPv4CIDR(pool.GetCidr())
		if parseErr != nil {
			return nil, nil, "", bizerr.New(bizerr.ErrCode(codes.Internal), "live resource pool %d has invalid CIDR", pool.GetId())
		}
		exclusions = append(exclusions, &globalngv1.ResourcePoolCidrExclusion{
			Type:                    globalngv1.ResourcePoolCidrExclusion_RESOURCE_POOL,
			Cidr:                    parsed.canonical,
			ResourcePoolId:          proto.Uint64(pool.GetId()),
			ResourcePoolDisplayName: proto.String(pool.GetName()),
			RegionId:                enumPointer(pool.GetRegionId()),
			CloudProviderId:         enumPointer(pool.GetProviderId()),
		})
	}

	configuration, err := s.regionalCli.GetSharedPoolProvisionConfiguration(
		ctx,
		cloudRegion,
		&clusterngv1beta.GetSharedPoolProvisionConfigurationRequest{},
	)
	if err != nil {
		return nil, nil, "", normalizeNetworkEvaluationError(err)
	}
	o11y, parseErr := parseCanonicalIPv4CIDR(configuration.GetO11YClusterCidr())
	if parseErr != nil {
		return nil, nil, "", bizerr.New(bizerr.ErrCode(codes.Internal), "regional O11Y CIDR is missing or invalid")
	}
	exclusions = append(exclusions, &globalngv1.ResourcePoolCidrExclusion{
		Type: globalngv1.ResourcePoolCidrExclusion_O11Y_CLUSTER,
		Cidr: o11y.canonical,
	})

	seenReserved := make(map[string]struct{}, len(reservedCIDRs))
	for _, reservedCIDR := range reservedCIDRs {
		if _, ok := seenReserved[reservedCIDR]; ok {
			continue
		}
		parsed, parseErr := parseCanonicalIPv4CIDR(reservedCIDR)
		if parseErr != nil || !isPrivateCIDR(parsed) {
			return nil, nil, "", bizerr.New(bizerr.ErrCode(codes.Internal), "configured reserved CIDR is invalid")
		}
		seenReserved[reservedCIDR] = struct{}{}
		exclusions = append(exclusions, &globalngv1.ResourcePoolCidrExclusion{
			Type: globalngv1.ResourcePoolCidrExclusion_TIDB_CLOUD_RESERVED,
			Cidr: parsed.canonical,
		})
	}

	slices.SortStableFunc(exclusions, func(leftExclusion, rightExclusion *globalngv1.ResourcePoolCidrExclusion) int {
		if result := cmp.Compare(leftExclusion.GetType(), rightExclusion.GetType()); result != 0 {
			return result
		}
		left, _ := parseCanonicalIPv4CIDR(leftExclusion.GetCidr())
		right, _ := parseCanonicalIPv4CIDR(rightExclusion.GetCidr())
		if result := cmp.Compare(left.interval.start, right.interval.start); result != 0 {
			return result
		}
		if result := cmp.Compare(left.interval.end, right.interval.end); result != 0 {
			return result
		}
		return cmp.Compare(leftExclusion.GetResourcePoolId(), rightExclusion.GetResourcePoolId())
	})
	intervals := make([]cidrInterval, 0, len(exclusions))
	for _, exclusion := range exclusions {
		parsed, _ := parseCanonicalIPv4CIDR(exclusion.GetCidr())
		intervals = append(intervals, parsed.interval)
	}
	return exclusions, mergeCIDRIntervals(intervals), o11y.canonical, nil
}

func (s *serviceImpl) evaluateResourcePoolNetworkCapacity(
	ctx context.Context,
	cloudRegion *entity.Region,
	prefixLength int,
	ha globalngv1.ResourcePool_HighAvailabilityType,
) (bool, int64, bizerr.BizError) {
	ctx, span := tracer.Start(ctx, "Domain.EvaluateResourcePoolNetworkCapacity")
	defer span.End()
	provider := cloudRegion.GetCloudProviderId().String()
	haLabel := ha.String()

	regionalHA := convertResourcePoolHAType(ha)
	if regionalHA == clusterngv1beta.CreateSharedPoolRequest_InitSharedPoolConfig_HIGH_AVAILABILITY_TYPE_UNSPECIFIED {
		return false, 0, bizerr.New(bizerr.ErrCodeParams, "high availability type is invalid")
	}
	response, err := s.regionalCli.EvaluateSharedPoolNetworkCapacity(
		ctx,
		cloudRegion,
		&clusterngv1beta.EvaluateSharedPoolNetworkCapacityRequest{
			CidrPrefixLength:     proto.Int32(int32(prefixLength)),
			HighAvailabilityType: regionalHA,
		},
	)
	if err != nil {
		tilog.Error(ctx, "resource pool network capacity evaluation failed",
			zap.String("reason", "REGIONAL_NETWORK_VALIDATION_UNAVAILABLE"),
			zap.String("provider", provider),
			zap.Int("prefix_length", prefixLength),
			zap.String("high_availability_type", haLabel),
			zap.Error(err),
		)
		return false, 0, normalizeNetworkEvaluationError(err)
	}
	if response == nil || (response.GetSupported() && response.GetSupportedNodeCount() <= 0) ||
		(!response.GetSupported() && response.GetSupportedNodeCount() != 0) {
		tilog.Error(ctx, "resource pool network capacity response is inconsistent",
			zap.String("provider", provider),
			zap.Int("prefix_length", prefixLength),
			zap.String("high_availability_type", haLabel),
		)
		return false, 0, bizerr.New(bizerr.ErrCode(codes.Internal), "regional network capacity response is inconsistent")
	}
	return response.GetSupported(), response.GetSupportedNodeCount(), nil
}

func normalizeNetworkEvaluationError(err bizerr.BizError) bizerr.BizError {
	if err == nil {
		return nil
	}
	switch err.ErrCode() {
	case bizerr.ErrCode(codes.Canceled):
		return err
	case bizerr.ErrCode(codes.Internal), bizerr.ErrCode(codes.InvalidArgument):
		return bizerr.New(bizerr.ErrCode(codes.Internal), "regional network capacity evaluation failed")
	default:
		return bizerr.New(bizerr.ErrCode(codes.Unavailable), "regional network validation is unavailable")
	}
}

func (s *serviceImpl) resourcePoolCIDRPolicy(
	providerID globalcommonv1.CloudProvider_Name,
) *globalngv1.ResourcePoolCidrPolicy {
	policy := &globalngv1.ResourcePoolCidrPolicy{
		SupportedPrivateCidrs: []string{"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"},
	}
	if providerID == globalcommonv1.CloudProvider_AWS {
		awsMinPrefixLength, awsMaxPrefixLength := s.awsCIDRPrefixLengthRange()
		policy.AllowedPrefixRanges = []*globalngv1.ResourcePoolCidrPrefixRange{{
			MinPrefixLength: int32(awsMinPrefixLength),
			MaxPrefixLength: int32(awsMaxPrefixLength),
		}}
	}
	return policy
}

func (s *serviceImpl) awsCIDRPrefixLengthRange() (awsMinPrefixLength, awsMaxPrefixLength int) {
	networkConfig := config.ResourcePoolNetworkConfig{}
	if s.cfg != nil {
		networkConfig = s.cfg.ResourcePoolNetwork
	}
	return networkConfig.AWSPrefixLengthRange()
}

func (s *serviceImpl) recommendationPrefixLengths(providerID globalcommonv1.CloudProvider_Name) []int {
	if providerID == globalcommonv1.CloudProvider_AWS {
		awsMinPrefixLength, awsMaxPrefixLength := s.awsCIDRPrefixLengthRange()
		startPrefixLength := awsMinPrefixLength
		if s.cfg != nil && s.cfg.ResourcePoolNetwork.AWSRecommendationStartPrefixLength != 0 {
			startPrefixLength = s.cfg.ResourcePoolNetwork.AWSRecommendationStartPrefixLength
		}

		prefixLengths := make([]int, 0, awsMaxPrefixLength-startPrefixLength+1)
		for prefixLength := startPrefixLength; prefixLength <= awsMaxPrefixLength; prefixLength++ {
			prefixLengths = append(prefixLengths, prefixLength)
		}
		return prefixLengths
	}
	return []int{defaultNonAWSRecommendationPrefixLength}
}

func (s *serviceImpl) recommendResourcePoolCIDR(
	ctx context.Context,
	cloudRegion *entity.Region,
	exclusions []cidrInterval,
	ha globalngv1.ResourcePool_HighAvailabilityType,
) (*globalngv1.ResourcePoolCidrRecommendation, bool, bizerr.BizError) {
	type capacityResult struct {
		supported bool
		count     int64
	}
	capacityByPrefix := make(map[int]capacityResult)
	for _, privateRange := range privateCIDRPrefixes {
		privateParsed, parseErr := parseCanonicalIPv4CIDR(privateRange.String())
		if parseErr != nil {
			return nil, false, bizerr.New(bizerr.ErrCode(codes.Internal), "private CIDR policy is invalid")
		}
		for _, prefixLength := range s.recommendationPrefixLengths(cloudRegion.GetCloudProviderId()) {
			candidate, ok := firstAlignedCIDRGap(privateParsed.interval, exclusions, prefixLength)
			if !ok {
				continue
			}
			capacity, evaluated := capacityByPrefix[prefixLength]
			if !evaluated {
				supported, count, err := s.evaluateResourcePoolNetworkCapacity(ctx, cloudRegion, prefixLength, ha)
				if err != nil {
					return nil, false, err
				}
				capacity = capacityResult{supported: supported, count: count}
				capacityByPrefix[prefixLength] = capacity
			}
			if !capacity.supported {
				continue
			}
			return &globalngv1.ResourcePoolCidrRecommendation{
				Cidr:               intervalStartToCIDR(candidate, prefixLength),
				SupportedNodeCount: capacity.count,
			}, true, nil
		}
	}
	return &globalngv1.ResourcePoolCidrRecommendation{}, false, nil
}

func parseCanonicalIPv4CIDR(input string) (parsedCIDR, error) {
	prefix, err := netip.ParsePrefix(strings.TrimSpace(input))
	if err != nil || !prefix.Addr().Is4() || prefix != prefix.Masked() {
		return parsedCIDR{}, errors.New("invalid canonical IPv4 CIDR")
	}
	startBytes := prefix.Addr().As4()
	start := binary.BigEndian.Uint32(startBytes[:])
	blockSize := uint64(1) << (32 - prefix.Bits())
	end := uint32(uint64(start) + blockSize - 1)
	return parsedCIDR{
		prefix:    prefix,
		canonical: prefix.String(),
		interval:  cidrInterval{start: start, end: end},
	}, nil
}

func isPrivateCIDR(parsed parsedCIDR) bool {
	for _, privatePrefix := range privateCIDRPrefixes {
		privateRange, _ := parseCanonicalIPv4CIDR(privatePrefix.String())
		if parsed.interval.start >= privateRange.interval.start && parsed.interval.end <= privateRange.interval.end {
			return true
		}
	}
	return false
}

func intervalsOverlap(left, right cidrInterval) bool {
	return left.start <= right.end && right.start <= left.end
}

func mergeCIDRIntervals(intervals []cidrInterval) []cidrInterval {
	if len(intervals) == 0 {
		return nil
	}
	slices.SortFunc(intervals, func(a, b cidrInterval) int {
		if a.start < b.start {
			return -1
		}
		if a.start > b.start {
			return 1
		}
		return 0
	})
	merged := make([]cidrInterval, 0, len(intervals))
	for _, current := range intervals {
		last := len(merged) - 1
		if last < 0 || uint64(current.start) > uint64(merged[last].end)+1 {
			merged = append(merged, current)
			continue
		}
		if current.end > merged[last].end {
			merged[last].end = current.end
		}
	}
	return merged
}

func firstAlignedCIDRGap(privateRange cidrInterval, exclusions []cidrInterval, prefixLength int) (uint32, bool) {
	blockSize := uint64(1) << (32 - prefixLength)
	candidate := uint64(privateRange.start)
	end := uint64(privateRange.end)
	for candidate+blockSize-1 <= end {
		advanced := false
		for _, exclusion := range exclusions {
			if uint64(exclusion.end) < candidate {
				continue
			}
			if uint64(exclusion.start) > candidate+blockSize-1 {
				break
			}
			candidate = alignIPv4Address(uint64(exclusion.end)+1, blockSize)
			advanced = true
			break
		}
		if !advanced {
			return uint32(candidate), true
		}
	}
	return 0, false
}

func alignIPv4Address(value, blockSize uint64) uint64 {
	return (value + blockSize - 1) &^ (blockSize - 1)
}

func intervalStartToCIDR(start uint32, prefixLength int) string {
	address := netip.AddrFrom4([4]byte{byte(start >> 24), byte(start >> 16), byte(start >> 8), byte(start)})
	return fmt.Sprintf("%s/%d", address, prefixLength)
}

func cidrFinding(
	code globalngv1.ResourcePoolCidrValidationFinding_Code,
	message string,
	exclusion *globalngv1.ResourcePoolCidrExclusion,
) *globalngv1.ResourcePoolCidrValidationFinding {
	return &globalngv1.ResourcePoolCidrValidationFinding{
		Code:                 code,
		Message:              message,
		ConflictingExclusion: exclusion,
	}
}

func enumPointer[T ~int32](value T) *T {
	return &value
}
