package cluster

import (
	"context"
	"fmt"
	"net"
	"strings"

	"github.com/pingcap/dbaas/central/config"
	"github.com/pingcap/dbaas/central/db/entity"
	"github.com/pingcap/dbaas/pkg/constants"
	"github.com/pingcap/errcode"
	"github.com/tidbcloud/pkgv2/log"
	"go.uber.org/zap"
)

func isPrivateCIDR(ctx context.Context, ipNet *net.IPNet) bool {
	var privateIPBlocks []*net.IPNet
	ip := ipNet.IP

	// https://en.wikipedia.org/wiki/Private_network
	for _, cidr := range []string{
		//"127.0.0.0/8",    // IPv4 loopback
		"10.0.0.0/8",     // RFC1918
		"172.16.0.0/12",  // RFC1918
		"192.168.0.0/16", // RFC1918
		//"169.254.0.0/16", // RFC3927 link-local
		//"::1/128",        // IPv6 loopback
		//"fe80::/10",      // IPv6 link-local
		//"fc00::/7",       // IPv6 unique local addr
	} {
		_, block, err := net.ParseCIDR(cidr)
		if err != nil {
			log.Info(ctx, "failed to parse private ip blocks", zap.Error(err))
			continue
		}
		privateIPBlocks = append(privateIPBlocks, block)
	}

	if ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() {
		return false
	}

	for _, block := range privateIPBlocks {
		if block.Contains(ip) {
			return true
		}
	}
	return false
}

func isValidProjectCIDR(ctx context.Context, CIDR string, provider entity.Cloud) error {
	logger := log.WithContextL(ctx)

	// for now, we're limiting the Project CIDR to these two value, after infra team remove
	// the restriction, just remove this
	limitedProjectCIDR := config.Global().ServerConfig.LimitedProjectCIDR
	if len(limitedProjectCIDR) == 0 {
		logger.Info("limitedProjectCIDR not set, ignore this step")
	} else {
		found := false
		for _, i := range limitedProjectCIDR {
			if CIDR == i {
				found = true
			}
		}

		if !found {
			logger.Error("given CIDR not in limitedProjectCIDR", zap.String("CIDR", CIDR))
			return errcode.NewInvalidInputErr(
				fmt.Errorf("Please use one of the following network addresses: %s.", strings.Join(limitedProjectCIDR, ", ")), //nolint
			)
		}
	}

	_, ipNet, err := net.ParseCIDR(CIDR)
	if err != nil {
		logger.Error("failed to parse CIDR", zap.String("CIDR", CIDR), zap.Error(err))
		return errcode.NewInvalidInputErr(fmt.Errorf("This is neither a valid IP address nor CIDR notation.")) //nolint
	}

	if ipNet.String() != CIDR {
		return errcode.NewInvalidInputErr(fmt.Errorf("Invalid IP address range. Use CIDR notation and enter the lowest IP address in the subnet.")) //nolint
	}

	if !isPrivateCIDR(ctx, ipNet) {
		logger.Error("user typed a non-private ip block", zap.String("CIDR", CIDR))
		return errcode.NewInvalidInputErr(fmt.Errorf("CIDR block must be a private network address.")) //nolint
	}

	ones, _ := ipNet.Mask.Size()
	switch provider {
	case entity.Aws:
		if !(ones >= constants.MinAWSProjectCIDRMaskBits && ones <= constants.MaxProjectCIDRMaskBits) {
			errTemplate := "The AWS CIDR block must be at least /%d and at most /%d."
			logger.Error("net mask is invalid", zap.String("CIDR", CIDR), zap.Int("ones", ones), zap.Error(err))
			return errcode.NewInvalidInputErr(fmt.Errorf(
				errTemplate, constants.MaxProjectCIDRMaskBits, constants.MinAWSProjectCIDRMaskBits,
			))
		}
	case entity.Gcp:
		if !(ones >= constants.MinGCPProjectCIDRMaskBits && ones <= constants.MaxProjectCIDRMaskBits) {
			logger.Error("net mask is invalid", zap.String("CIDR", CIDR), zap.Int("ones", ones), zap.Error(err))
			errTemplate := "The GCP CIDR block must be at least /%d and at most /%d."
			return errcode.NewInvalidInputErr(fmt.Errorf(
				errTemplate,
				constants.MaxProjectCIDRMaskBits, constants.MinGCPProjectCIDRMaskBits,
			))
		}
	}

	return nil
}
