package ack

import (
	"context"
	"errors"
	"fmt"
	"maps"
	"sort"
	"time"

	alivpcv1alpha1 "github.com/crossplane-contrib/provider-upjet-alibabacloud/apis/vpc/v1alpha1"
	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	"github.com/tidbcloud/infra-provider/regional-server/api/v1alpha1"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/consts"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/controllers"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/controllers/vpc/task"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/deploy"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/k8s"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/namer"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/util"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/utils/ptr"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

type subnetTask struct {
	*task.BaseTask
	nodeSubnets     []*alivpcv1alpha1.Vswitch
	publicSubnets   []*alivpcv1alpha1.Vswitch
	privateSubnets  []*alivpcv1alpha1.Vswitch
	endpointSubnets []*alivpcv1alpha1.Vswitch
}

func NewSubnet(ctx *task.Context) deploy.Task {
	return &subnetTask{
		BaseTask:        &task.BaseTask{Context: ctx},
		nodeSubnets:     []*alivpcv1alpha1.Vswitch{},
		publicSubnets:   []*alivpcv1alpha1.Vswitch{},
		privateSubnets:  []*alivpcv1alpha1.Vswitch{},
		endpointSubnets: []*alivpcv1alpha1.Vswitch{},
	}
}

// Name implements Task interface.
// Return the name of the task.
func (s *subnetTask) Name() string {
	return "Subnet"
}

func (s *subnetTask) getVPCName() string {
	return s.VPC.Status.VPC.Name
}

// Create implements Task interface. In the regular layout it creates node,
// public, and private subnets for every zone. In optimized node-zone layout it
// restricts node subnets to nodeZones and public/private subnets to two AZs for
// ACK control plane and NLB zone-maps.
func (s *subnetTask) Create(ctx context.Context) deploy.Result {
	s.BeginAt = util.Pointer(time.Now())
	tags := controllers.ConvertToCloudTags(s.VPC.Labels, consts.ProviderTypeACK)
	tags = util.MergeMaps(tags, s.Config.AdditionalCloudTags)
	vpcName := s.getVPCName()
	key := types.NamespacedName{
		Name: vpcName,
	}
	vpcRes := &alivpcv1alpha1.VPC{}
	err := s.Local.Get(ctx, key, vpcRes)
	if err != nil {
		return deploy.Panic(err).WithRequeuAfter(time.Second * 30)
	}

	zones := s.GetZones()
	nodeZones := s.GetNodeZones()
	configuredEndpointZones := s.VPC.Spec.PrivateLinkEndpointZones
	useEndpointSubnets := util.ShouldUsePrivateLinkEndpointSubnets(configuredEndpointZones, nodeZones)
	endpointZones := []string(nil)
	if useEndpointSubnets {
		endpointZones = configuredEndpointZones
	}
	if shouldUseOptimizedCIDR(nodeZones, zones) {
		if err := s.createOptimizedSubnets(ctx, vpcRes, tags, nodeZones, zones, endpointZones); err != nil {
			return deploy.Panic(err).WithRequeuAfter(time.Second * 30)
		}
		return deploy.Done()
	}

	if useEndpointSubnets {
		return deploy.Panic(fmt.Errorf(
			"vpc.spec.privateLinkEndpointZones requires optimized CIDR layout, nodeZones=%v, zones=%v",
			nodeZones,
			zones,
		)).WithRequeuAfter(time.Second * 30)
	}

	workers, public, private, err := util.GetAliyunSubnetCIDRs(s.VPC.Spec.CIDR, len(zones))
	if err != nil {
		return deploy.Panic(err).WithRequeuAfter(time.Second * 30)
	}

	for i, zone := range zones {
		// node subnet
		nodeSubnet, err := s.createSubnet(ctx, namer.NodesSubnetName(vpcName, zone), zone, workers[i], vpcRes, tags)
		if err != nil {
			return deploy.Panic(err).WithRequeuAfter(time.Second * 30)
		}
		s.nodeSubnets = append(s.nodeSubnets, nodeSubnet)

		// public subnet
		publicSubnet, err := s.createSubnet(ctx, namer.PublicSubnetName(vpcName, zone), zone, public[i], vpcRes, tags)
		if err != nil {
			return deploy.Panic(err).WithRequeuAfter(time.Second * 30)
		}
		s.publicSubnets = append(s.publicSubnets, publicSubnet)

		// private subnet
		privateSubnet, err := s.createSubnet(
			ctx, namer.PrivateSubnetName(vpcName, zone), zone, private[i], vpcRes, tags)
		if err != nil {
			return deploy.Panic(err).WithRequeuAfter(time.Second * 30)
		}
		s.privateSubnets = append(s.privateSubnets, privateSubnet)
	}

	return deploy.Done()
}

func (s *subnetTask) createOptimizedSubnets(
	ctx context.Context,
	vpcRes *alivpcv1alpha1.VPC,
	tags map[string]string,
	nodeZones []string,
	zones []string,
	endpointZones []string,
) error {
	workers, public, private, err := util.GetAliyunSubnetCIDRsOptimized(s.VPC.Spec.CIDR, nodeZones, zones)
	if err != nil {
		return err
	}
	endpoints := map[string]string{}
	if len(endpointZones) > 0 {
		endpoints, err = util.GetAliyunPrivateLinkEndpointSubnetCIDRs(s.VPC.Spec.CIDR, endpointZones)
		if err != nil {
			return err
		}
	}

	for _, zone := range sortedSubnetZones(workers) {
		nodeSubnet, createErr := s.createSubnet(ctx, namer.NodesSubnetName(vpcRes.Name, zone), zone, workers[zone], vpcRes, tags)
		if createErr != nil {
			return createErr
		}
		s.nodeSubnets = append(s.nodeSubnets, nodeSubnet)
	}

	for _, zone := range sortedSubnetZones(public) {
		publicSubnet, createErr := s.createSubnet(ctx, namer.PublicSubnetName(vpcRes.Name, zone), zone, public[zone], vpcRes, tags)
		if createErr != nil {
			return createErr
		}
		s.publicSubnets = append(s.publicSubnets, publicSubnet)

		privateSubnet, createErr := s.createSubnet(ctx, namer.PrivateSubnetName(vpcRes.Name, zone), zone, private[zone], vpcRes, tags)
		if createErr != nil {
			return createErr
		}
		s.privateSubnets = append(s.privateSubnets, privateSubnet)
	}

	for _, zone := range sortedSubnetZones(endpoints) {
		endpointSubnet, createErr := s.createSubnet(
			ctx,
			namer.PrivateLinkEndpointSubnetName(vpcRes.Name, zone),
			zone,
			endpoints[zone],
			vpcRes,
			tags,
		)
		if createErr != nil {
			return createErr
		}
		s.endpointSubnets = append(s.endpointSubnets, endpointSubnet)
	}

	return nil
}

func (s *subnetTask) createSubnet(
	ctx context.Context, subnetName, zone, cidr string, vpcRes *alivpcv1alpha1.VPC, tags map[string]string,
) (*alivpcv1alpha1.Vswitch, error) {
	subnetTags := make(map[string]string, len(tags))
	maps.Copy(subnetTags, tags)
	subnetLabels := make(map[string]string, len(s.VPC.Labels))
	maps.Copy(subnetLabels, s.VPC.Labels)
	subnetLabels[v1alpha1.VPCNameKey] = s.VPC.Name

	subnet := &alivpcv1alpha1.Vswitch{
		ObjectMeta: metav1.ObjectMeta{
			Name:   subnetName,
			Labels: subnetLabels,
		},
		Spec: alivpcv1alpha1.VswitchSpec{
			ForProvider: alivpcv1alpha1.VswitchParameters{
				VswitchName: ptr.To(subnetName),
				Region:      vpcRes.Spec.ForProvider.Region,
				ZoneID:      ptr.To(zone),
				CidrBlock:   ptr.To(cidr),
				Tags:        util.ToMapPointer(subnetTags),
				VPCIDRef: &xpv1.Reference{
					Name: vpcRes.Name,
				},
			},
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{Name: s.ProviderConfigName()},
				ManagementPolicies:      controllers.GetResourceManagementPolicies(),
			},
		},
	}

	if err := k8s.Apply(ctx, s.Local, subnet); err != nil {
		return nil, err
	}
	return subnet, nil
}

func (s *subnetTask) PostCreate(ctx context.Context) deploy.Result {
	var err error
	s.VPC.Status.NodeSubnets, err = s.convertCloudSubnets(ctx, s.nodeSubnets)
	if err != nil {
		return deploy.Panic(err).WithRequeuAfter(time.Second * 30)
	}
	s.VPC.Status.PublicSubnets, err = s.convertCloudSubnets(ctx, s.publicSubnets)
	if err != nil {
		return deploy.Panic(err).WithRequeuAfter(time.Second * 30)
	}
	s.VPC.Status.PrivateSubnets, err = s.convertCloudSubnets(ctx, s.privateSubnets)
	if err != nil {
		return deploy.Panic(err).WithRequeuAfter(time.Second * 30)
	}
	if len(s.endpointSubnets) > 0 {
		s.VPC.Status.PrivateLinkEndpointSubnets, err = s.convertCloudSubnets(ctx, s.endpointSubnets)
		if err != nil {
			return deploy.Panic(err).WithRequeuAfter(time.Second * 30)
		}
	} else {
		s.VPC.Status.PrivateLinkEndpointSubnets = nil
	}

	// this update won't trigger reconcile
	if err := s.VPCRW.UpdateStatus(ctx, s.VPC); err != nil {
		return deploy.Panic(err).WithRequeuAfter(time.Second * 30)
	}

	return deploy.Done()
}

func (s *subnetTask) Delete(ctx context.Context) deploy.Result {
	labelOpt := client.MatchingLabels{v1alpha1.VPCNameKey: s.VPC.Name}
	subnetList := &alivpcv1alpha1.VswitchList{}
	if err := s.Local.List(ctx, subnetList, labelOpt); client.IgnoreNotFound(err) != nil {
		return deploy.Panic(err).WithRequeuAfter(time.Second * 30)
	}

	for _, subnet := range subnetList.Items {
		if err := controllers.DeleteUpboundResource(ctx, s.Local, &subnet); err != nil {
			return deploy.Error(err)
		}
	}
	return deploy.Done()
}

func (s *subnetTask) PostDelete(ctx context.Context) deploy.Result {
	labelOpt := client.MatchingLabels{v1alpha1.VPCNameKey: s.VPC.Name}
	subnetList := &alivpcv1alpha1.VswitchList{}
	if err := s.Local.List(ctx, subnetList, labelOpt); client.IgnoreNotFound(err) != nil {
		return deploy.Panic(err).WithRequeuAfter(time.Second * 30)
	}

	if len(subnetList.Items) > 0 {
		return deploy.Error(errors.New("subnets still exist")).WithRequeuAfter(time.Second * 30)
	}
	return deploy.Done()
}

func (s *subnetTask) convertCloudSubnets(ctx context.Context, subnets []*alivpcv1alpha1.Vswitch) ([]v1alpha1.Subnet, error) {
	subnetRecords := make([]v1alpha1.Subnet, 0, len(subnets))
	for _, subnet := range subnets {
		subnetRecord, err := s.convertCloudSubnet(ctx, subnet)
		if err != nil {
			return nil, err
		}
		subnetRecords = append(subnetRecords, *subnetRecord)
	}
	return subnetRecords, nil
}

func (s *subnetTask) convertCloudSubnet(ctx context.Context, subnet *alivpcv1alpha1.Vswitch) (*v1alpha1.Subnet, error) {
	if err := s.Local.Get(ctx, types.NamespacedName{Name: subnet.Name}, subnet); err != nil {
		return nil, err
	}
	if !controllers.IsResourceReady(subnet.Status.ResourceStatus) {
		return nil, fmt.Errorf("subnet %s is not ready", subnet.Name)
	}
	return &v1alpha1.Subnet{
		SubnetID:           *subnet.Status.AtProvider.ID,
		Name:               subnet.Name,
		AvailabilityZone:   *subnet.Status.AtProvider.ZoneID,
		AvailabilityZoneID: *subnet.Status.AtProvider.ZoneID,
		CIDRBlock:          *subnet.Status.AtProvider.CidrBlock,
	}, nil
}

func sortedSubnetZones(subnets map[string]string) []string {
	zones := make([]string, 0, len(subnets))
	for zone := range subnets {
		zones = append(zones, zone)
	}
	sort.Strings(zones)
	return zones
}

func shouldUseOptimizedCIDR(nodeZones []string, zones []string) bool {
	return len(nodeZones) > 0 && len(nodeZones) <= 2 && len(nodeZones) < len(zones)
}
