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

// Package ldapupstreamwatcher implements a controller which watches LDAPIdentityProviders.
package ldapupstreamwatcher

import (
	"context"
	"fmt"

	"k8s.io/apimachinery/pkg/api/equality"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/labels"
	corev1informers "k8s.io/client-go/informers/core/v1"
	"k8s.io/klog/v2/klogr"

	"go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1"
	pinnipedclientset "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned"
	idpinformers "go.pinniped.dev/generated/latest/client/supervisor/informers/externalversions/idp/v1alpha1"
	pinnipedcontroller "go.pinniped.dev/internal/controller"
	"go.pinniped.dev/internal/controller/conditionsutil"
	"go.pinniped.dev/internal/controller/supervisorconfig/upstreamwatchers"
	"go.pinniped.dev/internal/controllerlib"
	"go.pinniped.dev/internal/oidc/provider"
	"go.pinniped.dev/internal/upstreamldap"
)

const (
	ldapControllerName = "ldap-upstream-observer"
)

type ldapUpstreamGenericLDAPImpl struct {
	ldapIdentityProvider v1alpha1.LDAPIdentityProvider
}

func (g *ldapUpstreamGenericLDAPImpl) Spec() upstreamwatchers.UpstreamGenericLDAPSpec {
	return &ldapUpstreamGenericLDAPSpec{g.ldapIdentityProvider}
}

func (g *ldapUpstreamGenericLDAPImpl) Namespace() string {
	return g.ldapIdentityProvider.Namespace
}

func (g *ldapUpstreamGenericLDAPImpl) Name() string {
	return g.ldapIdentityProvider.Name
}

func (g *ldapUpstreamGenericLDAPImpl) Generation() int64 {
	return g.ldapIdentityProvider.Generation
}

func (g *ldapUpstreamGenericLDAPImpl) Status() upstreamwatchers.UpstreamGenericLDAPStatus {
	return &ldapUpstreamGenericLDAPStatus{g.ldapIdentityProvider}
}

type ldapUpstreamGenericLDAPSpec struct {
	ldapIdentityProvider v1alpha1.LDAPIdentityProvider
}

func (s *ldapUpstreamGenericLDAPSpec) Host() string {
	return s.ldapIdentityProvider.Spec.Host
}

func (s *ldapUpstreamGenericLDAPSpec) TLSSpec() *v1alpha1.TLSSpec {
	return s.ldapIdentityProvider.Spec.TLS
}

func (s *ldapUpstreamGenericLDAPSpec) BindSecretName() string {
	return s.ldapIdentityProvider.Spec.Bind.SecretName
}

func (s *ldapUpstreamGenericLDAPSpec) UserSearch() upstreamwatchers.UpstreamGenericLDAPUserSearch {
	return &ldapUpstreamGenericLDAPUserSearch{s.ldapIdentityProvider.Spec.UserSearch}
}

func (s *ldapUpstreamGenericLDAPSpec) GroupSearch() upstreamwatchers.UpstreamGenericLDAPGroupSearch {
	return &ldapUpstreamGenericLDAPGroupSearch{s.ldapIdentityProvider.Spec.GroupSearch}
}

func (s *ldapUpstreamGenericLDAPSpec) DetectAndSetSearchBase(_ context.Context, config *upstreamldap.ProviderConfig) *v1alpha1.Condition {
	config.GroupSearch.Base = s.ldapIdentityProvider.Spec.GroupSearch.Base
	config.UserSearch.Base = s.ldapIdentityProvider.Spec.UserSearch.Base
	return nil
}

type ldapUpstreamGenericLDAPUserSearch struct {
	userSearch v1alpha1.LDAPIdentityProviderUserSearch
}

func (u *ldapUpstreamGenericLDAPUserSearch) Base() string {
	return u.userSearch.Base
}

func (u *ldapUpstreamGenericLDAPUserSearch) Filter() string {
	return u.userSearch.Filter
}

func (u *ldapUpstreamGenericLDAPUserSearch) UsernameAttribute() string {
	return u.userSearch.Attributes.Username
}

func (u *ldapUpstreamGenericLDAPUserSearch) UIDAttribute() string {
	return u.userSearch.Attributes.UID
}

type ldapUpstreamGenericLDAPGroupSearch struct {
	groupSearch v1alpha1.LDAPIdentityProviderGroupSearch
}

func (g *ldapUpstreamGenericLDAPGroupSearch) Base() string {
	return g.groupSearch.Base
}

func (g *ldapUpstreamGenericLDAPGroupSearch) Filter() string {
	return g.groupSearch.Filter
}

func (g *ldapUpstreamGenericLDAPGroupSearch) GroupNameAttribute() string {
	return g.groupSearch.Attributes.GroupName
}

type ldapUpstreamGenericLDAPStatus struct {
	ldapIdentityProvider v1alpha1.LDAPIdentityProvider
}

func (s *ldapUpstreamGenericLDAPStatus) Conditions() []v1alpha1.Condition {
	return s.ldapIdentityProvider.Status.Conditions
}

// UpstreamLDAPIdentityProviderICache is a thread safe cache that holds a list of validated upstream LDAP IDP configurations.
type UpstreamLDAPIdentityProviderICache interface {
	SetLDAPIdentityProviders([]provider.UpstreamLDAPIdentityProviderI)
}

type ldapWatcherController struct {
	cache                        UpstreamLDAPIdentityProviderICache
	validatedSecretVersionsCache upstreamwatchers.SecretVersionCacheI
	ldapDialer                   upstreamldap.LDAPDialer
	client                       pinnipedclientset.Interface
	ldapIdentityProviderInformer idpinformers.LDAPIdentityProviderInformer
	secretInformer               corev1informers.SecretInformer
}

// New instantiates a new controllerlib.Controller which will populate the provided UpstreamLDAPIdentityProviderICache.
func New(
	idpCache UpstreamLDAPIdentityProviderICache,
	client pinnipedclientset.Interface,
	ldapIdentityProviderInformer idpinformers.LDAPIdentityProviderInformer,
	secretInformer corev1informers.SecretInformer,
	withInformer pinnipedcontroller.WithInformerOptionFunc,
) controllerlib.Controller {
	return newInternal(
		idpCache,
		// start with an empty secretVersionCache
		upstreamwatchers.NewSecretVersionCache(),
		// nil means to use a real production dialer when creating objects to add to the cache
		nil,
		client,
		ldapIdentityProviderInformer,
		secretInformer,
		withInformer,
	)
}

// For test dependency injection purposes.
func newInternal(
	idpCache UpstreamLDAPIdentityProviderICache,
	validatedSecretVersionsCache upstreamwatchers.SecretVersionCacheI,
	ldapDialer upstreamldap.LDAPDialer,
	client pinnipedclientset.Interface,
	ldapIdentityProviderInformer idpinformers.LDAPIdentityProviderInformer,
	secretInformer corev1informers.SecretInformer,
	withInformer pinnipedcontroller.WithInformerOptionFunc,
) controllerlib.Controller {
	c := ldapWatcherController{
		cache:                        idpCache,
		validatedSecretVersionsCache: validatedSecretVersionsCache,
		ldapDialer:                   ldapDialer,
		client:                       client,
		ldapIdentityProviderInformer: ldapIdentityProviderInformer,
		secretInformer:               secretInformer,
	}
	return controllerlib.New(
		controllerlib.Config{Name: ldapControllerName, Syncer: &c},
		withInformer(
			ldapIdentityProviderInformer,
			pinnipedcontroller.MatchAnythingFilter(pinnipedcontroller.SingletonQueue()),
			controllerlib.InformerOption{},
		),
		withInformer(
			secretInformer,
			pinnipedcontroller.MatchAnySecretOfTypeFilter(upstreamwatchers.LDAPBindAccountSecretType, pinnipedcontroller.SingletonQueue()),
			controllerlib.InformerOption{},
		),
	)
}

// Sync implements controllerlib.Syncer.
func (c *ldapWatcherController) Sync(ctx controllerlib.Context) error {
	actualUpstreams, err := c.ldapIdentityProviderInformer.Lister().List(labels.Everything())
	if err != nil {
		return fmt.Errorf("failed to list LDAPIdentityProviders: %w", err)
	}

	requeue := false
	validatedUpstreams := make([]provider.UpstreamLDAPIdentityProviderI, 0, len(actualUpstreams))
	for _, upstream := range actualUpstreams {
		valid, requestedRequeue := c.validateUpstream(ctx.Context, upstream)
		if valid != nil {
			validatedUpstreams = append(validatedUpstreams, valid)
		}
		if requestedRequeue {
			requeue = true
		}
	}

	c.cache.SetLDAPIdentityProviders(validatedUpstreams)

	if requeue {
		return controllerlib.ErrSyntheticRequeue
	}
	return nil
}

func (c *ldapWatcherController) validateUpstream(ctx context.Context, upstream *v1alpha1.LDAPIdentityProvider) (p provider.UpstreamLDAPIdentityProviderI, requeue bool) {
	spec := upstream.Spec

	config := &upstreamldap.ProviderConfig{
		Name:        upstream.Name,
		ResourceUID: upstream.UID,
		Host:        spec.Host,
		UserSearch: upstreamldap.UserSearchConfig{
			Base:              spec.UserSearch.Base,
			Filter:            spec.UserSearch.Filter,
			UsernameAttribute: spec.UserSearch.Attributes.Username,
			UIDAttribute:      spec.UserSearch.Attributes.UID,
		},
		GroupSearch: upstreamldap.GroupSearchConfig{
			Base:               spec.GroupSearch.Base,
			Filter:             spec.GroupSearch.Filter,
			GroupNameAttribute: spec.GroupSearch.Attributes.GroupName,
		},
		Dialer: c.ldapDialer,
	}

	conditions := upstreamwatchers.ValidateGenericLDAP(ctx, &ldapUpstreamGenericLDAPImpl{*upstream}, c.secretInformer, c.validatedSecretVersionsCache, config)

	c.updateStatus(ctx, upstream, conditions.Conditions())

	return upstreamwatchers.EvaluateConditions(conditions, config)
}

func (c *ldapWatcherController) updateStatus(ctx context.Context, upstream *v1alpha1.LDAPIdentityProvider, conditions []*v1alpha1.Condition) {
	log := klogr.New().WithValues("namespace", upstream.Namespace, "name", upstream.Name)
	updated := upstream.DeepCopy()

	hadErrorCondition := conditionsutil.Merge(conditions, upstream.Generation, &updated.Status.Conditions, log)

	updated.Status.Phase = v1alpha1.LDAPPhaseReady
	if hadErrorCondition {
		updated.Status.Phase = v1alpha1.LDAPPhaseError
	}

	if equality.Semantic.DeepEqual(upstream, updated) {
		return // nothing to update
	}

	_, err := c.client.
		IDPV1alpha1().
		LDAPIdentityProviders(upstream.Namespace).
		UpdateStatus(ctx, updated, metav1.UpdateOptions{})
	if err != nil {
		log.Error(err, "failed to update status")
	}
}