package gke

import (
	"context"
	"fmt"
	"time"

	"github.com/aws/smithy-go/ptr"
	gkev1beta1 "github.com/crossplane-contrib/provider-gcp/apis/container/v1beta1"
	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"

	"github.com/tidbcloud/eks-provider/api/v1alpha1"
	"github.com/tidbcloud/eks-provider/controllers/dm/task"
	"github.com/tidbcloud/eks-provider/controllers/utils"
	"github.com/tidbcloud/infra-provider/pkg/deploy"
	"github.com/tidbcloud/infra-provider/pkg/k8s"
	"github.com/tidbcloud/infra-provider/pkg/util"
	"github.com/tidbcloud/infra-provider/util/collections"
)

const (
	// this value is meaningless for now, nodes are created when pod triggers node pool scale out
	// the number of nodes will be the same as the number our pods(1 pod on 1 node)
	// keep this if we need to support share node-pool among jobs later.
	// same value as tidb-cluster, for 3 zone, we can have 300 nodes at most.
	maxNodeCountPerZone = 100

	nodePoolNamePrefix = "dm"
)

// BaseNodePool manage lifecycle node pool for gke.
// Note: during test we found the node pool won't scale-in after dataplane-master pods is deleted
// the node pool keeps scale(in or out), but the node count won't become 0. but for tidb node pool
// using nearly same config, it DOES scale-in, not sure why. here is the diff:
// - our node version: 1.23.14-gke.1800, tidb node version: 1.24.9-gke.3200
// - tidb node pool Location policy: Balanced, our node pool don't have it.
// if we want to share node pool by spec code, need to fix this.
type BaseNodePool struct {
	*task.BaseTask
	dmSpec     *v1alpha1.DMSpec
	nodeLabels map[string]string
	taskName   string
}

func NewBaseNodePool(ctx *task.Context,
	taskName string,
	dmSpec *v1alpha1.DMSpec,
	nodeLabels map[string]string) *BaseNodePool {
	return &BaseNodePool{
		BaseTask:   &task.BaseTask{Context: ctx},
		dmSpec:     dmSpec,
		nodeLabels: nodeLabels,
		taskName:   taskName,
	}
}

func (t *BaseNodePool) Name() string {
	return t.taskName
}

func (t *BaseNodePool) Create(ctx context.Context) deploy.Result {

	zones := t.Zones()
	providerConfigName := t.GetProviderConfigName()
	taints := t.getSpecTaints()
	np := &gkev1beta1.NodePool{
		ObjectMeta: metav1.ObjectMeta{
			Name: t.getNodePoolName(),
		},
	}
	labels := t.nodeLabels
	sa, err := getGCPNodePoolServiceAccountEmail(t.BaseTask)
	if err != nil {
		return deploy.Error(err)
	}
	mutateFunc := func() error {
		np.Labels = collections.MergeMaps(np.Labels, labels)
		size, err := util.ParseDiskSizeAsGiB(t.dmSpec.SystemVolumeSize)
		if err != nil {
			return err
		}
		np.Spec = gkev1beta1.NodePoolSpec{
			ForProvider: gkev1beta1.NodePoolParameters{
				Autoscaling: &gkev1beta1.NodePoolAutoscaling{
					Enabled:      ptr.Bool(true),
					MaxNodeCount: ptr.Int64(maxNodeCountPerZone),
					MinNodeCount: ptr.Int64(0),
				},
				Config: &gkev1beta1.NodeConfig{
					DiskSizeGb:     ptr.Int64(size),
					DiskType:       ptr.String(utils.GetGcpNodepoolDiskType(t.dmSpec.InstanceType.Name)),
					ImageType:      ptr.String("cos_containerd"),
					Labels:         labels,
					MachineType:    &t.dmSpec.InstanceType.Name,
					ServiceAccount: &sa,
					OauthScopes: []string{
						"https://www.googleapis.com/auth/cloud-platform",
					},
					Taints: taints,
					// WorkloadMetadataConfig: &gkev1beta1.WorkloadMetadataConfig{
					// 	Mode: "GCE_METADATA",
					// },
				},
				Locations: zones,
				Management: &gkev1beta1.NodeManagementSpec{
					// Do it manually
					AutoUpgrade: ptr.Bool(false),
					AutoRepair:  ptr.Bool(false),
				},
				// same as tidb cluster, we don't create nodes on creation of node pool
				// let pod trigger node pool scale out
				InitialNodeCount: ptr.Int64(0),
				Cluster:          t.buildCrossPlaneBaseNodePoolClusterName(),
			},
			ResourceSpec: xpv1.ResourceSpec{ProviderConfigReference: &xpv1.Reference{Name: providerConfigName}},
		}
		return nil
	}

	if _, err := controllerutil.CreateOrUpdate(ctx, t.Local, np, mutateFunc); err != nil {
		return deploy.Error(err)
	}
	return deploy.Done()
}

func (t *BaseNodePool) PostCreate(ctx context.Context) deploy.Result {

	var g gkev1beta1.NodePool
	nodePoolName := t.getNodePoolName()
	ready := false
	if err := t.Local.Get(ctx, types.NamespacedName{Name: nodePoolName}, &g); err == nil {
		ready = g.Status.AtProvider.Status == gkev1beta1.NodePoolStateRunning
	} else {
		t.Logger.Error(err, "fail to get node pool "+nodePoolName)
	}

	if ready {
		return deploy.Done()
	}
	return deploy.Error(fmt.Errorf("node pool not ready: %s/%s", nodePoolName, g.Status.AtProvider.Status)).WithRequeuAfter(time.Second * 20)
}

func (t *BaseNodePool) Delete(ctx context.Context) deploy.Result {
	nodePool := &gkev1beta1.NodePool{
		ObjectMeta: metav1.ObjectMeta{
			Name: t.getNodePoolName(),
		},
	}
	return deploy.ErrorOrDone(k8s.DeleteObject(ctx, t.Local, nodePool))
}

func (t *BaseNodePool) PostDelete(ctx context.Context) deploy.Result {

	nodePool := &gkev1beta1.NodePool{
		ObjectMeta: metav1.ObjectMeta{
			Name: t.getNodePoolName(),
		},
	}
	return deploy.ErrorOrDone(k8s.DeleteObjectCompleted(ctx, t.Local, nodePool))
}

func (t *BaseNodePool) getNodePoolName() string {
	return getNodePoolName(t.BaseTask, t.dmSpec)
}

func (t *BaseNodePool) getSpecTaints() []*gkev1beta1.NodeTaint {

	var taints []*gkev1beta1.NodeTaint
	taints = append(taints, &gkev1beta1.NodeTaint{
		Key:    "dedicated",
		Value:  "tidb-" + task.ComponentName,
		Effect: "NO_SCHEDULE",
	})
	// taints = append(taints, taintToNodeTaint(utils.TaintWith3LayerArch()))
	return taints
}

func (t *BaseNodePool) buildCrossPlaneBaseNodePoolClusterName() string {
	return fmt.Sprintf("projects/%s/locations/%s/clusters/%s",
		t.EnvProvider.GetAccount(),
		t.GetRegion(),
		t.Kubernetes.Name)
}
