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

package controller

import (
	"context"
	"fmt"
	"reflect"

	corev1 "k8s.io/api/core/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
	corev1informers "k8s.io/client-go/informers/core/v1"
	corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
	"k8s.io/client-go/tools/events"

	"go.pinniped.dev/internal/controllerlib"
	"go.pinniped.dev/internal/controllerlib/test/integration/examplecontroller/api"
)

func NewExampleUpdatingController(
	services corev1informers.ServiceInformer,
	secrets corev1informers.SecretInformer,
	secretClient corev1client.SecretsGetter,
	recorder events.EventRecorder,
	secretData string,
) controllerlib.Controller {
	serviceLister := services.Lister()
	secretLister := secrets.Lister()

	// note that these functions do not need to be inlined
	// this just demonstrates that for simple Syncer implementations, everything can be in one place

	toServiceName := func(secret *corev1.Secret) (string, bool) {
		serviceName := secret.Annotations[api.ServiceNameAnnotation]
		return serviceName, len(serviceName) != 0
	}

	ensureSecretData := func(service *corev1.Service, secretCopy *corev1.Secret) bool {
		var needsUpdate bool

		expectedData := map[string][]byte{
			api.SecretDataKey: []byte(secretData),
		}
		if !reflect.DeepEqual(secretCopy.Data, expectedData) {
			secretCopy.Data = expectedData
			needsUpdate = true
		}

		expectedOwnerReferences := []metav1.OwnerReference{
			{
				APIVersion: "v1",
				Kind:       "Service",
				Name:       service.Name,
				UID:        service.UID,
			},
		}
		if !reflect.DeepEqual(secretCopy.OwnerReferences, expectedOwnerReferences) {
			secretCopy.OwnerReferences = expectedOwnerReferences
			needsUpdate = true
		}

		return needsUpdate
	}

	isSecretValidForService := func(service *corev1.Service, secret *corev1.Secret) bool {
		if service.Annotations[api.SecretNameAnnotation] != secret.Name {
			return false
		}
		if secret.Annotations[api.ServiceUIDAnnotation] != string(service.UID) {
			return false
		}
		return true
	}

	getServiceForSecret := func(secret *corev1.Secret) (*corev1.Service, error) {
		serviceName, ok := toServiceName(secret)
		if !ok {
			return nil, nil
		}
		service, err := serviceLister.Services(secret.Namespace).Get(serviceName)
		if apierrors.IsNotFound(err) {
			return nil, nil
		}
		if err != nil {
			return nil, fmt.Errorf("unable to get service %s/%s: %w", secret.Namespace, serviceName, err)
		}
		return service, nil
	}

	syncer := controllerlib.SyncFunc(func(ctx controllerlib.Context) error {
		secret, err := secretLister.Secrets(ctx.Key.Namespace).Get(ctx.Key.Name)
		if apierrors.IsNotFound(err) {
			return nil
		}
		if err != nil {
			return fmt.Errorf("unable to get the secret %s/%s: %w", secret.Namespace, secret.Name, err)
		}

		service, err := getServiceForSecret(secret)
		if err != nil || service == nil {
			return err
		}

		if !isSecretValidForService(service, secret) {
			//nolint: goerr113
			utilruntime.HandleError(fmt.Errorf("secret %s/%s does not have corresponding service UID %v", secret.Namespace, secret.Name, service.UID))
			return nil
		}

		// make a copy to avoid mutating cache state
		secretCopy := secret.DeepCopy()

		if needsUpdate := ensureSecretData(service, secretCopy); needsUpdate {
			_, updateErr := secretClient.Secrets(secretCopy.Namespace).Update(context.TODO(), secretCopy, metav1.UpdateOptions{})
			return updateErr
		}

		return nil
	})

	config := controllerlib.Config{
		Name:   "example-controller-updating",
		Syncer: syncer,
	}

	addSecret := func(obj metav1.Object) bool {
		secret := obj.(*corev1.Secret)
		_, ok := toServiceName(secret)
		return ok
	}

	return controllerlib.New(config,
		controllerlib.WithInformer(services, controllerlib.FilterFuncs{}, controllerlib.InformerOption{SkipEvents: true}),

		controllerlib.WithInformer(secrets, controllerlib.FilterFuncs{
			AddFunc: addSecret,
			UpdateFunc: func(oldObj, newObj metav1.Object) bool {
				return addSecret(newObj) || addSecret(oldObj)
			},
		}, controllerlib.InformerOption{}),

		controllerlib.WithRecorder(recorder), // TODO actually use the recorder
	)
}
