package clusterconfig

import (
	"context"
	"time"

	"github.com/go-logr/logr"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/fields"
	"k8s.io/apimachinery/pkg/util/wait"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"

	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
)

type GarbageCollector struct {
	Log logr.Logger
	client.Client
	period            time.Duration
	configRevisionTTL time.Duration
	nsPrefix          string
}

func NewGarbageCollector(mgr ctrl.Manager, logger logr.Logger, period time.Duration, ttl time.Duration, nsPrefix string) *GarbageCollector {
	return &GarbageCollector{
		Log:               logger,
		Client:            mgr.GetClient(),
		period:            period,
		configRevisionTTL: ttl,
		nsPrefix:          nsPrefix,
	}
}

func (c *GarbageCollector) Start(ctx context.Context) error {
	wait.UntilWithContext(ctx, c.performGC, c.period)
	c.Log.Info("Shutdown signal received, stopping gc worker")
	return nil
}

func (c *GarbageCollector) NeedLeaderElection() bool {
	return true
}

func (c *GarbageCollector) SetupWithManager(mgr ctrl.Manager) error {
	return mgr.Add(c)
}

func (c *GarbageCollector) performGC(ctx context.Context) {
	c.Log.Info("Performing garbage collection")
	crs := &cloudv1.ConfigRevisionList{}
	err := c.Client.List(ctx, crs, &client.ListOptions{})
	if err != nil {
		c.Log.Error(err, "Failed to list ConfigRevisions")
		return
	}
	// TODO: maybe parallel
	for _, cr := range crs.Items {
		logger := c.Log.WithValues("cr", cr.Name)

		cs := &cloudv1.ClusterList{}
		if err = c.Client.List(context.TODO(), cs, &client.ListOptions{
			FieldSelector: fields.OneTermEqualSelector(ConfigRevisionRefField, cr.Name),
		}); err != nil {
			continue
		}
		activeAt, err := time.Parse(time.RFC3339, cr.Annotations[cloudv1.ConfigRevisionActiveAnnotation])
		if err != nil {
			logger.Info("Failed to parse active time in annotation", "error", err)
			// consider timeout if activeAt not set
			activeAt = time.Now().Add(-c.configRevisionTTL)
		}

		// get the owner cluster
		clusterFound, err := c.isOwnerClusterFound(ctx, &cr)
		if err != nil {
			continue
		}

		// delete this cr when it meets all the following conditions:
		// 1. reference count is 0
		// 2. the owner cluster is not found
		// 3. the time since last update is greater than the TTL, delete the CR
		if len(cs.Items) == 0 && !clusterFound {
			if activeAt.Add(c.configRevisionTTL).Before(time.Now()) {
				logger.Info("Deleting config revision")
				if err := c.Client.Delete(context.TODO(), &cr); err != nil {
					logger.Error(err, "Failed to delete config revision")
					continue
				}
			}
		} else { // refresh active time only when don't meet condition 1 or condition 2 above
			logger.Info("Refresh active time")
			cr.Annotations[cloudv1.ConfigRevisionActiveAnnotation] = time.Now().Format(time.RFC3339)
			if err := c.Client.Update(context.TODO(), &cr); err != nil {
				logger.Error(err, "Failed to update config revision")
				continue
			}
		}
	}
}
func (c *GarbageCollector) isOwnerClusterFound(ctx context.Context, cr *cloudv1.ConfigRevision) (bool, error) {
	clusterName := cr.ClusterName()
	project := cr.Project()
	if clusterName == "" || project == "" {
		return false, nil
	}

	key := client.ObjectKey{Namespace: cloudv1.BuildClusterNamespace(c.nsPrefix, project), Name: clusterName}
	if err := c.Client.Get(ctx, key, &cloudv1.Cluster{}); err != nil {
		if apierrors.IsNotFound(err) {
			return false, nil
		}
		c.Log.Error(err, "get cluster")
		return false, err
	}
	return true, nil
}
