package eks

import (
	"context"
	"fmt"
	"time"

	ec2v1alpha1 "github.com/crossplane-contrib/provider-aws/apis/ec2/v1alpha1"
	"github.com/crossplane-contrib/provider-aws/apis/ec2/v1beta1"
	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	"github.com/samber/lo"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/utils/pointer"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"

	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/internet/task"
)

const (
	defaultRequeuInterval = time.Second * 30
)

type Route struct {
	*task.BaseTask
	routeTables []*v1beta1.RouteTable
	routes      []*ec2v1alpha1.Route
}

func NewRoute(c *task.Context) *Route {
	return &Route{BaseTask: &task.BaseTask{Context: c}}
}

// Name implements Task Interface
// Returns the name of the task.
func (t *Route) Name() string {
	return "Route"
}

func (t *Route) Create(ctx context.Context) deploy.Result {
	if t.IsHibernated() {
		return deploy.Done()
	}
	name := t.InternetName()
	tags := cloudv1.TagsFromLabels(t.Internet.Labels)
	// TODO(cwen0): use zone config may cause problem, if zone was deleted or updated, the route table will not be deleted.
	zones := t.GetZones()
	vpcRes := &v1beta1.VPC{}
	if err := t.Local.Get(ctx, types.NamespacedName{Name: t.VPCName()}, vpcRes); err != nil {
		return deploy.Panic(err).WithRequeuAfter(defaultRequeuInterval)
	}
	haveCreateGardenerPublicRoute := false
	for _, zone := range zones {
		nodeSubnet, err := findSubnetByZone(ctx, t.Local, t.VPC.Status.NodeSubnets, zone)
		if err != nil {
			return deploy.Panic(err).WithRequeuAfter(defaultRequeuInterval)
		}
		privateSubnet, err := findSubnetByZone(ctx, t.Local, t.VPC.Status.PrivateSubnets, zone)
		if err != nil {
			return deploy.Panic(err).WithRequeuAfter(defaultRequeuInterval)
		}
		publicSubnet, err := findSubnetByZone(ctx, t.Local, t.VPC.Status.PublicSubnets, zone)
		if err != nil {
			return deploy.Panic(err).WithRequeuAfter(defaultRequeuInterval)
		}
		t.Logger.Info("find subnets", "nodeSubnet", nodeSubnet.Name, "privateSubnet", privateSubnet.Name, "publicSubnet", publicSubnet.Name)

		secondarySubnets, err := findSecondarySubnestByZone(ctx, t.Local, t.VPC.Status.SecondaryNodeSubnets, zone)
		if err != nil {
			return deploy.Panic(err).WithRequeuAfter(defaultRequeuInterval)
		}
		t.Logger.Info("find secondary subnets", "secondarySubnets", secondarySubnets)

		natName := namer.PublicSubnetName(name, zone)
		nat := v1alpha1.FindNATByName(t.Internet.Status.NATs, natName)
		if nat == nil {
			return deploy.Panic(fmt.Errorf("NAT %s not found", natName)).WithRequeuAfter(defaultRequeuInterval)
		}
		natRes := &v1beta1.NATGateway{}
		if err := t.Local.Get(ctx, types.NamespacedName{Name: nat.Name}, natRes); err != nil {
			return deploy.Panic(err).WithRequeuAfter(time.Second * 30)
		}
		// create route table for Private Subnet
		privateName := namer.PrivateSubnetName(name, zone)
		tags := util.AddResourceName(tags, privateName)
		nodeSubnets := []*v1beta1.Subnet{nodeSubnet, privateSubnet}
		if len(secondarySubnets) > 0 {
			nodeSubnets = append(nodeSubnets, secondarySubnets...)
		}
		privateRT, err := t.createRouteTable(ctx, privateName, tags, nodeSubnets, vpcRes)
		if err != nil {
			return deploy.Panic(err)
		}
		// create route for Private Subnet
		privateR, err := t.createRoute(ctx, privateName, ec2v1alpha1.CustomRouteParameters{
			NATGatewayIDRef: &xpv1.Reference{Name: nat.Name},
			RouteTableIDRef: &xpv1.Reference{Name: privateRT.Name},
		})
		if err != nil {
			return deploy.Panic(err)
		}
		t.routeTables = append(t.routeTables, privateRT)
		t.routes = append(t.routes, privateR)

		// create route table for Public Subnet
		// gardener will create one public route associated all public subnets
		// if 'haveCreateGardenerPublicRoute' is true, represent we have created gardener public route table and route
		// so we don't need to create them again.
		if haveCreateGardenerPublicRoute {
			continue
		}
		publicName := namer.PublicSubnetName(name, zone)
		tags = util.AddResourceName(tags, publicName)
		publicSubnets := []*v1beta1.Subnet{publicSubnet}
		if v1alpha1.IsOriginatedFromGardener(t.Internet) {
			publicName = vpcPublicRouteTableName(t.VPCName())
			tags = util.AddResourceName(tags, publicName)
			// reset publicSubnets to all public subnets
			publicSubnets = []*v1beta1.Subnet{}
			for _, subnet := range t.VPC.Status.PublicSubnets {
				publicSubnets = append(publicSubnets, &v1beta1.Subnet{
					ObjectMeta: metav1.ObjectMeta{
						Name: subnet.Name,
					},
				})
			}
			haveCreateGardenerPublicRoute = true
		}
		publicRT, err := t.createRouteTable(ctx, publicName, tags, publicSubnets, vpcRes)
		if err != nil {
			return deploy.Panic(err)
		}
		// create route for Public Subnet
		publicR, err := t.createRoute(ctx, publicName, ec2v1alpha1.CustomRouteParameters{
			GatewayIDRef:    &xpv1.Reference{Name: t.Internet.Status.InternetGateway.Name},
			RouteTableIDRef: &xpv1.Reference{Name: publicRT.Name},
		})
		if err != nil {
			return deploy.Panic(err)
		}

		t.routeTables = append(t.routeTables, publicRT)
		t.routes = append(t.routes, publicR)
	}

	return deploy.Done()
}

func vpcPublicRouteTableName(vpcName string) string {
	return vpcName + "-public"
}

func (t *Route) createRoute(ctx context.Context, name string, parameters ec2v1alpha1.CustomRouteParameters) (*ec2v1alpha1.Route, error) {
	route := &ec2v1alpha1.Route{
		TypeMeta: metav1.TypeMeta{
			APIVersion: "ec2.aws.crossplane.io/v1alpha1",
			Kind:       "Route",
		},
		ObjectMeta: metav1.ObjectMeta{
			Name: name,
			Labels: map[string]string{
				v1alpha1.InternetNameKey: t.InternetName(),
			},
		},
		Spec: ec2v1alpha1.RouteSpec{
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{Name: t.ProviderConfigName()},
			},
			ForProvider: ec2v1alpha1.RouteParameters{
				Region:                t.Internet.Spec.Region,
				DestinationCIDRBlock:  pointer.String("0.0.0.0/0"),
				CustomRouteParameters: parameters,
			},
		},
	}
	if t.Internet.Labels[v1alpha1.IsLegacyKey] == "true" {
		mutateFn := func() error {
			return nil
		}
		if _, err := controllerutil.CreateOrUpdate(ctx, t.Local, route, mutateFn); err != nil {
			return nil, err
		}
		return route, nil
	}
	if err := k8s.Apply(ctx, t.Local, route); err != nil {
		return nil, err
	}
	return route, nil
}

func (t *Route) createRouteTable(ctx context.Context, name string, tags map[string]string, subnets []*v1beta1.Subnet, vpc *v1beta1.VPC) (*v1beta1.RouteTable, error) {
	associations := make([]v1beta1.Association, 0, len(subnets))
	for _, subnet := range subnets {
		associations = append(associations, v1beta1.Association{
			SubnetIDRef: &xpv1.Reference{Name: subnet.Name},
		})
	}
	rt := &v1beta1.RouteTable{
		TypeMeta: metav1.TypeMeta{
			APIVersion: "ec2.aws.crossplane.io/v1beta1",
			Kind:       "RouteTable",
		},
		ObjectMeta: metav1.ObjectMeta{
			Name: name,
			Labels: map[string]string{
				v1alpha1.InternetNameKey: t.InternetName(),
			},
		},
		Spec: v1beta1.RouteTableSpec{
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{Name: t.ProviderConfigName()},
			},
			ForProvider: v1beta1.RouteTableParameters{
				Region:       t.Internet.Spec.Region,
				Associations: associations,
				// Ignore routes in route table controller, we will create routes in route controller.
				// See https://github.com/crossplane-contrib/provider-aws/issues/1036#issuecomment-1003075859 for more detail
				IgnoreRoutes: pointer.Bool(true),
				Tags:         util.GetEC2V1BetaTags(tags),
				VPCIDRef:     &xpv1.Reference{Name: vpc.Name},
			},
		},
	}

	// TODO: check it
	// if len(privateRouteTableIDs) > 0 {
	// 	setExternalName(privateRT, privateRouteTableIDs[i])
	// }
	if t.Internet.Labels[v1alpha1.IsLegacyKey] == "true" {
		mutateFn := func() error {
			return controllerutil.SetControllerReference(vpc, rt, t.Local.Scheme())
		}
		if _, err := controllerutil.CreateOrUpdate(ctx, t.Local, rt, mutateFn); err != nil {
			return nil, err
		}
		return rt, nil
	}
	if err := controllerutil.SetControllerReference(vpc, rt, t.Local.Scheme()); err != nil {
		return nil, err
	}
	if err := k8s.Apply(ctx, t.Local, rt); err != nil {
		return nil, err
	}
	return rt, nil
}

func (t *Route) PostCreate(ctx context.Context) deploy.Result {
	if t.IsHibernated() {
		return deploy.Done()
	}
	rtRecords := []v1alpha1.RouteTable{}
	for _, rt := range t.routeTables {
		rtRes := &v1beta1.RouteTable{}
		if err := t.Local.Get(ctx, types.NamespacedName{Name: rt.Name}, rtRes); err != nil {
			return deploy.Panic(err).WithRequeuAfter(defaultRequeuInterval)
		}

		if rtRes.Status.GetCondition(xpv1.TypeReady).Status != corev1.ConditionTrue {
			return deploy.Panic(fmt.Errorf("route table %s not ready", rt.Name)).WithRequeuAfter(defaultRequeuInterval)
		}

		rtRecords = append(rtRecords, v1alpha1.RouteTable{
			RouteTableID: rtRes.Status.AtProvider.RouteTableID,
			Name:         rt.Name,
		})
	}

	rRecords := []v1alpha1.Route{}
	for _, r := range t.routes {
		rRes := &ec2v1alpha1.Route{}
		if err := t.Local.Get(ctx, types.NamespacedName{Name: r.Name}, rRes); err != nil {
			return deploy.Panic(err).WithRequeuAfter(defaultRequeuInterval)
		}
		if rRes.Status.GetCondition(xpv1.TypeReady).Status != corev1.ConditionTrue {
			return deploy.Panic(fmt.Errorf("route %s not ready", r.Name)).WithRequeuAfter(defaultRequeuInterval)
		}
		// if rRes.Status.AtProvider.Return != nil && *rRes.Status.AtProvider.Return == true {
		// 	return deploy.Panic(fmt.Errorf("failed to get route %s id", r.Name)).WithRequeuAfter(time.Second * 30)
		// }
		rRecords = append(rRecords, v1alpha1.Route{
			Name:          r.Name,
			RouteTableRef: rRes.Spec.ForProvider.CustomRouteParameters.RouteTableIDRef.Name,
		})
	}

	t.Internet.Status.RouteTables = rtRecords
	t.Internet.Status.Routes = rRecords
	if err := t.InternetRW.UpdateStatus(ctx, t.Internet); err != nil {
		return deploy.Panic(err).WithRequeuAfter(defaultRequeuInterval)
	}

	return deploy.Done()
}

func (t *Route) Delete(ctx context.Context) deploy.Result {
	name := t.InternetName()
	// TODO(cwen0): use zone config may cause problem.
	zones := t.GetZones()
	for _, zone := range zones {
		if err := t.deleteRouteTable(ctx, namer.PrivateSubnetName(name, zone)); err != nil {
			return deploy.Error(err)
		}
		if err := t.deleteRoute(ctx, namer.PrivateSubnetName(name, zone)); err != nil {
			return deploy.Error(err)
		}
		if v1alpha1.IsOriginatedFromGardener(t.Internet) {
			if err := t.deleteRouteTable(ctx, vpcPublicRouteTableName(t.VPCName())); err != nil {
				return deploy.Error(err)
			}
			if err := t.deleteRoute(ctx, vpcPublicRouteTableName(t.VPCName())); err != nil {
				return deploy.Error(err)
			}
			continue
		}
		if err := t.deleteRouteTable(ctx, namer.PublicSubnetName(name, zone)); err != nil {
			return deploy.Error(err)
		}
		if err := t.deleteRoute(ctx, namer.PublicSubnetName(name, zone)); err != nil {
			return deploy.Error(err)
		}
	}
	return deploy.Done()
}

func (t *Route) deleteRouteTable(ctx context.Context, name string) error {
	rt := &v1beta1.RouteTable{
		ObjectMeta: metav1.ObjectMeta{
			Name: name,
		},
	}
	return k8s.DeleteObject(ctx, t.Local, rt)
}

func (t *Route) deleteRoute(ctx context.Context, name string) error {
	r := &ec2v1alpha1.Route{
		ObjectMeta: metav1.ObjectMeta{
			Name: name,
		},
	}
	return k8s.DeleteObject(ctx, t.Local, r)
}

func (t *Route) PostDelete(ctx context.Context) deploy.Result {
	return deploy.Done()
}

func findSecondarySubnestByZone(ctx context.Context, cli client.Client, subnets []v1alpha1.Subnet, zone string) ([]*v1beta1.Subnet, error) {
	records := lo.Filter(subnets, func(subnet v1alpha1.Subnet, _ int) bool {
		return subnet.AvailabilityZone == zone
	})
	subRes := []*v1beta1.Subnet{}
	for _, record := range records {
		key := types.NamespacedName{
			Name: record.Name,
		}
		sub := &v1beta1.Subnet{}
		if err := cli.Get(ctx, key, sub); err != nil {
			return nil, err
		}
		subRes = append(subRes, sub)
	}
	return subRes, nil
}
