package common

import (
	"context"
	"runtime/debug"
	"time"

	"github.com/crossplane-contrib/provider-gcp/apis/container/v1beta1"
	"github.com/pingcap/errors"
	ctrl "sigs.k8s.io/controller-runtime"

	"github.com/tidbcloud/eks-provider/controllers/cluster/task"
	clusteragent "github.com/tidbcloud/eks-provider/controllers/utils/cluster_agent"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-provider/pkg/deploy"
	"github.com/tidbcloud/infra-provider/pkg/gkeheterogeneous"
	"github.com/tidbcloud/infra-provider/pkg/k8s"
)

const (
	gkeComputeClassCleanupRequeueAfter        = 30 * time.Second
	gkeComputeClassCleanupDeleteMaxAttempts   = 3
	gkeComputeClassCleanupDeleteRetryInterval = 5 * time.Second
)

type GKEComputeClassCleanup struct {
	*task.BaseTask
	sleep func(context.Context, time.Duration) error
}

var _ deploy.Task = &GKEComputeClassCleanup{}

func NewGKEComputeClassCleanup(ctx *task.Context) deploy.Task {
	return &GKEComputeClassCleanup{BaseTask: &task.BaseTask{Context: ctx}}
}

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

func (t *GKEComputeClassCleanup) Create(ctx context.Context) (result deploy.Result) {
	defer t.recoverCreate(&result)

	if err := t.cleanupGKEComputeClassCandidates(ctx); err != nil {
		t.logError(err, "skip gke compute class cleanup after task error")
		if requeue := k8s.ShouldRequeue(err); !requeue.IsZero() {
			return deploy.Done().WithRequeuAfter(requeue.RequeueAfter)
		}
		return deploy.Done()
	}
	return deploy.Done()
}

func (t *GKEComputeClassCleanup) recoverCreate(result *deploy.Result) {
	if recovered := recover(); recovered != nil {
		err := errors.Errorf("panic in GKEComputeClassCleanup.Create: %v", recovered)
		t.logError(err, "panic in GKEComputeClassCleanup.Create", "stack", string(debug.Stack()))
		*result = deploy.Done()
	}
}

func (t *GKEComputeClassCleanup) logError(err error, msg string, keysAndValues ...interface{}) {
	if t == nil || t.BaseTask == nil || t.Context == nil {
		return
	}
	t.Logger.Error(err, msg, keysAndValues...)
}

func (t *GKEComputeClassCleanup) cleanupGKEComputeClassCandidates(ctx context.Context) error {
	if t.Cluster == nil || t.Cluster.GetProviderType() != cloudv1.ProviderTypeGKE || t.Local == nil {
		return nil
	}
	candidates, err := gkeheterogeneous.ComputeClassCleanupCandidates(t.Cluster)
	if err != nil {
		t.Logger.Info("skip malformed gke compute class cleanup candidates", "error", err)
		return nil
	}
	if len(candidates) == 0 {
		return nil
	}

	nodePoolTask := &NodePool{BaseTask: t.BaseTask}
	plan, err := nodePoolTask.buildSharedGKEComputeClassPlan(ctx)
	if err != nil {
		return err
	}
	if plan.peerViewIncomplete {
		return nil
	}
	peers, err := nodePoolTask.listClustersInSharedNodePoolScope(ctx)
	if err != nil {
		return err
	}
	nodePools := &v1beta1.NodePoolList{}
	labelSelector, err := nodePoolTask.apiManagedGKEHeterogeneousNodePoolLabelSelector()
	if err != nil {
		return err
	}
	if err := t.Local.List(ctx, nodePools, labelSelector); err != nil {
		return err
	}

	remainingCandidates := make([]string, 0, len(candidates))
	for i, computeClassName := range candidates {
		intentRefs, err := gkeheterogeneous.ComputeClassIntentRefs(peers, computeClassName)
		if err != nil {
			t.Logger.Info("skip gke compute class cleanup candidate with incomplete intent refs", "computeClass", computeClassName, "error", err)
			if err := gkeheterogeneous.SetComputeClassCleanupCandidates(t.Cluster, append(remainingCandidates, candidates[i:]...)); err != nil {
				return err
			}
			return newGKEComputeClassCleanupRequeueError(errors.Wrapf(err, "inspect intent refs for gke compute class %s", computeClassName))
		}
		if len(intentRefs) > 0 {
			continue
		}
		if refs := gkeheterogeneous.NodePoolComputeClassRefs(nodePools.Items, computeClassName); len(refs) > 0 {
			continue
		}
		if t.EnvProvider == nil {
			if err := gkeheterogeneous.SetComputeClassCleanupCandidates(t.Cluster, append(remainingCandidates, candidates[i:]...)); err != nil {
				return err
			}
			return newGKEComputeClassCleanupRequeueError(errors.Errorf("dataplane provider is unavailable"))
		}
		dataplaneClient := t.EnvProvider.GetClient()
		assignRefs, err := gkeheterogeneous.GatekeeperAssignRefsForComputeClass(ctx, dataplaneClient, computeClassName)
		if err != nil {
			t.Logger.Info("skip gke compute class cleanup candidate because gatekeeper assign refs are unavailable", "computeClass", computeClassName, "error", err)
			if err := gkeheterogeneous.SetComputeClassCleanupCandidates(t.Cluster, append(remainingCandidates, candidates[i:]...)); err != nil {
				return err
			}
			return newGKEComputeClassCleanupRequeueError(errors.Wrapf(err, "list gatekeeper assigns for gke compute class %s", computeClassName))
		}
		if len(assignRefs) > 0 {
			t.Logger.Info(
				"skip gke compute class cleanup candidate because compute class is still referenced by gatekeeper assign",
				"computeClass", computeClassName,
				"refs", gkeheterogeneous.FormatGatekeeperAssignComputeClassRefs(assignRefs),
			)
			continue
		}
		agentClient, agentErr := clusteragent.NewClient(t.Cluster)
		if agentErr != nil {
			t.Logger.Info("skip gke compute class cleanup candidate because cluster-agent is unavailable", "computeClass", computeClassName, "error", agentErr)
			if err := gkeheterogeneous.SetComputeClassCleanupCandidates(t.Cluster, append(remainingCandidates, candidates[i:]...)); err != nil {
				return err
			}
			return newGKEComputeClassCleanupRequeueError(errors.Wrapf(agentErr, "cleanup gke compute class %s", computeClassName))
		}
		if err := t.deleteGKEComputeClassWithRetry(ctx, agentClient, t.Cluster.Name, computeClassName); err != nil {
			t.Logger.Info(
				"drop gke compute class cleanup candidate after delete attempts failed",
				"computeClass", computeClassName,
				"attempts", gkeComputeClassCleanupDeleteMaxAttempts,
				"error", err,
			)
		}
	}
	return gkeheterogeneous.SetComputeClassCleanupCandidates(t.Cluster, remainingCandidates)
}

func (t *GKEComputeClassCleanup) deleteGKEComputeClassWithRetry(ctx context.Context, agentClient clusteragent.Client, clusterName, computeClassName string) error {
	var err error
	for attempt := 1; attempt <= gkeComputeClassCleanupDeleteMaxAttempts; attempt++ {
		err = agentClient.DeleteGKEHeterogeneousComputeClass(ctx, clusterName, computeClassName)
		if err == nil {
			return nil
		}
		if attempt == gkeComputeClassCleanupDeleteMaxAttempts {
			break
		}
		t.Logger.Info(
			"retry gke compute class cleanup after delete attempt failed",
			"computeClass", computeClassName,
			"attempt", attempt,
			"maxAttempts", gkeComputeClassCleanupDeleteMaxAttempts,
			"error", err,
		)
		if sleepErr := t.sleepBeforeGKEComputeClassCleanupDeleteRetry(ctx); sleepErr != nil {
			return errors.Wrapf(sleepErr, "wait before retry cleanup gke compute class %s", computeClassName)
		}
	}
	return err
}

func (t *GKEComputeClassCleanup) sleepBeforeGKEComputeClassCleanupDeleteRetry(ctx context.Context) error {
	if t.sleep != nil {
		return t.sleep(ctx, gkeComputeClassCleanupDeleteRetryInterval)
	}

	timer := time.NewTimer(gkeComputeClassCleanupDeleteRetryInterval)
	defer timer.Stop()
	select {
	case <-ctx.Done():
		return ctx.Err()
	case <-timer.C:
		return nil
	}
}

func newGKEComputeClassCleanupRequeueError(err error) error {
	return k8s.NewRetriableError(err, ctrl.Result{Requeue: true, RequeueAfter: gkeComputeClassCleanupRequeueAfter})
}
