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"

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

const (
	DefaultInstanceType = "n2-standard-2"
	DefaultNodeMaxSize  = 10
	DefaultNodeMinSize  = 1
)

type NodePool struct {
	*task.BaseTask
	gcpCli gcp.Client
}

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

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

func (t *NodePool) Create(ctx context.Context) deploy.Result {
	t.BeginAt = util.Pointer(time.Now())
	if t.APPBox.Labels[v1alpha1.IsLegacyKey] == "true" {
		return deploy.Done()
	}

	ng := gkev1beta1.NodePool{
		ObjectMeta: metav1.ObjectMeta{
			Name:   t.NodePoolName(),
			Labels: t.Namer.Labels(),
		},
	}

	adminNodeConfig := t.APPBox.Spec.KubernetesConfig.AdminNodeConfig
	instanceType := DefaultInstanceType
	if adminNodeConfig.InstanceType != "" {
		instanceType = adminNodeConfig.InstanceType
	}
	nodeMaxSize := DefaultNodeMaxSize
	if adminNodeConfig.MaxSize != nil {
		nodeMaxSize = int(*adminNodeConfig.MaxSize)
	}
	nodeMinSize := DefaultNodeMinSize
	if adminNodeConfig.MinSize != nil {
		nodeMinSize = int(*adminNodeConfig.MinSize)
	}

	desiredNodePoolLocations, err := t.nodePoolLocations()
	if err != nil {
		return deploy.Error(err)
	}
	mut := func() error {
		sa := t.APPBox.Status.IAM.ServiceAccountEmail
		locations := desiredNodePoolLocations
		if ng.GetResourceVersion() != "" {
			locations = append([]string(nil), ng.Spec.ForProvider.Locations...)
		}
		ng.Spec = gkev1beta1.NodePoolSpec{
			ForProvider: gkev1beta1.NodePoolParameters{
				ClusterRef: &xpv1.Reference{
					Name: t.Cluster.GetName(),
				},
				Autoscaling: &gkev1beta1.NodePoolAutoscaling{
					Enabled:      pointer.Bool(true),
					MaxNodeCount: pointer.Int64(int64(nodeMaxSize)),
					MinNodeCount: pointer.Int64(int64(nodeMinSize)),
				},
				MaxPodsConstraint: &gkev1beta2.MaxPodsConstraint{
					MaxPodsPerNode: task.GKEAdminMaxPodsPerNode,
				},
				Config: &gkev1beta1.NodeConfig{
					DiskSizeGb:     pointer.Int64(task.GKEAdminNodeDiskSizeGB),
					DiskType:       pointer.String("pd-standard"),
					ImageType:      pointer.String("cos_containerd"),
					Labels:         t.labels(),
					MachineType:    pointer.String(instanceType),
					ServiceAccount: &sa,
					OauthScopes: []string{
						"https://www.googleapis.com/auth/cloud-platform",
					},
				},
				Locations:        locations,
				InitialNodeCount: pointer.Int64(1),
				Management: &gkev1beta1.NodeManagementSpec{
					// TODO: make it enabled, currently it will block the deletion
					AutoRepair: pointer.Bool(false),
				},
			},
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{Name: t.ProviderConfigName()},
			},
		}
		return nil
	}
	_, err = controllerutil.CreateOrUpdate(ctx, t.Local, &ng, mut)
	return deploy.ErrorOrDone(err)
}

func (t *NodePool) NodePoolName() string {
	if name := t.APPBox.Annotations[v1alpha1.AnnotationKeyOwnCrossplaneNameNodePool]; name != "" {
		return name
	}
	return namer.UniqueIdentifier(
		t.APPBox.ObjectMeta,
		namer.WithPrefix(consts.TiDBCloudPrefix),
		namer.WithLimit(task.GKENameLimit), // GKE node pool name has limit of 40, see https://github.com/tidbcloud/infra-provider/issues/793
	)
}

func (t *NodePool) nodePoolLocations() ([]string, error) {
	cluster, ok := t.Cluster.(task.GKEClusterIdentifier)
	if !ok {
		return nil, fmt.Errorf("cluster identifier is not a GKE cluster")
	}

	zone := stableZone(cluster.GetLocations(), t.APPBox.Namespace+"/"+t.APPBox.Name+"/"+t.NodePoolName())
	if zone == "" {
		return nil, nil
	}
	return []string{zone}, nil
}

func (t *NodePool) labels() map[string]string {
	l := cloudv1.TagsFromLabels(t.APPBox.Labels)

	// NodePool type
	l[consts.LabelKeyNodeGroupType] = consts.LabelValNodeGroupTypeControlPlane

	return l
}

func (t *NodePool) PostCreate(ctx context.Context) deploy.Result {
	var pool gkev1beta1.NodePool
	key := types.NamespacedName{Name: t.NodePoolName()}
	if err := t.Local.Get(ctx, key, &pool); err != nil {
		return deploy.Fatal(err)
	}
	status := pool.Status.AtProvider.Status
	if status != gkev1beta1.NodePoolStateRunning {
		// remove create pending annotation to trigger reconcile
		if err := t.deleteCreatePendinggAnnotation(ctx, &pool); err != nil {
			return deploy.Error(err)
		}
		return deploy.Error(fmt.Errorf("non-active NodePool %s has the state %q", pool.Name, status))
	}
	return deploy.Done()
}

func (t *NodePool) Delete(ctx context.Context) deploy.Result {
	// Delete admin nodepools
	return deploy.PanicOrDone(t.deleteAdminNodeResources(ctx))
}

func (t *NodePool) deleteAdminNodeResources(ctx context.Context) error {
	np := &gkev1beta1.NodePool{
		ObjectMeta: metav1.ObjectMeta{
			Name: t.NodePoolName(),
		},
	}
	if err := k8s.DeleteObject(ctx, t.Local, np); err != nil {
		return err
	}

	return k8s.GetDeletedObject(ctx, t.Local, types.NamespacedName{Name: np.Name}, np)
}

func (t *NodePool) deleteCreatePendinggAnnotation(ctx context.Context, pool *gkev1beta1.NodePool) error {
	if err := k8s.PatchObject(ctx, t.Local, pool, k8s.RemoveCrossplaneExternalCreatePending, k8s.ReconcileTrigger); err != nil {
		return err
	}
	return nil
}

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