package gke

import (
	"context"
	"fmt"
	"regexp"
	"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"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	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/network/task"
	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"
)

var (
	// InstanceGroup URL: https://www.googleapis.com/compute/{version}/projects/{projectName}/zones/{zone}/instanceGroupManagers/{instanceGroupName}
	instanceGroupUrlRe = regexp.MustCompile(`^https://www.googleapis.com/compute/(\S+)/projects/(\S+)/zones/(\S+)/instanceGroupManagers/(\S+)$`)
)

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.Cluster == nil {
		return deploy.Fatal(fmt.Errorf("empty cluster"))
	}
	name := namer.UniqueIdentifier(t.Network.ObjectMeta)

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

	mut := func() error {
		sa := namer.LegacyBuildGCPNodePoolServiceAccountEmailName(t.Network, t.GKENetworkSpec.ProjectID)
		ng.Spec = gkev1beta1.NodePoolSpec{
			// TODO: current we let the following task can be passed, we will optimize the configuration later.
			ForProvider: gkev1beta1.NodePoolParameters{
				ClusterRef: &xpv1.Reference{
					Name: t.Cluster.GetName(),
				},
				Autoscaling: &gkev1beta1.NodePoolAutoscaling{
					Enabled:      pointer.Bool(true),
					MaxNodeCount: pointer.Int64(10),
					MinNodeCount: pointer.Int64(0),
				},
				MaxPodsConstraint: &gkev1beta2.MaxPodsConstraint{
					MaxPodsPerNode: task.GKEAdminMaxPodsPerNode,
				},
				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-2"),
					ServiceAccount: &sa,
					OauthScopes: []string{
						"https://www.googleapis.com/auth/cloud-platform",
					},
				},
				// TODO: investigate how to keep shared node pool in single az.
				Locations:        []string{fmt.Sprintf("%s-a", t.Network.Spec.Region)},
				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: name},
			},
		}

		return nil
	}
	_, err := controllerutil.CreateOrUpdate(ctx, t.Local, &ng, mut)
	return deploy.ErrorOrDone(err)
}

func (t *NodePool) NodePoolName() string {
	return namer.UniqueIdentifier(
		t.Network.ObjectMeta,
		namer.WithPrefix(consts.TiDBCloudPrefix),
		namer.WithLimit(40), // GKE node pool name has limit of 40, see https://github.com/tidbcloud/infra-provider/issues/793
	)
}

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

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

	return l
}

func (t *NodePool) PostCreate(ctx context.Context) deploy.Result {
	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))
	}
	return deploy.Done()
}

// Delete all nodepools, including control plane and data plane
func (t *NodePool) Delete(ctx context.Context) deploy.Result {
	// Delete control plane
	if err := t.deleteControlPlane(ctx); err != nil {
		return deploy.Panic(err)
	}

	// Delete data plane
	if err := t.deleteDataPlane(ctx); err != nil {
		return deploy.Panic(err).WithRequeuAfter(20 * time.Second)
	}

	return deploy.Done()
}

func (t *NodePool) deleteControlPlane(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) deleteDataPlane(ctx context.Context) error {
	nps := &gkev1beta1.NodePoolList{}
	if err := t.Local.List(ctx, nps, t.Namer.LegacyNetworkSelector()); err != nil {
		if apierrors.IsNotFound(err) {
			return nil
		}
		return err
	}
	for _, np := range nps.Items {
		for _, url := range np.Status.AtProvider.InstanceGroupUrls {
			strs := instanceGroupUrlRe.FindStringSubmatch(url)
			if len(strs) != 5 {
				return fmt.Errorf("parse instance group url error, find %d submatch strings", len(strs))
			}
			size, err := t.gcpCli.GetInstanceGroupSize(ctx, strs[2], strs[3], strs[4])
			if err != nil {
				return err
			}
			if size > int64(0) {
				return fmt.Errorf("not empty nodepool, nodepool: %s, instancegroup: %s, size: %d", np.Name, url, size)
			}
		}

		t.Logger.Info("delete empty nodepool", "name", np.Name)
		if err := t.Local.Delete(ctx,
			&np,
		); err != nil {
			return err
		}
	}
	if len(nps.Items) > 0 {
		return fmt.Errorf("nodepools not deleted, remain %d", len(nps.Items))
	}

	return nil
}

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