package gke

import (
	"context"
	"fmt"
	"net"
	"time"

	gcpcompute "google.golang.org/api/compute/v1"
	"k8s.io/apimachinery/pkg/types"

	gcpv1alpha1 "github.com/crossplane-contrib/provider-gcp/apis/compute/v1alpha1"
	"github.com/tidbcloud/infra-provider/pkg/cloud/gcp"
	pkgconfig "github.com/tidbcloud/infra-provider/pkg/config"
	"github.com/tidbcloud/infra-provider/pkg/deploy"
	"github.com/tidbcloud/infra-provider/regional-server/api/v1alpha1"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/common/cidr"
	rsutils "github.com/tidbcloud/infra-provider/regional-server/pkg/common/utils"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/controllers/privatelinkservice/task"
)

type NATSubnet struct {
	*task.BaseTask
	gcpCli      gcp.Client
	iPCidrRange string
}

func NewNATSubnet(ctx *task.Context, gcpCli gcp.Client) *NATSubnet {
	return &NATSubnet{BaseTask: &task.BaseTask{Context: ctx}, gcpCli: gcpCli}
}

func (t *NATSubnet) Name() string {
	return "NATSubnet"
}

// no need to call gcp to createpscsubnet again for migrated resource.
func (t *NATSubnet) Create(ctx context.Context) deploy.Result {
	if t.PrivateLinkService.Labels[v1alpha1.IsLegacyKey] == "true" {
		return deploy.Done()
	}
	firewall := &gcpv1alpha1.Firewall{}
	key := types.NamespacedName{
		Name: t.VPC.Status.PSCFirewall.Name,
	}
	err := t.Local.Get(ctx, key, firewall)
	if err != nil {
		return deploy.Panic(err).WithRequeuAfter(time.Second * 30)
	}
	ready, synced := rsutils.GetConditions(firewall.Status.Conditions)
	if !ready || !synced {
		return deploy.Panic(fmt.Errorf("PSC firewall not ready or not synced")).WithRequeuAfter(time.Second * 30)
	}
	sourceRanges := firewall.Spec.ForProvider.SourceRanges
	if len(sourceRanges) < 1 {
		t.Logger.Error(nil, "no source ranges found")
		return deploy.Panic(fmt.Errorf("no source ranges found")).WithRequeuAfter(time.Second * 30)
	}

	existingSubnets, err := t.gcpCli.ListSubnetworks(
		ctx, t.VPC.Spec.AccountRef.AccountID, t.PrivateLinkService.Spec.Region, t.VPC.Status.VPC.VPCID)
	if err != nil {
		t.Logger.Error(err, "failed to list subnetworks")
		return deploy.Panic(err).WithRequeuAfter(time.Second * 30)
	}

	tenantID := t.VPC.GetLabels()[v1alpha1.TagTenantKey]
	projectID := t.VPC.GetLabels()[v1alpha1.TagProjectKey]
	var subnetCIDR string
	if t.Config.FeatureGates != nil &&
		pkgconfig.FeatureEnabled(t.Config.FeatureGates, pkgconfig.AllocateMoreNATs, tenantID, projectID) {
		cidr, err := t.findNATSubnetwork(sourceRanges, existingSubnets)
		if err != nil {
			t.Logger.Error(err, "failed to find NAT subnetwork")
			return deploy.Panic(err).WithRequeuAfter(time.Second * 30)
		}
		subnetCIDR = cidr
	} else {
		cidr, err := t.legacyFindNATSubnetwork(sourceRanges[0], existingSubnets)
		if err != nil {
			t.Logger.Error(err, "failed to find NAT subnetwork")
			return deploy.Panic(err).WithRequeuAfter(time.Second * 30)
		}
		subnetCIDR = cidr
	}

	// Add createSubnet
	// crossplane-gcp don't support to specify the purpose of subnet
	if err := t.gcpCli.CreatePSCSubnet(
		ctx, t.VPC.Spec.AccountRef.AccountID, t.PrivateLinkService.Spec.Region, t.natSubnetworkName(), t.VPC.Status.VPC.VPCID, subnetCIDR); err != nil {
		t.Logger.Error(err, "failed to create NAT subnetwork")
		return deploy.Panic(err).WithRequeuAfter(time.Second * 30)
	}
	t.iPCidrRange = subnetCIDR

	return deploy.Done()
}

// no need to update the Status.NATSubnet again for migrated resource.
// the migration handler will set the status.
func (t *NATSubnet) PostCreate(ctx context.Context) deploy.Result {
	if t.PrivateLinkService.Labels[v1alpha1.IsLegacyKey] == "true" {
		return deploy.Done()
	}
	exists, err := t.gcpCli.IsSubnetExists(ctx, t.VPC.Spec.AccountRef.AccountID, t.PrivateLinkService.Spec.Region, t.natSubnetworkName())
	if err != nil {
		t.Logger.Error(err, "failed to check nat subnet")
		return deploy.Panic(err).WithRequeuAfter(time.Second * 30)
	}

	if !exists {
		t.Logger.Error(nil, "nat subnet not found")
		return deploy.Panic(fmt.Errorf("nat subnet not found")).WithRequeuAfter(time.Second * 30)
	}

	t.PrivateLinkService.Status.NATSubnet = v1alpha1.NATSubnetStatus{
		Name:        t.natSubnetworkName(),
		IPCidrRange: t.iPCidrRange,
	}

	if err := t.PrivateLinkServiceRW.UpdateStatus(ctx, t.PrivateLinkService); err != nil {
		t.Logger.Error(err, "failed to update status")
		return deploy.Panic(err).WithRequeuAfter(time.Second * 30)
	}

	return deploy.Done()
}

// findNATSubnetwork find a not-used subnetwork to perform PSC NAT.
// 'prefix' is the total CIDR can be used in this network, and the prefix lenth is /25. The following 3-bit is
// the index-bit, all cluster in this network will take one, so up to 8(2^3) clusters can use PSC in a network.
// As a result, the NAT subnetwork prefix is always /28.
//
// For example, the prefix CIDR is `10.250.14.128/25`, and the subnetwork `10.250.14.128/28` is already used for
// another cluster, so the result is `10.250.14.144/25`
func (t *NATSubnet) legacyFindNATSubnetwork(prefix string, subnetworks []*gcpcompute.Subnetwork) (string, error) {
	existing := make(map[string]bool)
	for _, s := range subnetworks {
		if s.Name == t.natSubnetworkName() {
			return s.IpCidrRange, nil
		}
		existing[s.IpCidrRange] = true
	}

	_, prefixNet, err := net.ParseCIDR(prefix)
	if err != nil {
		return "", err
	}
	prefixNet.Mask = net.CIDRMask(28, 32)
	suffixInt := int(prefixNet.IP.To4()[3] & 0b10000000)
	for i := 0; i < 8; i++ {
		prefixNet.IP.To4()[3] = byte(suffixInt)
		if !existing[prefixNet.String()] {
			return prefixNet.String(), nil
		}
		suffixInt += 16
	}

	return "", fmt.Errorf("nat subnet has been used up")
}

func (t *NATSubnet) findNATSubnetwork(prefixes []string, subnetworks []*gcpcompute.Subnetwork) (string, error) {
	existing := make(map[string]struct{})
	for _, prefix := range prefixes {
		for _, s := range subnetworks {
			if s.Name == t.natSubnetworkName() {
				return s.IpCidrRange, nil
			}
			existing[s.IpCidrRange] = struct{}{}
		}

		_, prefixNet, err := net.ParseCIDR(prefix)
		if err != nil {
			return "", err
		}
		size, _ := prefixNet.Mask.Size()
		nets, err := cidr.SplitSubnet(prefixNet, 1<<(28-size))
		if err != nil {
			return "", err
		}
		for _, n := range nets {
			if _, ok := existing[n.String()]; !ok {
				return n.String(), nil
			}
		}
	}

	return "", fmt.Errorf("nat subnet has been used up")
}

func (t *NATSubnet) natSubnetworkName() string {
	if name := t.PrivateLinkService.Annotations[v1alpha1.AnnotationKeyOwnCrossplaneNamePSCNATSubenet]; name != "" {
		return name
	}
	return fmt.Sprintf("tidb%s-psc-nat", t.PrivateLinkService.Name)
}

func (t *NATSubnet) Delete(ctx context.Context) deploy.Result {
	if err := t.gcpCli.DeleteSubnet(
		ctx, t.VPC.Spec.AccountRef.AccountID, t.PrivateLinkService.Spec.Region, t.natSubnetworkName()); err != nil {
		t.Logger.Error(err, "failed to delete nat subnet")
		return deploy.Panic(err).WithRequeuAfter(time.Second * 30)
	}

	return deploy.Done()
}

func (t *NATSubnet) PostDelete(ctx context.Context) deploy.Result {
	exists, err := t.gcpCli.IsSubnetExists(
		ctx, t.VPC.Spec.AccountRef.AccountID, t.PrivateLinkService.Spec.Region, t.natSubnetworkName())
	if err != nil {
		t.Logger.Error(err, "failed to check nat subnet")
		return deploy.Panic(err).WithRequeuAfter(time.Second * 30)
	}

	if exists {
		t.Logger.Error(nil, "nat subnet not deleted")
		return deploy.Panic(fmt.Errorf("nat subnet not deleted")).WithRequeuAfter(time.Second * 30)
	}

	return deploy.Done()
}
