// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

// Package cachecleaner implements a controller for garbage collecting authenticators from an authenticator cache.
package cachecleaner

import (
	"fmt"

	"github.com/go-logr/logr"
	"k8s.io/apimachinery/pkg/labels"
	"k8s.io/klog/v2"

	auth1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/authentication/v1alpha1"
	authinformers "go.pinniped.dev/generated/latest/client/concierge/informers/externalversions/authentication/v1alpha1"
	pinnipedcontroller "go.pinniped.dev/internal/controller"
	"go.pinniped.dev/internal/controller/authenticator"
	"go.pinniped.dev/internal/controller/authenticator/authncache"
	"go.pinniped.dev/internal/controllerlib"
)

// New instantiates a new controllerlib.Controller which will garbage collect authenticators from the provided Cache.
func New(
	cache *authncache.Cache,
	webhooks authinformers.WebhookAuthenticatorInformer,
	jwtAuthenticators authinformers.JWTAuthenticatorInformer,
	log logr.Logger,
) controllerlib.Controller {
	return controllerlib.New(
		controllerlib.Config{
			Name: "cachecleaner-controller",
			Syncer: &controller{
				cache:             cache,
				webhooks:          webhooks,
				jwtAuthenticators: jwtAuthenticators,
				log:               log.WithName("cachecleaner-controller"),
			},
		},
		controllerlib.WithInformer(
			webhooks,
			pinnipedcontroller.MatchAnythingFilter(pinnipedcontroller.SingletonQueue()),
			controllerlib.InformerOption{},
		),
		controllerlib.WithInformer(
			jwtAuthenticators,
			pinnipedcontroller.MatchAnythingFilter(pinnipedcontroller.SingletonQueue()),
			controllerlib.InformerOption{},
		),
	)
}

type controller struct {
	cache             *authncache.Cache
	webhooks          authinformers.WebhookAuthenticatorInformer
	jwtAuthenticators authinformers.JWTAuthenticatorInformer
	log               logr.Logger
}

// Sync implements controllerlib.Syncer.
func (c *controller) Sync(_ controllerlib.Context) error {
	webhooks, err := c.webhooks.Lister().List(labels.Everything())
	if err != nil {
		return fmt.Errorf("failed to list WebhookAuthenticators: %w", err)
	}

	jwtAuthenticators, err := c.jwtAuthenticators.Lister().List(labels.Everything())
	if err != nil {
		return fmt.Errorf("failed to list JWTAuthenticators: %w", err)
	}

	// Index the current authenticators by cache key.
	authenticatorSet := map[authncache.Key]bool{}
	for _, webhook := range webhooks {
		key := authncache.Key{
			Name:     webhook.Name,
			Kind:     "WebhookAuthenticator",
			APIGroup: auth1alpha1.SchemeGroupVersion.Group,
		}
		authenticatorSet[key] = true
	}
	for _, jwtAuthenticator := range jwtAuthenticators {
		key := authncache.Key{
			Name:     jwtAuthenticator.Name,
			Kind:     "JWTAuthenticator",
			APIGroup: auth1alpha1.SchemeGroupVersion.Group,
		}
		authenticatorSet[key] = true
	}

	// Delete any entries from the cache which are no longer in the cluster.
	for _, key := range c.cache.Keys() {
		if key.APIGroup != auth1alpha1.SchemeGroupVersion.Group || (key.Kind != "WebhookAuthenticator" && key.Kind != "JWTAuthenticator") {
			continue
		}
		if _, exists := authenticatorSet[key]; !exists {
			c.log.WithValues(
				"authenticator",
				klog.KRef("", key.Name),
				"kind",
				key.Kind,
			).Info("deleting authenticator from cache")

			value := c.cache.Get(key)
			if closer, ok := value.(authenticator.Closer); ok {
				closer.Close()
			}

			c.cache.Delete(key)
		}
	}
	return nil
}