/*
Copyright 2024 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package generic

import (
	"context"
	"errors"
	"fmt"

	admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
	"k8s.io/apimachinery/pkg/api/meta"
	"k8s.io/apimachinery/pkg/runtime/schema"
	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
	"k8s.io/apimachinery/pkg/util/sets"
	"k8s.io/apiserver/pkg/admission"
	"k8s.io/apiserver/pkg/admission/initializer"
	"k8s.io/apiserver/pkg/admission/plugin/policy/matching"
	"k8s.io/apiserver/pkg/authorization/authorizer"
	"k8s.io/client-go/dynamic"
	"k8s.io/client-go/informers"
	"k8s.io/client-go/kubernetes"
)

// H is the Hook type generated by the source and consumed by the dispatcher.
type sourceFactory[H any] func(informers.SharedInformerFactory, kubernetes.Interface, dynamic.Interface, meta.RESTMapper) Source[H]
type dispatcherFactory[H any] func(authorizer.Authorizer, *matching.Matcher) Dispatcher[H]

// admissionResources is the list of resources related to CEL-based admission
// features.
var admissionResources = []schema.GroupResource{
	{Group: admissionregistrationv1.GroupName, Resource: "validatingadmissionpolicies"},
	{Group: admissionregistrationv1.GroupName, Resource: "validatingadmissionpolicybindings"},
	{Group: admissionregistrationv1.GroupName, Resource: "mutatingadmissionpolicies"},
	{Group: admissionregistrationv1.GroupName, Resource: "mutatingadmissionpolicybindings"},
}

// AdmissionPolicyManager is an abstract admission plugin with all the
// infrastructure to define Admit or Validate on-top.
type Plugin[H any] struct {
	*admission.Handler

	sourceFactory     sourceFactory[H]
	dispatcherFactory dispatcherFactory[H]

	source     Source[H]
	dispatcher Dispatcher[H]
	matcher    *matching.Matcher

	informerFactory   informers.SharedInformerFactory
	client            kubernetes.Interface
	restMapper        meta.RESTMapper
	dynamicClient     dynamic.Interface
	excludedResources sets.Set[schema.GroupResource]
	stopCh            <-chan struct{}
	authorizer        authorizer.Authorizer
	enabled           bool
}

var (
	_ initializer.WantsExternalKubeInformerFactory = &Plugin[any]{}
	_ initializer.WantsExternalKubeClientSet       = &Plugin[any]{}
	_ initializer.WantsRESTMapper                  = &Plugin[any]{}
	_ initializer.WantsDynamicClient               = &Plugin[any]{}
	_ initializer.WantsDrainedNotification         = &Plugin[any]{}
	_ initializer.WantsAuthorizer                  = &Plugin[any]{}
	_ initializer.WantsExcludedAdmissionResources  = &Plugin[any]{}
	_ admission.InitializationValidator            = &Plugin[any]{}
)

func NewPlugin[H any](
	handler *admission.Handler,
	sourceFactory sourceFactory[H],
	dispatcherFactory dispatcherFactory[H],
) *Plugin[H] {
	return &Plugin[H]{
		Handler:           handler,
		sourceFactory:     sourceFactory,
		dispatcherFactory: dispatcherFactory,

		// always exclude admission/mutating policies and bindings
		excludedResources: sets.New(admissionResources...),
	}
}

func (c *Plugin[H]) SetExternalKubeInformerFactory(f informers.SharedInformerFactory) {
	c.informerFactory = f
}

func (c *Plugin[H]) SetExternalKubeClientSet(client kubernetes.Interface) {
	c.client = client
}

func (c *Plugin[H]) SetRESTMapper(mapper meta.RESTMapper) {
	c.restMapper = mapper
}

func (c *Plugin[H]) SetDynamicClient(client dynamic.Interface) {
	c.dynamicClient = client
}

func (c *Plugin[H]) SetDrainedNotification(stopCh <-chan struct{}) {
	c.stopCh = stopCh
}

func (c *Plugin[H]) SetAuthorizer(authorizer authorizer.Authorizer) {
	c.authorizer = authorizer
}

func (c *Plugin[H]) SetMatcher(matcher *matching.Matcher) {
	c.matcher = matcher
}

func (c *Plugin[H]) SetEnabled(enabled bool) {
	c.enabled = enabled
}

func (c *Plugin[H]) SetExcludedAdmissionResources(excludedResources []schema.GroupResource) {
	c.excludedResources.Insert(excludedResources...)
}

// ValidateInitialization - once clientset and informer factory are provided, creates and starts the admission controller
func (c *Plugin[H]) ValidateInitialization() error {
	// By default enabled is set to false. It is up to types which embed this
	// struct to set it to true (if feature gate is enabled, or other conditions)
	if !c.enabled {
		return nil
	}
	if c.Handler == nil {
		return errors.New("missing handler")
	}
	if c.informerFactory == nil {
		return errors.New("missing informer factory")
	}
	if c.client == nil {
		return errors.New("missing kubernetes client")
	}
	if c.restMapper == nil {
		return errors.New("missing rest mapper")
	}
	if c.dynamicClient == nil {
		return errors.New("missing dynamic client")
	}
	if c.stopCh == nil {
		return errors.New("missing stop channel")
	}
	if c.authorizer == nil {
		return errors.New("missing authorizer")
	}

	// Use default matcher
	namespaceInformer := c.informerFactory.Core().V1().Namespaces()
	c.matcher = matching.NewMatcher(namespaceInformer.Lister(), c.client)

	if err := c.matcher.ValidateInitialization(); err != nil {
		return err
	}

	c.source = c.sourceFactory(c.informerFactory, c.client, c.dynamicClient, c.restMapper)
	c.dispatcher = c.dispatcherFactory(c.authorizer, c.matcher)

	pluginContext, pluginContextCancel := context.WithCancel(context.Background())
	go func() {
		defer pluginContextCancel()
		<-c.stopCh
	}()

	go func() {
		err := c.source.Run(pluginContext)
		if err != nil && !errors.Is(err, context.Canceled) {
			utilruntime.HandleError(fmt.Errorf("policy source context unexpectedly closed: %v", err))
		}
	}()

	c.SetReadyFunc(func() bool {
		return namespaceInformer.Informer().HasSynced() && c.source.HasSynced()
	})
	return nil
}

func (c *Plugin[H]) Dispatch(
	ctx context.Context,
	a admission.Attributes,
	o admission.ObjectInterfaces,
) (err error) {
	if !c.enabled {
		return nil
	} else if c.shouldIgnoreResource(a) {
		return nil
	} else if !c.WaitForReady() {
		return admission.NewForbidden(a, fmt.Errorf("not yet ready to handle request"))
	}

	return c.dispatcher.Dispatch(ctx, a, o, c.source.Hooks())
}

func (c *Plugin[H]) shouldIgnoreResource(attr admission.Attributes) bool {
	gvr := attr.GetResource()
	// exclusion decision ignores the version.
	gr := gvr.GroupResource()
	return c.excludedResources.Has(gr)
}
