package eks

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

	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	"github.com/crossplane/crossplane-runtime/pkg/meta"
	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/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"
	"github.com/upbound/provider-aws/apis/ec2/v1beta1"
	"sigs.k8s.io/controller-runtime/pkg/client"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/utils/pointer"
)

type VPC struct {
	*task.BaseTask
}

func NewVPC(ctx *task.Context) *VPC {
	return &VPC{&task.BaseTask{Context: ctx}}
}

// Name implements Task interface.
func (t *VPC) Name() string {
	return "VPC"
}

func (t *VPC) getVPCName() string {
	return controllers.UniqueIdentifier(t.VPC.ObjectMeta, namer.WithPrefix(t.Config.ResourceNamePrefix))
}

func (t *VPC) Create(ctx context.Context) deploy.Result {
	t.BeginAt = util.Pointer(time.Now())
	name := t.getVPCName()
	tags := controllers.ConvertToCloudTags(t.VPC.Labels, cloudv1.ProviderTypeEKS)
	tags = util.BuildAWSTagWithName(tags, name)
	tags = util.MergeMaps(tags, t.Config.AdditionalCloudTags)

	labels := util.MergeMaps(t.Namer.Labels(), t.VPC.Labels)
	labels[v1alpha1.VPCNameKey] = name
	vpc := v1beta1.VPC{
		ObjectMeta: metav1.ObjectMeta{
			Name:   name,
			Labels: labels,
		},
		Spec: v1beta1.VPCSpec{
			ForProvider: v1beta1.VPCParameters_2{
				Region:             pointer.String(t.VPC.Spec.Region),
				CidrBlock:          pointer.String(t.VPC.Spec.CIDR),
				EnableDNSSupport:   pointer.Bool(true),
				EnableDNSHostnames: pointer.Bool(true),
				Tags:               util.ToMapPointer(tags),
			},
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{Name: t.ProviderConfigName()},
				ManagementPolicies:      controllers.GetResourceManagementPolicies(),
			},
		},
	}

	if err := k8s.Apply(ctx, t.Local, &vpc); err != nil {
		return deploy.Panic(err).WithRequeuAfter(time.Second * 30)
	}

	return deploy.Done()
}

func (t *VPC) PostCreate(ctx context.Context) deploy.Result {
	name := t.getVPCName()
	vpc := &v1beta1.VPC{}

	if err := t.Local.Get(ctx, types.NamespacedName{Name: name}, vpc); err != nil {
		return deploy.Panic(err).WithRequeuAfter(time.Second * 30)
	}

	if !controllers.IsResourceReady(vpc.Status.ResourceStatus) {
		return deploy.Panic(errors.New("vpc is not ready")).WithRequeuAfter(time.Second * 30)
	}

	if err := t.setVPCStatus(ctx, vpc); err != nil {
		return deploy.Panic(err).WithRequeuAfter(time.Second * 30)
	}

	return deploy.Done()
}

func (t *VPC) Delete(ctx context.Context) deploy.Result {
	name := t.getVPCName()
	// TODO: check if need to delete vpc endpoints
	vpc := &v1beta1.VPC{
		ObjectMeta: metav1.ObjectMeta{
			Name: name,
		},
	}

	if err := t.Local.Get(ctx, client.ObjectKeyFromObject(vpc), vpc); client.IgnoreNotFound(err) != nil {
		return deploy.Error(err)
	}

	vpc.Spec.ResourceSpec.ManagementPolicies = xpv1.ManagementPolicies{xpv1.ManagementActionAll}

	if err := k8s.PatchObject(ctx, t.Local, vpc, controllers.ManagementPoliciesPatch); err != nil {
		return deploy.Error(err)
	}

	if err := k8s.DeleteObject(ctx, t.Local, vpc); err != nil {
		return deploy.Error(err)
	}

	return deploy.Done()
}

func (t *VPC) PostDelete(ctx context.Context) deploy.Result {
	return deploy.ErrorOrDone(k8s.GetDeletedObject(ctx, t.Local, types.NamespacedName{Name: controllers.UniqueIdentifier(t.VPC.ObjectMeta, namer.WithPrefix(t.Config.ResourceNamePrefix))}, &v1beta1.VPC{}))
}

func (t *VPC) setVPCStatus(ctx context.Context, vpcRes *v1beta1.VPC) error {
	vpcID := meta.GetExternalName(vpcRes)
	t.VPC.Status.VPC = v1alpha1.VPCRes{
		VPCID: vpcID,
		Name:  vpcRes.Name,
	}
	// this update won't trigger reconcile, and t.VPC is updated as well.
	return t.VPCRW.UpdateStatus(ctx, t.VPC)
}

func eksClusterNameTagKey(clusterName string) string {
	return fmt.Sprintf("kubernetes.io/cluster/%s", clusterName)
}
