package vpc

import (
	"context"
	"encoding/binary"
	"fmt"
	"net"
	"strconv"
	"strings"

	regionaldedicatedv1 "github.com/tidbcloud/infra-provider/api/tidbcloud/regional-server/v1"
	"github.com/tidbcloud/tiinf/monitor/tilog"

	globalcommonv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/common/v1"
	"github.com/tidbcloud/tidb-management-service/dedicated-global-service/domain/adapter/account"
	"github.com/tidbcloud/tidb-management-service/dedicated-global-service/domain/adapter/regionserver"
	"github.com/tidbcloud/tidb-management-service/dedicated-global-service/domain/entity"
	cidrrepo "github.com/tidbcloud/tidb-management-service/dedicated-global-service/domain/repository/cidr"
	regionrepo "github.com/tidbcloud/tidb-management-service/dedicated-global-service/domain/repository/region"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
	"github.com/tidbcloud/tidb-management-service/utilities/featuregate"
)

var (
	// AwsValidCIDRRanges ... We recommend configuring an IP range size between /16 and /23. The following network addresses are supported:
	// 10.250.0.0 - 10.251.255.255
	// 172.16.0.0 - 172.31.255.255
	// 192.168.0.0 - 192.168.255.255
	AwsValidCIDRRanges = []string{
		"10.250.0.0/16",
		"10.251.0.0/16",
		"172.16.0.0/12",
		"192.168.0.0/16",
	}
	AwsCidrMaskMin = 16
	AwsCidrMaskMax = 23

	// GcpValidCIDRRanges ... We recommend configuring an IP range size between /19 and /20. The following network addresses are supported:
	// 10.250.0.0 - 10.251.255.255
	// 172.16.0.0 - 172.17.255.255
	// 172.30.0.0 - 172.31.255.255
	GcpValidCIDRRanges = []string{
		"10.140.224.0/20",
		"10.81.224.0/20",
		"10.0.0.0/16",
		"10.250.0.0/16",
		"10.251.0.0/16",
		"172.16.0.0/16",
		"172.17.0.0/16",
		"172.30.0.0/16",
		"172.31.0.0/16",
	}
	GcpCidrMaskAllowListMin = 16
	GcpCidrMaskMin          = 19
	GcpCidrMaskMax          = 20

	AzureValidCIDRRanges = []string{
		"10.250.0.0/16",
		"10.251.0.0/16",
		"172.16.0.0/16",
		"172.17.0.0/16",
		"172.18.0.0/16",
		"172.19.0.0/16",
		"172.20.0.0/16",
	}
	AzureCidrMaskMin = 16
	AzureCidrMaskMax = 20

	// CidrMask2MaxNodeIP ... See PRD: https://pingcap.feishu.cn/wiki/AbVQwLR2AiGe4dkHv8McGS8rnUv
	// GCP, See: https://pingcap.feishu.cn/wiki/LHjXw9SzpiIvRAkQpPpcD5ddngf#HGqBdfpVRoBIF7x1eLCcwpXHnCL
	// Azure: https://pingcap-cn.feishu.cn/wiki/VquvwlyGWiIHS1klLqmcNq0MnOr
	CidrMask2MaxNodeIP = map[globalcommonv1.CloudProvider_Name]map[int]uint32{
		globalcommonv1.CloudProvider_AWS: {
			16: 3065,
			17: 1529,
			18: 761,
			19: 377,
			20: 185,
			21: 89,
			22: 41,
			23: 17,
		},
		globalcommonv1.CloudProvider_GCP: {
			16: 1020,
			17: 508,
			18: 252,
			19: 124,
			20: 60,
		},
		globalcommonv1.CloudProvider_AZURE: {
			16: 1020,
			17: 508,
			18: 252,
			19: 124,
			20: 60,
		},
	}

	// DefaultTemplateCIDR ... used for generating region default CIDR for a project
	// NOTE: We need to adjust GcpValidCIDRRanges if region count > 16
	DefaultTemplateCIDR            = "172.16.0.0/16"
	RegionCIDRShiftBits            = 5
	RegionGCPAndAzureCIDRShiftBits = 3
)

// IsOverlapCidr ... check weather the cidr is overlap with the already exist ones
func IsOverlapCidr(
	ctx context.Context,
	cidrA, cidrB, regionB string,
	cidrType CidrType,
	cloudProvider string,
) bizerr.BizError {
	var (
		logger         = tilog.WithContextS(ctx)
		err            error
		ipNetA, ipNetB *net.IPNet
		upperCloud     string
	)
	upperCloud = strings.ToUpper(cloudProvider)
	_, ipNetA, err = net.ParseCIDR(cidrA)
	if err != nil {
		logger.Infof("parse cidr(%s) failed: %s", cidrA, err)
		return bizerr.New(bizerr.ErrCodeParams, "invalid CIDR address: %s", cidrA)
	}
	_, ipNetB, err = net.ParseCIDR(cidrB)
	if err != nil {
		logger.Infof("parse cidr(%s) failed: %s", cidrB, err)
		return bizerr.New(bizerr.ErrCodeParams, "invalid CIDR address: %s", cidrB)
	}
	// different overlap error message for different cidr type
	if OverlapCidrRange(ipNetA, ipNetB) {
		switch cidrType {
		case CidrTypeCurrent:
			return bizerr.New(
				bizerr.ErrCodeVPCPeeringConflictWithCurrentPeeringCidr,
				ErrMsgVPCPeeringConflictWithCurrentPeeringCidr,
				upperCloud,
			)
		default:
			return bizerr.New(
				bizerr.ErrCodeVPCPeeringConflictWithRegionCidr,
				ErrMsgVPCPeeringConflictWithRegionCidr,
				upperCloud,
				cidrB,
				regionB,
			)
		}
	}
	return nil
}

func OverlapCidrRange(cidrRangeA, cidrRangeB *net.IPNet) bool {
	return cidrRangeA.Contains(cidrRangeB.IP) || cidrRangeB.Contains(cidrRangeA.IP)
}

type CidrType string

const (
	CidrTypeCurrent = CidrType("currentcidr")
	CidrTypeRegion  = CidrType("regioncidr")
)

type Overlap struct {
	ProjectID uint64
	Regions   []*entity.BasicRegion
}

type ValidateOnlyResponse struct {
	Overlaps  []*Overlap
	MaxNodeIP uint32
}

type Helper interface {
	ValidateCIDR(
		cidr string,
		cloudProviderName globalcommonv1.CloudProvider_Name,
		tenantID uint64,
		projectID uint64,
	) (int, bizerr.BizError)
	CheckCidrState(ctx context.Context, cidr *entity.CIDR, project *account.ProjectInfo) bizerr.BizError
	BuildValidateOnlyResponse(
		ctx context.Context,
		cloudProviderName globalcommonv1.CloudProvider_Name,
		cidr string,
		cidrMask int,
		project *account.ProjectInfo,
		currentCIDR *entity.CIDR,
	) (*ValidateOnlyResponse, bizerr.BizError)
}

type networkHelperImp struct {
	cidrRepo        cidrrepo.Repo
	regionRepo      regionrepo.Repo
	featGateCli     featuregate.Service
	accountCli      account.Client
	regionServerCli regionserver.Client
}

func NewHelper(
	cidrRepo cidrrepo.Repo,
	regionRepo regionrepo.Repo,
	featGateCli featuregate.Service,
	accountCli account.Client,
	regionServerCli regionserver.Client,
) Helper {
	return &networkHelperImp{
		cidrRepo:        cidrRepo,
		regionRepo:      regionRepo,
		featGateCli:     featGateCli,
		accountCli:      accountCli,
		regionServerCli: regionServerCli,
	}
}

func (h *networkHelperImp) CheckCidrState(
	ctx context.Context,
	cidr *entity.CIDR,
	project *account.ProjectInfo,
) bizerr.BizError {
	region, err := h.regionRepo.SelectRegionById(ctx, cidr.BasicRegion.Name, project.TenantID)
	if err != nil {
		return err
	}

	vpcs, err := h.regionServerCli.ListVpcs(ctx, region,
		&regionaldedicatedv1.ListVpcsRequest{
			TenantId:  strconv.FormatUint(project.TenantID, 10),
			ProjectId: strconv.FormatUint(project.ID, 10),
			ClusterId: "", // leave it empty
			Statuses: []regionaldedicatedv1.Vpc_Status{
				regionaldedicatedv1.Vpc_ACTIVE,
				regionaldedicatedv1.Vpc_CREATING,
				regionaldedicatedv1.Vpc_FAILED,
			},
		},
	)
	if err != nil {
		return err
	}
	if len(vpcs.Vpcs) > 0 {
		return bizerr.New(bizerr.ErrCodeParams, "This Network container can not be deleted.")
	}

	return nil
}

func (h *networkHelperImp) BuildValidateOnlyResponse(
	ctx context.Context,
	cloudProviderName globalcommonv1.CloudProvider_Name,
	cidr string,
	cidrMask int,
	project *account.ProjectInfo,
	currentCIDR *entity.CIDR,
) (*ValidateOnlyResponse, bizerr.BizError) {
	out := &ValidateOnlyResponse{}
	maxNodeIP, err := GetMaxNodeIP(cloudProviderName, cidrMask)
	if err != nil {
		return nil, err
	}
	out.MaxNodeIP = maxNodeIP

	projectFilter := account.ProjectFilter{
		TenantIDs:  []uint64{project.TenantID},
		ProjectIDs: nil,
		Statuses:   []string{account.ProjectStatusActive},
	}
	projects, err := h.accountCli.QueryProject(ctx, projectFilter)
	if err != nil {
		return nil, err
	}

	var overlaps []*Overlap
	for _, p := range projects {
		cidrFilter := &cidrrepo.Filter{
			IDs:                nil,
			ProjectIDs:         []uint64{p.ID},
			RegionNames:        nil,
			CloudProviderNames: []globalcommonv1.CloudProvider_Name{cloudProviderName},
		}
		var cidrList []*entity.CIDR
		cidrList, err = h.cidrRepo.Query(ctx, cidrFilter, nil)
		if err != nil {
			return nil, err
		}
		cidrList = filterCurrentCIDR(p.ID, cidrList, currentCIDR)

		var overlap *Overlap
		overlap, err = GetOverlap(p.ID, cidr, cidrList)
		if err != nil {
			return nil, err
		}
		if overlap != nil && len(overlap.Regions) > 0 {
			overlaps = append(overlaps, overlap)
		}
	}
	out.Overlaps = overlaps
	return out, nil
}

func filterCurrentCIDR(projectID uint64, cidrs []*entity.CIDR, currentCIDR *entity.CIDR) []*entity.CIDR {
	// currentCIDR is nil when create
	if currentCIDR == nil {
		return cidrs
	}

	if projectID != currentCIDR.ProjectID {
		return cidrs
	}

	var cidrList []*entity.CIDR
	for _, cidr := range cidrs {
		if cidr.ID == currentCIDR.ID {
			continue
		}
		cidrList = append(cidrList, cidr)
	}
	return cidrList
}

func CheckCidrOverlap(cidr string, cidrs []*entity.CIDR) ([]*entity.BasicRegion, bizerr.BizError) {
	_, ipNet, err := net.ParseCIDR(cidr)
	if err != nil {
		return nil, bizerr.New(bizerr.ErrCodeParams, "Must be a valid IPv4 CIDR")
	}
	var regions []*entity.BasicRegion
	for _, c := range cidrs {
		var ipNet2 *net.IPNet
		_, ipNet2, err = net.ParseCIDR(c.CIDR)
		if err != nil {
			return nil, bizerr.New(
				bizerr.ErrCodeParams,
				"invalid CIDR %s of region %s",
				c.CIDR,
				c.BasicRegion.DisplayName,
			)
		}
		if OverlapCidrRange(ipNet, ipNet2) {
			regions = append(regions, c.BasicRegion)
		}
	}
	return regions, nil
}

func GetOverlap(projectID uint64, cidr string, cidrList []*entity.CIDR) (*Overlap, bizerr.BizError) {
	regions, err := CheckCidrOverlap(cidr, cidrList)
	if err != nil {
		return nil, err
	}

	return &Overlap{
		ProjectID: projectID,
		Regions:   regions,
	}, nil
}

func GetMaxNodeIP(cloudProviderName globalcommonv1.CloudProvider_Name, cidrMask int) (uint32, bizerr.BizError) {
	mapper, ok := CidrMask2MaxNodeIP[cloudProviderName]
	if !ok {
		return 0, bizerr.New(bizerr.ErrCodeParams, "unsupported cloud provider")
	}
	maxIpCount, ok := mapper[cidrMask]
	if !ok {
		return 0, bizerr.New(bizerr.ErrCodeParams, "unsupported CIDR mask")
	}
	return maxIpCount, nil
}

// ValidateCIDR ... validate CIDR and return network mask
func (h *networkHelperImp) ValidateCIDR(
	cidr string,
	cloudProviderName globalcommonv1.CloudProvider_Name,
	tenantID, projectID uint64,
) (int, bizerr.BizError) {
	ipAddr, ipNet, err := net.ParseCIDR(cidr)
	if err != nil {
		return 0, bizerr.New(bizerr.ErrCodeParams, "Must be a valid IPv4 CIDR")
	}

	if ipAddr.To4() == nil {
		return 0, bizerr.New(bizerr.ErrCodeParams, "Must be a valid IPv4 CIDR")
	}

	if ipNet.String() != cidr {
		return 0, bizerr.New(bizerr.ErrCodeParams, "This is not a valid CIDR for TiDB Cloud")
	}

	ones, _ := ipNet.Mask.Size()
	var validCIDRRanges []string
	switch cloudProviderName {
	case globalcommonv1.CloudProvider_AWS:
		if ones < AwsCidrMaskMin || ones > AwsCidrMaskMax {
			return ones, bizerr.New(
				bizerr.ErrCodeParams,
				"CIDR block size must be between /%d and /%d",
				AwsCidrMaskMin,
				AwsCidrMaskMax,
			)
		}
		validCIDRRanges = AwsValidCIDRRanges
	case globalcommonv1.CloudProvider_GCP:
		minGcpCidrMask := GcpCidrMaskMin
		maxGcpCidrMask := GcpCidrMaskMax
		if h.featGateCli.CheckFeatureEnabled(
			featuregate.CidrBlockSize,
			tenantID,
			projectID,
			globalcommonv1.CloudProvider_NAME_UNSPECIFIED.String(),
			globalcommonv1.Region_NAME_UNSPECIFIED.String(),
		) {
			minGcpCidrMask = GcpCidrMaskAllowListMin
		}

		if ones < minGcpCidrMask || ones > maxGcpCidrMask {
			return ones, bizerr.New(
				bizerr.ErrCodeParams,
				"CIDR block size must be between /%d and /%d",
				GcpCidrMaskMin,
				GcpCidrMaskMax,
			)
		}
		validCIDRRanges = GcpValidCIDRRanges
	case globalcommonv1.CloudProvider_AZURE:
		if ones < AzureCidrMaskMin || ones > AzureCidrMaskMax {
			return ones, bizerr.New(
				bizerr.ErrCodeParams,
				"CIDR block size must be between /%d and /%d",
				AzureCidrMaskMin,
				AzureCidrMaskMax,
			)
		}
		validCIDRRanges = AzureValidCIDRRanges
	default:
		return ones, bizerr.New(bizerr.ErrCodeParams, "invalid cloud provider: %s", cloudProviderName)
	}

	for _, r := range validCIDRRanges {
		var validNet *net.IPNet
		_, validNet, err = net.ParseCIDR(r)
		if err != nil {
			continue
		}

		if validNet.Contains(ipNet.IP) {
			return ones, nil
		}
	}

	return ones, bizerr.New(bizerr.ErrCodeParams, "This is not a valid CIDR for TiDB Cloud")
}

func ip2int(ip net.IP) uint32 {
	if len(ip) == 16 {
		return binary.BigEndian.Uint32(ip[12:16])
	}

	return binary.BigEndian.Uint32(ip)
}

// getRegionDefaultCIDR ... get region default cidr based on templateCIDR. regionList parameter is used to calculate the default cidr offset.
func getRegionDefaultCIDR(
	cloudProviderName globalcommonv1.CloudProvider_Name,
	regionName globalcommonv1.Region_Name,
	regionList []string,
) (string, bizerr.BizError) {
	templateCIDR := DefaultTemplateCIDR

	ipAddr, ipNet, err := net.ParseCIDR(templateCIDR)
	if err != nil {
		return "", bizerr.New(bizerr.ErrCodeUnknown, "parse template cidr(%s): %s", templateCIDR, err)
	}

	// Shift template CIDR to region CIDR
	// AWS: "172.16.0.0/16" -> "172.16.0.0/21"
	// GCP: "172.16.0.0/16" -> "172.16.0.0/19"
	ones, _ := ipNet.Mask.Size()
	regionMask := ones + RegionCIDRShiftBits
	if cloudProviderName == globalcommonv1.CloudProvider_GCP ||
		cloudProviderName == globalcommonv1.CloudProvider_AZURE {
		regionMask = ones + RegionGCPAndAzureCIDRShiftBits
	}
	regionCIDR := fmt.Sprintf("%s/%d", ipAddr.String(), regionMask)
	regionIP, regionIPNet, err := net.ParseCIDR(regionCIDR)
	if err != nil {
		return "", bizerr.New(bizerr.ErrCodeUnknown, "parse region cidr(%s): %s", regionCIDR, err)
	}

	// Calculate region CIDR offset
	ipInt := ip2int(regionIP)
	ones, total := regionIPNet.Mask.Size()
	var offset uint32 = 1 << (total - ones)

	// Iter region list to get the final ipInt
	found := false
	for _, i := range regionList {
		if i == regionName.String() {
			found = true
			break
		}

		ipInt += offset
	}
	if !found {
		return "", bizerr.New(bizerr.ErrCodeUnknown, "region %s not found in regionList", regionName)
	}

	ret := make(net.IP, 4)
	binary.BigEndian.PutUint32(ret, ipInt)

	// Convert to final region CIDR
	defaultCIDR := fmt.Sprintf("%s/%d", ret.String(), regionMask)
	return defaultCIDR, nil
}
