package eks

import (
	"context"
	"errors"
	"fmt"
	"time"

	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	"github.com/upbound/provider-aws/apis/ec2/v1beta1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/utils/pointer"
	"sigs.k8s.io/controller-runtime/pkg/client"

	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-provider/regional-server/api/v1alpha1"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/controllers"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/controllers/internet/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"
)

const (
	defaultRequeuInterval = time.Second * 30
)

type Route struct {
	*task.BaseTask
}

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 {
	t.BeginAt = util.Pointer(time.Now())
	if t.IsHibernated() {
		return deploy.Done()
	}
	name := t.InternetName()
	tags := t.Internet.Labels
	nodeZones := t.GetNodeZones()
	vpcRes := &v1beta1.VPC{}
	if err := t.Local.Get(ctx, types.NamespacedName{Name: t.VPCName()}, vpcRes); err != nil {
		return deploy.Panic(err).WithRequeuAfter(defaultRequeuInterval)
	}

	for _, zone := range nodeZones {
		if err := t.createPrivateRouteTable(ctx, name, zone, tags, vpcRes); err != nil {
			return deploy.Panic(err).WithRequeuAfter(defaultRequeuInterval)
		}

		if err := t.createPublicRouteTable(ctx, zone, tags, vpcRes); err != nil {
			return deploy.Panic(err).WithRequeuAfter(defaultRequeuInterval)
		}
	}

	return deploy.Done()
}

func (t *Route) createPrivateRouteTable(
	ctx context.Context,
	internetName, zone string,
	tags map[string]string,
	vpc *v1beta1.VPC,
) error {
	privateSubnet, err := findPrivateSubnetByZone(ctx, t.Local, t.VPC, zone)
	if err != nil {
		return err
	}
	nodeSubnet, err := findNodeSubnetByZone(ctx, t.Local, t.VPC, zone)
	if err != nil {
		return err
	}
	t.Logger.Info("find private subnets", "privateSubnet", privateSubnet.Name, "nodeSubnet", nodeSubnet.Name)

	natName := namer.PublicSubnetName(internetName, zone)
	nat := v1alpha1.FindNATByName(t.Internet.Status.NATs, natName)
	if nat == nil {
		return fmt.Errorf("NAT %s not found", natName)
	}
	natRes := &v1beta1.NATGateway{}
	if err := t.Local.Get(ctx, types.NamespacedName{Name: nat.Name}, natRes); err != nil {
		return err
	}

	routeTable, associations, route := t.buildPrivateRouteTable(zone, tags, privateSubnet, nodeSubnet, vpc, natRes)
	objects := make([]client.Object, 0, 2+len(associations))
	objects = append(objects, routeTable)
	for _, association := range associations {
		objects = append(objects, association)
	}
	objects = append(objects, route)

	for _, object := range objects {
		if err := k8s.Apply(ctx, t.Local, object); err != nil {
			return err
		}
	}
	return nil
}

func (t *Route) createPublicRouteTable(
	ctx context.Context,
	zone string,
	tags map[string]string,
	vpc *v1beta1.VPC,
) error {
	publicSubnet, err := findPublicSubnetByZone(ctx, t.Local, t.VPC, zone)
	if err != nil {
		return err
	}
	t.Logger.Info("find public subnets", "publicSubnet", publicSubnet.Name)

	routeTable, association, route := t.buildPublicRouteTable(zone, tags, publicSubnet, vpc)
	objects := []client.Object{routeTable, association, route}
	for _, object := range objects {
		if err := k8s.Apply(ctx, t.Local, object); err != nil {
			return err
		}
	}
	return nil
}

func (t *Route) buildPrivateRouteTable(
	zone string,
	tags map[string]string,
	privateSubnet *v1beta1.Subnet,
	nodeSubnet *v1beta1.Subnet,
	vpc *v1beta1.VPC,
	nat *v1beta1.NATGateway,
) (*v1beta1.RouteTable, []*v1beta1.RouteTableAssociation, *v1beta1.Route) {
	name := namer.NodesSubnetName(t.InternetName(), zone)
	routeTags := controllers.ConvertToCloudTags(tags, cloudv1.ProviderTypeEKS)
	routeTags = util.MergeMaps(routeTags, t.Config.AdditionalCloudTags)
	routeTags = util.BuildAWSTagWithName(routeTags, name)
	labels := util.MergeMaps(t.Internet.Labels, map[string]string{v1alpha1.InternetNameKey: t.Internet.Name})
	rt := &v1beta1.RouteTable{
		ObjectMeta: metav1.ObjectMeta{
			Name:   name,
			Labels: labels,
		},
		Spec: v1beta1.RouteTableSpec{
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{Name: t.ProviderConfigName()},
				ManagementPolicies:      controllers.GetResourceManagementPolicies(),
			},
			ForProvider: v1beta1.RouteTableParameters_2{
				Region:   pointer.String(t.Internet.Spec.Region),
				Tags:     util.ToMapPointer(routeTags),
				VPCIDRef: &xpv1.Reference{Name: vpc.Name},
			},
		},
	}

	associations := []*v1beta1.RouteTableAssociation{
		{
			ObjectMeta: metav1.ObjectMeta{
				Name:   fmt.Sprintf("%s-private", name),
				Labels: labels,
			},
			Spec: v1beta1.RouteTableAssociationSpec{
				ResourceSpec: xpv1.ResourceSpec{
					ProviderConfigReference: &xpv1.Reference{Name: t.ProviderConfigName()},
					ManagementPolicies:      controllers.GetResourceManagementPolicies(),
				},
				ForProvider: v1beta1.RouteTableAssociationParameters{
					Region:          pointer.String(t.Internet.Spec.Region),
					RouteTableIDRef: &xpv1.Reference{Name: rt.Name},
					SubnetIDRef:     &xpv1.Reference{Name: privateSubnet.Name},
				},
			},
		},
		{
			ObjectMeta: metav1.ObjectMeta{
				Name:   fmt.Sprintf("%s-node", name),
				Labels: labels,
			},
			Spec: v1beta1.RouteTableAssociationSpec{
				ResourceSpec: xpv1.ResourceSpec{
					ProviderConfigReference: &xpv1.Reference{Name: t.ProviderConfigName()},
					ManagementPolicies:      controllers.GetResourceManagementPolicies(),
				},
				ForProvider: v1beta1.RouteTableAssociationParameters{
					Region:          pointer.String(t.Internet.Spec.Region),
					RouteTableIDRef: &xpv1.Reference{Name: rt.Name},
					SubnetIDRef:     &xpv1.Reference{Name: nodeSubnet.Name},
				},
			},
		},
	}

	route := &v1beta1.Route{
		ObjectMeta: metav1.ObjectMeta{
			Name:   name,
			Labels: labels,
		},
		Spec: v1beta1.RouteSpec{
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{Name: t.ProviderConfigName()},
				ManagementPolicies:      controllers.GetResourceManagementPolicies(),
			},
			ForProvider: v1beta1.RouteParameters_2{
				Region:               pointer.String(t.Internet.Spec.Region),
				DestinationCidrBlock: pointer.String("0.0.0.0/0"),
				NATGatewayIDRef:      &xpv1.Reference{Name: nat.Name},
				RouteTableIDRef:      &xpv1.Reference{Name: rt.Name},
			},
		},
	}

	return rt, associations, route
}

func (t *Route) buildPublicRouteTable(
	zone string,
	tags map[string]string,
	publicSubnet *v1beta1.Subnet,
	vpc *v1beta1.VPC,
) (*v1beta1.RouteTable, *v1beta1.RouteTableAssociation, *v1beta1.Route) {
	name := namer.PublicSubnetName(t.InternetName(), zone)
	routeTags := controllers.ConvertToCloudTags(tags, cloudv1.ProviderTypeEKS)
	routeTags = util.MergeMaps(routeTags, t.Config.AdditionalCloudTags)
	routeTags = util.BuildAWSTagWithName(routeTags, name)
	labels := util.MergeMaps(t.Internet.Labels, map[string]string{v1alpha1.InternetNameKey: t.Internet.Name})
	rt := &v1beta1.RouteTable{
		ObjectMeta: metav1.ObjectMeta{
			Name:   name,
			Labels: labels,
		},
		Spec: v1beta1.RouteTableSpec{
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{Name: t.ProviderConfigName()},
				ManagementPolicies:      controllers.GetResourceManagementPolicies(),
			},
			ForProvider: v1beta1.RouteTableParameters_2{
				Region:   pointer.String(t.Internet.Spec.Region),
				Tags:     util.ToMapPointer(routeTags),
				VPCIDRef: &xpv1.Reference{Name: vpc.Name},
			},
		},
	}

	association := &v1beta1.RouteTableAssociation{
		ObjectMeta: metav1.ObjectMeta{
			Name:   fmt.Sprintf("%s-public", name),
			Labels: labels,
		},
		Spec: v1beta1.RouteTableAssociationSpec{
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{Name: t.ProviderConfigName()},
				ManagementPolicies:      controllers.GetResourceManagementPolicies(),
			},
			ForProvider: v1beta1.RouteTableAssociationParameters{
				Region:          pointer.String(t.Internet.Spec.Region),
				RouteTableIDRef: &xpv1.Reference{Name: rt.Name},
				SubnetIDRef:     &xpv1.Reference{Name: publicSubnet.Name},
			},
		},
	}

	route := &v1beta1.Route{
		ObjectMeta: metav1.ObjectMeta{
			Name:   name,
			Labels: labels,
		},
		Spec: v1beta1.RouteSpec{
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{Name: t.ProviderConfigName()},
				ManagementPolicies:      controllers.GetResourceManagementPolicies(),
			},
			ForProvider: v1beta1.RouteParameters_2{
				Region:               pointer.String(t.Internet.Spec.Region),
				DestinationCidrBlock: pointer.String("0.0.0.0/0"),
				GatewayIDRef:         &xpv1.Reference{Name: t.Internet.Status.InternetGateway.Name},
				RouteTableIDRef:      &xpv1.Reference{Name: rt.Name},
			},
		},
	}

	return rt, association, route
}

func (t *Route) PostCreate(ctx context.Context) deploy.Result {
	if t.IsHibernated() {
		return deploy.Done()
	}
	routeTables, routeTableAssociations, routes, err := t.getRouteTables(ctx)
	if err != nil {
		return deploy.Panic(err).WithRequeuAfter(defaultRequeuInterval)
	}
	if len(routeTables) == 0 {
		return deploy.Panic(errors.New("route tables not found")).WithRequeuAfter(defaultRequeuInterval)
	}
	if len(routeTableAssociations) == 0 {
		return deploy.Panic(errors.New("route table associations not found")).WithRequeuAfter(defaultRequeuInterval)
	}
	if len(routes) == 0 {
		return deploy.Panic(errors.New("routes not found")).WithRequeuAfter(defaultRequeuInterval)
	}

	rtRecords := make([]v1alpha1.RouteTable, 0, len(routeTables))
	for _, rt := range routeTables {
		if !controllers.IsResourceReady(rt.Status.ResourceStatus) {
			return deploy.Panic(fmt.Errorf("route table %s not ready", rt.Name)).WithRequeuAfter(defaultRequeuInterval)
		}

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

	for _, rta := range routeTableAssociations {
		if !controllers.IsResourceReady(rta.Status.ResourceStatus) {
			return deploy.Panic(fmt.Errorf("route table association %s not ready", rta.Name)).WithRequeuAfter(defaultRequeuInterval)
		}
	}

	rRecords := make([]v1alpha1.Route, 0, len(routes))
	for _, r := range routes {
		if !controllers.IsResourceReady(r.Status.ResourceStatus) {
			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: r.Spec.ForProvider.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) getRouteTables(ctx context.Context) ([]v1beta1.RouteTable, []v1beta1.RouteTableAssociation, []v1beta1.Route, error) {
	labelOpt := client.MatchingLabels{
		v1alpha1.InternetNameKey: t.Internet.Name,
	}

	rtList := &v1beta1.RouteTableList{}
	if err := t.Local.List(ctx, rtList, labelOpt); err != nil {
		return nil, nil, nil, err
	}
	rtaList := &v1beta1.RouteTableAssociationList{}
	if err := t.Local.List(ctx, rtaList, labelOpt); err != nil {
		return nil, nil, nil, err
	}
	rList := &v1beta1.RouteList{}
	if err := t.Local.List(ctx, rList, labelOpt); err != nil {
		return nil, nil, nil, err
	}

	return rtList.Items, rtaList.Items, rList.Items, nil
}

func (t *Route) Delete(ctx context.Context) deploy.Result {
	routeTables, routeTableAssociations, routes, err := t.getRouteTables(ctx)
	if err != nil {
		return deploy.Error(err)
	}

	for _, r := range routes {
		r.Spec.ResourceSpec.ManagementPolicies = xpv1.ManagementPolicies{xpv1.ManagementActionAll}
		if err := k8s.PatchObject(ctx, t.Local, &r, controllers.ManagementPoliciesPatch); err != nil {
			return deploy.Error(err)
		}
		if err := k8s.DeleteObject(ctx, t.Local, &r); err != nil {
			return deploy.Error(err)
		}
	}
	for _, association := range routeTableAssociations {
		association.Spec.ResourceSpec.ManagementPolicies = xpv1.ManagementPolicies{xpv1.ManagementActionAll}
		if err := k8s.PatchObject(ctx, t.Local, &association, controllers.ManagementPoliciesPatch); err != nil {
			return deploy.Error(err)
		}
		if err := k8s.DeleteObject(ctx, t.Local, &association); err != nil {
			return deploy.Error(err)
		}
	}
	for _, rt := range routeTables {
		rt.Spec.ResourceSpec.ManagementPolicies = xpv1.ManagementPolicies{xpv1.ManagementActionAll}
		if err := k8s.PatchObject(ctx, t.Local, &rt, controllers.ManagementPoliciesPatch); err != nil {
			return deploy.Error(err)
		}
		if err := k8s.DeleteObject(ctx, t.Local, &rt); err != nil {
			return deploy.Error(err)
		}
	}
	return deploy.Done()
}

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