package gke

import (
	"context"
	"fmt"
	"time"

	gkev1beta1 "github.com/crossplane-contrib/provider-gcp/apis/container/v1beta1"
	gkev1beta2 "github.com/crossplane-contrib/provider-gcp/apis/container/v1beta2"
	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	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"

	"github.com/tidbcloud/eks-provider/controllers/dumpling/task"
	dutils "github.com/tidbcloud/eks-provider/controllers/dumpling/task/utils"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	compatnamer "github.com/tidbcloud/infra-provider/pkg/compat/namer"
	"github.com/tidbcloud/infra-provider/pkg/consts"
	"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"
)

type NodePool struct {
	*task.BaseTask
}

func NewNodePool(ctx *task.Context) *NodePool {
	return &NodePool{BaseTask: &task.BaseTask{Context: ctx}}
}

func (t *NodePool) Name() string {
	return "NodePool"
}

func (t *NodePool) Create(ctx context.Context) deploy.Result {
	switch phase := t.Dumpling.Status.Phase; phase {
	case "":
		t.BeginAt = util.Pointer(time.Now())
		name := namer.UniqueIdentifier(t.Network.ObjectMeta)

		zones, err := t.Provider.FindRegionZones(t.Network.Spec.Region)
		if err != nil {
			return deploy.Error(err)
		}

		ng := gkev1beta1.NodePool{
			ObjectMeta: metav1.ObjectMeta{
				Name:   t.NodePoolName(),
				Labels: t.Namer.Labels(),
			},
		}
		mut := func() error {
			sa := compatnamer.LegacyBuildGCPNodePoolServiceAccountEmailName(t.Network, t.GKENetworkSpec.ProjectID)
			ng.Spec = gkev1beta1.NodePoolSpec{
				ForProvider: gkev1beta1.NodePoolParameters{
					ClusterRef: &xpv1.Reference{
						Name: util.GetAnnotation(t.Network, consts.AnnotationKeyDataplaneName),
					},
					Autoscaling: &gkev1beta1.NodePoolAutoscaling{
						Enabled:      pointer.Bool(true),
						MaxNodeCount: pointer.Int64(1),
						MinNodeCount: pointer.Int64(0),
					},
					MaxPodsConstraint: &gkev1beta2.MaxPodsConstraint{
						MaxPodsPerNode: int64(32),
					},
					Config: &gkev1beta1.NodeConfig{
						DiskSizeGb:     pointer.Int64(100),
						DiskType:       pointer.String("pd-standard"),
						ImageType:      pointer.String("cos_containerd"),
						Labels:         t.labels(),
						MachineType:    pointer.String("n2-standard-8"),
						ServiceAccount: &sa,
						OauthScopes: []string{
							"https://www.googleapis.com/auth/cloud-platform",
						},
						Taints: t.taints(),
					},
					Locations: zones,
					// node will be created if dumpling pod trigger
					InitialNodeCount: pointer.Int64(0),
					Management: &gkev1beta1.NodeManagementSpec{
						AutoUpgrade: pointer.Bool(false),
						AutoRepair:  pointer.Bool(false),
					},
				},
				ResourceSpec: xpv1.ResourceSpec{
					ProviderConfigReference: &xpv1.Reference{Name: name},
				},
			}
			return nil
		}
		if _, err := controllerutil.CreateOrUpdate(ctx, t.Local, &ng, mut); err != nil {
			t.Dumpling.Status.Phase = cloudv1.DumplingPhaseFailed
			t.Dumpling.Status.Message = fmt.Sprintf("failed apply node pool: %v", err)
			return deploy.Error(fmt.Errorf("failed apply node group: %v", err))
		}
		t.Dumpling.Status.Phase = cloudv1.DumplingPhaseCreatingNodeGroup
	}
	return deploy.Done()
}

func (t *NodePool) PostCreate(ctx context.Context) deploy.Result {
	switch phase := t.Dumpling.Status.Phase; phase {
	case cloudv1.DumplingPhaseCreatingNodeGroup:
		var g gkev1beta1.NodePool
		key := types.NamespacedName{Name: t.NodePoolName()}
		if err := t.Local.Get(ctx, key, &g); err != nil {
			return deploy.Fatal(err)
		}
		status := g.Status.AtProvider.Status
		if status != gkev1beta1.NodePoolStateRunning {
			return deploy.Error(fmt.Errorf("non-active NodePool %s has the state %q", g.Name, status))
		} else {
			t.Dumpling.Status.Phase = cloudv1.DumplingPhaseCreatedNodeGroup
		}
	}
	return deploy.Done()
}

func (t *NodePool) Delete(ctx context.Context) deploy.Result {
	np := &gkev1beta1.NodePool{
		ObjectMeta: metav1.ObjectMeta{
			Name: t.NodePoolName(),
		},
	}
	if err := k8s.DeleteObject(ctx, t.Local, np); err != nil {
		return deploy.Panic(err)
	}
	if err := k8s.GetDeletedObject(ctx, t.Local, types.NamespacedName{Name: np.Name}, np); err != nil {
		return deploy.Panic(err)
	}
	return deploy.Done()
}

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

func (t *NodePool) NodePoolName() string {
	return "tidbcloud-node-" + t.Dumpling.Namespace + "-" + t.Dumpling.Name
}

// labels returns the labels for the node pool, which same as the labels for the eks node group.
// The labels are used to select the nodes for the dumpling pod.
func (t *NodePool) labels() map[string]string {
	l := cloudv1.TagsFromLabels(t.Network.Labels)
	l[dutils.DumplingNodeAffinityKey(t.Dumpling)] = dutils.DumplingNodeAffinityValue(t.Dumpling)
	return l
}

// taints returns the taints for the node pool, which same as the taints for the eks node group.
func (t *NodePool) taints() []*gkev1beta1.NodeTaint {
	return []*gkev1beta1.NodeTaint{
		{
			Key:    "dedicated",
			Value:  fmt.Sprintf("%s-%s", string(cloudv1.ComponentBackup), t.Dumpling.Name),
			Effect: "NO_SCHEDULE",
		},
	}
}
