package eks

import (
	"context"
	"encoding/binary"
	"fmt"
	"net"
	"time"

	"github.com/crossplane-contrib/provider-aws/apis/ec2/v1beta1"
	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	"github.com/pingcap/errors"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-provider/pkg/deploy"
	"github.com/tidbcloud/infra-provider/pkg/k8s"
	"github.com/tidbcloud/infra-provider/pkg/namer"
	"github.com/tidbcloud/infra-provider/pkg/util"
	"github.com/tidbcloud/infra-provider/regional-server/api/v1alpha1"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/controllers/vpc/task"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/utils/pointer"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
)

type Subnet struct {
	*task.BaseTask
	nodeSubnets    []*v1beta1.Subnet
	publicSubnets  []*v1beta1.Subnet
	privateSubnets []*v1beta1.Subnet
}

func NewSubnet(ctx *task.Context) *Subnet {
	return &Subnet{
		BaseTask:       &task.BaseTask{Context: ctx},
		nodeSubnets:    []*v1beta1.Subnet{},
		publicSubnets:  []*v1beta1.Subnet{},
		privateSubnets: []*v1beta1.Subnet{},
	}
}

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

// for each zone create 3 subnets:
//   - nodes-<zone>: used for node group
//   - private-<zone>: used for private LB
//   - public-<zone>: used for public LB and accessing internet(via NAT now).
func (s *Subnet) Create(ctx context.Context) deploy.Result {
	tags := cloudv1.TagsFromLabels(s.VPC.Labels)
	key := types.NamespacedName{
		Name: s.GetVPCName(),
	}
	vpcRes := &v1beta1.VPC{}
	err := s.Local.Get(ctx, key, vpcRes)
	if err != nil {
		return deploy.Panic(err).WithRequeuAfter(time.Second * 30)
	}

	zones := s.GetZones()

	workers, public, private, err := GetAWSCIDR(s.VPC.Spec.CIDR, s.VPC.Spec.Region, len(zones))
	if err != nil {
		return deploy.Panic(err)
	}

	for i := 0; i < len(workers); i++ {
		zone := zones[i]

		// node subnet
		nodeSubnet, err := s.createNodeSubnet(ctx, zone, workers[i], vpcRes, tags)
		if err != nil {
			return deploy.Panic(err)
		}
		s.nodeSubnets = append(s.nodeSubnets, nodeSubnet)

		// public subnet
		publicSubnet, err := s.createPublicSubnet(ctx, zone, public[i], vpcRes, tags)
		if err != nil {
			return deploy.Panic(err)
		}
		s.publicSubnets = append(s.publicSubnets, publicSubnet)

		// private subnet
		privateSubnet, err := s.createPrivateSubnet(ctx, zone, private[i], vpcRes, tags)
		if err != nil {
			return deploy.Panic(err)
		}
		s.privateSubnets = append(s.privateSubnets, privateSubnet)
	}

	return deploy.Done()
}

func (s *Subnet) createNodeSubnet(ctx context.Context, zone string, cidr string, vpcRes *v1beta1.VPC, tags map[string]string) (*v1beta1.Subnet, error) {
	nodeSubnet := &v1beta1.Subnet{
		ObjectMeta: metav1.ObjectMeta{
			Name: namer.NodesSubnetName(vpcRes.Name, zone),
		},
		Spec: v1beta1.SubnetSpec{
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{Name: s.ProviderConfigName()},
			},
			ForProvider: v1beta1.SubnetParameters{
				Region:           vpcRes.Spec.ForProvider.Region,
				CIDRBlock:        cidr,
				AvailabilityZone: pointer.String(zone),
				Tags:             util.GetEC2V1BetaTags(util.AddResourceName(tags, namer.NodesSubnetName(vpcRes.Name, zone))),
				VPCIDRef: &xpv1.Reference{
					Name: vpcRes.Name,
				},
			},
		},
	}
	_, err := controllerutil.CreateOrUpdate(ctx, s.Local, nodeSubnet, func() error {
		return controllerutil.SetControllerReference(vpcRes, nodeSubnet, s.Local.Scheme())
	})
	return nodeSubnet, err
}

func (s *Subnet) createPublicSubnet(ctx context.Context, zone string, cidr string, vpcRes *v1beta1.VPC, tags map[string]string) (*v1beta1.Subnet, error) {
	tags = util.AddResourceName(tags, namer.PublicSubnetName(vpcRes.Name, zone))
	publicSubnettags := util.GetEC2V1BetaTags(tags)
	publicSubnet := &v1beta1.Subnet{
		TypeMeta: metav1.TypeMeta{
			APIVersion: "ec2.aws.crossplane.io/v1beta1",
			Kind:       "Subnet",
		},
		ObjectMeta: metav1.ObjectMeta{
			Name: namer.PublicSubnetName(vpcRes.Name, zone),
		},
		Spec: v1beta1.SubnetSpec{
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{Name: s.ProviderConfigName()},
			},
			ForProvider: v1beta1.SubnetParameters{
				Region:           vpcRes.Spec.ForProvider.Region,
				CIDRBlock:        cidr,
				AvailabilityZone: pointer.String(zone),
				Tags:             publicSubnettags,
				VPCIDRef: &xpv1.Reference{
					Name: vpcRes.Name,
				},
			},
		},
	}
	// for migrated subenet, we need to update it's owner reference
	_, err := controllerutil.CreateOrUpdate(ctx, s.Local, publicSubnet, func() error {
		return controllerutil.SetControllerReference(vpcRes, publicSubnet, s.Local.Scheme())
	})
	if err != nil {
		return nil, err
	}
	return publicSubnet, nil
}

func (s *Subnet) createPrivateSubnet(ctx context.Context, zone string, cidr string, vpcRes *v1beta1.VPC, tags map[string]string) (*v1beta1.Subnet, error) {
	tags = util.AddResourceName(tags, namer.PrivateSubnetName(vpcRes.Name, zone))
	privateSubnetTags := util.GetEC2V1BetaTags(tags)
	privateSubnet := &v1beta1.Subnet{
		TypeMeta: metav1.TypeMeta{
			APIVersion: "ec2.aws.crossplane.io/v1beta1",
			Kind:       "Subnet",
		},
		ObjectMeta: metav1.ObjectMeta{
			Name: namer.PrivateSubnetName(vpcRes.Name, zone),
		},
		Spec: v1beta1.SubnetSpec{
			ResourceSpec: xpv1.ResourceSpec{
				// ProviderConfigReference: &xpv1.Reference{Name: name},
				ProviderConfigReference: &xpv1.Reference{Name: s.ProviderConfigName()},
			},
			ForProvider: v1beta1.SubnetParameters{
				Region:           vpcRes.Spec.ForProvider.Region,
				CIDRBlock:        cidr,
				AvailabilityZone: pointer.String(zone),
				Tags:             privateSubnetTags,
				VPCIDRef: &xpv1.Reference{
					Name: vpcRes.Name,
				},
			},
		},
	}
	// for migrated subenet, we need to update it's owner reference
	_, err := controllerutil.CreateOrUpdate(ctx, s.Local, privateSubnet, func() error {
		return controllerutil.SetControllerReference(vpcRes, privateSubnet, s.Local.Scheme())
	})
	if err != nil {
		return nil, err
	}
	return privateSubnet, nil
}

func (s *Subnet) PostCreate(ctx context.Context) deploy.Result {
	rangeSubnet := func(subnets []*v1beta1.Subnet) ([]v1alpha1.Subnet, error) {
		subnetRecords := []v1alpha1.Subnet{}
		for _, subnet := range subnets {
			if err := s.Local.Get(ctx, types.NamespacedName{Name: subnet.Name}, subnet); err != nil {
				return nil, err
			}
			if subnet.Status.AtProvider.SubnetState != "available" {
				return nil, fmt.Errorf("subnet status not available: %v", subnet.Status.AtProvider.SubnetState)
			}
			subnetRecords = append(subnetRecords, v1alpha1.Subnet{
				SubnetID:           subnet.Status.AtProvider.SubnetID,
				Name:               subnet.Name,
				AvailabilityZone:   *subnet.Spec.ForProvider.AvailabilityZone,
				AvailabilityZoneID: *subnet.Spec.ForProvider.AvailabilityZoneID,
				CIDRBlock:          subnet.Spec.ForProvider.CIDRBlock,
			})
		}
		return subnetRecords, nil
	}
	var err error
	s.VPC.Status.NodeSubnets, err = rangeSubnet(s.nodeSubnets)
	if err != nil {
		return deploy.Panic(err).WithRequeuAfter(time.Second * 30)
	}
	s.VPC.Status.PublicSubnets, err = rangeSubnet(s.publicSubnets)
	if err != nil {
		return deploy.Panic(err).WithRequeuAfter(time.Second * 30)
	}
	s.VPC.Status.PrivateSubnets, err = rangeSubnet(s.privateSubnets)
	if err != nil {
		return deploy.Panic(err).WithRequeuAfter(time.Second * 30)
	}

	// 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 *Subnet) Delete(ctx context.Context) deploy.Result {
	zones := s.GetZones()
	name := s.GetVPCName()

	// FIXME(cwen0): delete subnet by zone config may cause resource leaking or error
	// if the config is modified.

	for i := 0; i < len(zones); i++ {
		zone := zones[i]

		subnet := &v1beta1.Subnet{
			ObjectMeta: metav1.ObjectMeta{
				Name: namer.NodesSubnetName(name, zone),
			},
		}
		if err := k8s.DeleteObject(ctx, s.Local, subnet); err != nil {
			return deploy.Error(err)
		}
		subnet = &v1beta1.Subnet{
			ObjectMeta: metav1.ObjectMeta{
				Name: namer.PublicSubnetName(name, zone),
			},
		}
		if err := k8s.DeleteObject(ctx, s.Local, subnet); err != nil {
			return deploy.Error(err)
		}
		subnet = &v1beta1.Subnet{
			ObjectMeta: metav1.ObjectMeta{
				Name: namer.PrivateSubnetName(name, zone),
			},
		}
		if err := k8s.DeleteObject(ctx, s.Local, subnet); err != nil {
			return deploy.Error(err)
		}

	}
	return deploy.Done()
}

func (s *Subnet) PostDelete(ctx context.Context) deploy.Result {
	return deploy.Done()
}

func GetAWSCIDR(cidr string, region string, zones int) (workers []string, publicLBs []string, internalLBs []string, err error) {
	for i := 0; i < zones; i++ {
		zoneCIDR, err := getSubCIDR(cidr, 2, i)
		if err != nil {
			return nil, nil, nil, err
		}

		// split by one bit, worker take halve
		workerCIDR, err := getSubCIDR(zoneCIDR, 1, 0)
		if err != nil {
			return nil, nil, nil, err
		}
		workers = append(workers, workerCIDR)

		left, err := getSubCIDR(zoneCIDR, 1, 1)
		if err != nil {
			return nil, nil, nil, err
		}

		// split the left part for public and internal LB
		public, err := getSubCIDR(left, 1, 0)
		if err != nil {
			return nil, nil, nil, err
		}
		publicLBs = append(publicLBs, public)

		internal, err := getSubCIDR(left, 1, 1)
		if err != nil {
			return nil, nil, nil, err
		}
		internalLBs = append(internalLBs, internal)
	}
	// The first 3 return values are the same type, so we use named result parameters and naked return
	return // nolint:nakedret
}

func getSubCIDR(cidr string, subBits int, index int) (string, error) {
	_, ipNet, err := net.ParseCIDR(cidr)
	if err != nil {
		return "", errors.AddStack(err)
	}

	ipInt := binary.BigEndian.Uint32(ipNet.IP)

	ones, _ := ipNet.Mask.Size()
	if ones+subBits > 32 {
		return "", fmt.Errorf("host bits is %d, but want to split by %d bits", 32-ones, subBits)
	}

	index <<= (32 - ones - subBits)

	data := make([]byte, 4)
	binary.BigEndian.PutUint32(data, ipInt|uint32(index))

	sub := fmt.Sprintf("%s/%d", net.IP(data).String(), ones+subBits)
	return sub, nil
}
