package ack

import (
	"context"
	"errors"
	"fmt"
	"maps"
	"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/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/util"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/utils/ptr"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

const (
	routeTableAssociateType     = "VSwitch"
	routeNextHopTypeNAT         = "NatGateway"
	routeNextHopTypeIpv4Gateway = "Ipv4Gateway"
)

type routeTask struct {
	*task.BaseTask
}

func NewRoute(c *task.Context) deploy.Task {
	return &routeTask{BaseTask: &task.BaseTask{Context: c}}
}

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

func (t *routeTask) Create(ctx context.Context) deploy.Result {
	t.BeginAt = util.Pointer(time.Now())
	tags := controllers.ConvertToCloudTags(t.Internet.Labels, consts.ProviderTypeACK)
	tags = util.MergeMaps(tags, t.Config.AdditionalCloudTags)

	if err := t.createPrivateRouteTable(ctx, tags); err != nil {
		return deploy.Panic(fmt.Errorf("create private route table error: %w", err)).WithRequeuAfter(time.Second * 30)
	}
	if err := t.createPublicRouteTable(ctx, tags); err != nil {
		return deploy.Panic(fmt.Errorf("create public route table error: %w", err)).WithRequeuAfter(time.Second * 30)
	}

	return deploy.Done()
}

func (t *routeTask) createPrivateRouteTable(ctx context.Context, tags map[string]string) error {
	rtName := t.getPrivateRouteTableName()
	rtTags := make(map[string]string, len(tags))
	maps.Copy(rtTags, tags)
	rt, err := t.createRouteTable(ctx, rtName, rtTags)
	if err != nil {
		return fmt.Errorf("create route table error: %w", err)
	}

	if err := t.createPrivateRoute(ctx, rt); err != nil {
		return fmt.Errorf("create private route entry error: %w", err)
	}

	for _, subnet := range t.VPC.Status.NodeSubnets {
		if err := t.createRouteTableAttachment(ctx, rt, &subnet); err != nil {
			return fmt.Errorf("create route table attachment for subnet %s error: %w", subnet.Name, err)
		}
	}
	return nil
}

func (t *routeTask) createPublicRouteTable(ctx context.Context, tags map[string]string) error {
	rtName := t.getPublicRouteTableName()
	rtTags := make(map[string]string, len(tags))
	maps.Copy(rtTags, tags)
	rt, err := t.createRouteTable(ctx, rtName, rtTags)
	if err != nil {
		return fmt.Errorf("create route table error: %w", err)
	}

	if err := t.createPublicRoute(ctx, rt); err != nil {
		return fmt.Errorf("create public route entry error: %w", err)
	}

	for _, subnet := range t.VPC.Status.PublicSubnets {
		if err := t.createRouteTableAttachment(ctx, rt, &subnet); err != nil {
			return fmt.Errorf("create route table attachment for subnet %s error: %w", subnet.Name, err)
		}
	}
	return nil
}

func (t *routeTask) createRouteTable(ctx context.Context, name string, tags map[string]string) (*alivpcv1alpha1.RouteTable, error) {
	labels := util.MergeMaps(t.Internet.Labels, map[string]string{v1alpha1.InternetNameKey: t.Internet.Name})
	rt := &alivpcv1alpha1.RouteTable{
		ObjectMeta: metav1.ObjectMeta{
			Name:   name,
			Labels: labels,
		},
		Spec: alivpcv1alpha1.RouteTableSpec{
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{Name: t.ProviderConfigName()},
				ManagementPolicies:      controllers.GetResourceManagementPolicies(),
			},
			ForProvider: alivpcv1alpha1.RouteTableParameters{
				RouteTableName: ptr.To(name),
				Region:         ptr.To(t.Internet.Spec.Region),
				VPCIDRef: &xpv1.Reference{
					Name: t.VPCName(),
				},
				AssociateType: ptr.To(routeTableAssociateType),
				Tags:          util.ToMapPointer(tags),
			},
		},
	}
	if err := k8s.Apply(ctx, t.Local, rt); err != nil {
		return nil, err
	}
	return rt, nil
}

func (t *routeTask) createPrivateRoute(ctx context.Context, rt *alivpcv1alpha1.RouteTable) error {
	if len(t.Internet.Status.NATs) == 0 {
		return errors.New("no nat gateway found")
	}
	nat := t.Internet.Status.NATs[0]
	routeName := t.getPrivateRouteName(rt.Name)
	labels := util.MergeMaps(t.Internet.Labels, map[string]string{v1alpha1.InternetNameKey: t.Internet.Name})
	routeEntry := &alivpcv1alpha1.RouteEntry{
		ObjectMeta: metav1.ObjectMeta{
			Name:   routeName,
			Labels: labels,
		},
		Spec: alivpcv1alpha1.RouteEntrySpec{
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{Name: t.ProviderConfigName()},
				ManagementPolicies:      controllers.GetResourceManagementPolicies(),
			},
			ForProvider: alivpcv1alpha1.RouteEntryParameters{
				Name:   ptr.To(routeName),
				Region: ptr.To(t.Internet.Spec.Region),
				RouteTableIDRef: &xpv1.Reference{
					Name: rt.Name,
				},
				NexthopType:          ptr.To(routeNextHopTypeNAT),
				NexthopID:            ptr.To(nat.NATGatewayID),
				DestinationCidrblock: ptr.To("0.0.0.0/0"),
			},
		},
	}
	return k8s.Apply(ctx, t.Local, routeEntry)
}

func (t *routeTask) createPublicRoute(ctx context.Context, rt *alivpcv1alpha1.RouteTable) error {
	if t.Internet.Status.InternetGateway.InternetGatewayID == "" {
		return errors.New("no internet gateway found")
	}

	ig := t.Internet.Status.InternetGateway
	routeName := t.getPublicRouteName(rt.Name)
	labels := util.MergeMaps(t.Internet.Labels, map[string]string{v1alpha1.InternetNameKey: t.Internet.Name})
	routeEntry := &alivpcv1alpha1.RouteEntry{
		ObjectMeta: metav1.ObjectMeta{
			Name:   routeName,
			Labels: labels,
		},
		Spec: alivpcv1alpha1.RouteEntrySpec{
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{Name: t.ProviderConfigName()},
				ManagementPolicies:      controllers.GetResourceManagementPolicies(),
			},
			ForProvider: alivpcv1alpha1.RouteEntryParameters{
				Name:   ptr.To(routeName),
				Region: ptr.To(t.Internet.Spec.Region),
				RouteTableIDRef: &xpv1.Reference{
					Name: rt.Name,
				},
				NexthopType:          ptr.To(routeNextHopTypeIpv4Gateway),
				NexthopID:            ptr.To(ig.InternetGatewayID),
				DestinationCidrblock: ptr.To("0.0.0.0/0"),
			},
		},
	}
	return k8s.Apply(ctx, t.Local, routeEntry)
}

func (t *routeTask) createRouteTableAttachment(ctx context.Context, rt *alivpcv1alpha1.RouteTable, subnet *v1alpha1.Subnet) error {
	rtaName := t.getRouteTableAttachmentName(rt.Name, subnet.AvailabilityZone)
	labels := util.MergeMaps(t.Internet.Labels, map[string]string{v1alpha1.InternetNameKey: t.Internet.Name})
	rta := &alivpcv1alpha1.RouteTableAttachment{
		ObjectMeta: metav1.ObjectMeta{
			Name:   rtaName,
			Labels: labels,
		},
		Spec: alivpcv1alpha1.RouteTableAttachmentSpec{
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{Name: t.ProviderConfigName()},
				ManagementPolicies:      controllers.GetResourceManagementPolicies(),
			},
			ForProvider: alivpcv1alpha1.RouteTableAttachmentParameters{
				Region: ptr.To(t.Internet.Spec.Region),
				RouteTableIDRef: &xpv1.Reference{
					Name: rt.Name,
				},
				VswitchIDRef: &xpv1.Reference{
					Name: subnet.Name,
				},
			},
		},
	}
	return k8s.Apply(ctx, t.Local, rta)
}

func (t *routeTask) PostCreate(ctx context.Context) deploy.Result {
	routeTables, routes, routeTableAttachments, err := t.listRouteTables(ctx)
	if err != nil {
		return deploy.Panic(err).WithRequeuAfter(time.Second * 30)
	}
	if len(routeTables) == 0 {
		return deploy.Panic(errors.New("route tables not found")).WithRequeuAfter(time.Second * 30)
	}
	if len(routes) == 0 {
		return deploy.Panic(errors.New("routes not found")).WithRequeuAfter(time.Second * 30)
	}
	if len(routeTableAttachments) == 0 {
		return deploy.Panic(errors.New("route table attachments not found")).WithRequeuAfter(time.Second * 30)
	}

	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(time.Second * 30)
		}

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

	rRecords := make([]v1alpha1.Route, 0, len(routes))
	for _, r := range routes {
		if !controllers.IsResourceReady(r.Status.ResourceStatus) {
			return deploy.Panic(fmt.Errorf("route entry %s not ready", r.Name)).WithRequeuAfter(time.Second * 30)
		}

		rRecords = append(rRecords, v1alpha1.Route{
			Name:          r.Name,
			RouteTableRef: r.Spec.ForProvider.RouteTableIDRef.Name,
		})
	}

	for _, rta := range routeTableAttachments {
		if !controllers.IsResourceReady(rta.Status.ResourceStatus) {
			return deploy.Panic(fmt.Errorf("route table attachment %s not ready", rta.Name)).WithRequeuAfter(time.Second * 30)
		}
	}

	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(time.Second * 30)
	}

	return deploy.Done()
}

func (t *routeTask) listRouteTables(ctx context.Context) ([]alivpcv1alpha1.RouteTable, []alivpcv1alpha1.RouteEntry, []alivpcv1alpha1.RouteTableAttachment, error) {
	labelOpt := client.MatchingLabels{
		v1alpha1.InternetNameKey: t.Internet.Name,
	}

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

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

func (t *routeTask) Delete(ctx context.Context) deploy.Result {
	routeTables, routes, routeTableAttachments, err := t.listRouteTables(ctx)
	if err != nil {
		return deploy.Panic(err).WithRequeuAfter(time.Second * 30)
	}

	for i := range routes {
		if err := controllers.DeleteUpboundResource(ctx, t.Local, &routes[i]); err != nil {
			return deploy.Panic(fmt.Errorf("delete route %s error: %w", routes[i].Name, err)).WithRequeuAfter(time.Second * 30)
		}
	}
	for i := range routeTableAttachments {
		if err := controllers.DeleteUpboundResource(ctx, t.Local, &routeTableAttachments[i]); err != nil {
			return deploy.Panic(fmt.Errorf("delete route table attachment %s error: %w", routeTableAttachments[i].Name, err)).WithRequeuAfter(time.Second * 30)
		}
	}
	for i := range routeTables {
		if err := controllers.DeleteUpboundResource(ctx, t.Local, &routeTables[i]); err != nil {
			return deploy.Panic(fmt.Errorf("delete route table %s error: %w", routeTables[i].Name, err)).WithRequeuAfter(time.Second * 30)
		}
	}

	return deploy.Done()
}

func (t *routeTask) PostDelete(ctx context.Context) deploy.Result {
	routeTables, routes, routeTableAttachments, err := t.listRouteTables(ctx)
	if err != nil {
		return deploy.Error(err).WithRequeuAfter(time.Second * 30)
	}
	if len(routeTables) > 0 {
		return deploy.Error(errors.New("route tables still exist")).WithRequeuAfter(time.Second * 30)
	}
	if len(routes) > 0 {
		return deploy.Error(errors.New("route entries still exist")).WithRequeuAfter(time.Second * 30)
	}
	if len(routeTableAttachments) > 0 {
		return deploy.Error(errors.New("route table attachments still exist")).WithRequeuAfter(time.Second * 30)
	}
	return deploy.Done()
}

func (t *routeTask) getPrivateRouteTableName() string {
	return t.InternetName() + "-nodes"
}

func (t *routeTask) getPublicRouteTableName() string {
	return t.InternetName() + "-public"
}

func (t *routeTask) getRouteTableAttachmentName(routeTableName, zone string) string {
	return fmt.Sprintf("%s-%s", routeTableName, zone)
}

func (t *routeTask) getPrivateRouteName(routeTableName string) string {
	return fmt.Sprintf("%s-nat", routeTableName)
}

func (t *routeTask) getPublicRouteName(routeTableName string) string {
	return fmt.Sprintf("%s-ipv4gateway", routeTableName)
}
