// Copyright 2020-2023 Project Capsule Authors.
// SPDX-License-Identifier: Apache-2.0

package node

import (
	"context"
	"reflect"

	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/util/version"
	"k8s.io/client-go/tools/record"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"

	"github.com/projectcapsule/capsule/pkg/configuration"
	capsulewebhook "github.com/projectcapsule/capsule/pkg/webhook"
	"github.com/projectcapsule/capsule/pkg/webhook/utils"
)

type userMetadataHandler struct {
	configuration configuration.Configuration
	version       *version.Version
}

func UserMetadataHandler(configuration configuration.Configuration, ver *version.Version) capsulewebhook.Handler {
	return &userMetadataHandler{
		configuration: configuration,
		version:       ver,
	}
}

func (r *userMetadataHandler) OnCreate(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func {
	return func(context.Context, admission.Request) *admission.Response {
		return nil
	}
}

func (r *userMetadataHandler) OnDelete(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func {
	return func(context.Context, admission.Request) *admission.Response {
		return nil
	}
}

func (r *userMetadataHandler) getForbiddenNodeLabels(node *corev1.Node) map[string]string {
	forbiddenNodeLabels := make(map[string]string)

	forbiddenLabels := r.configuration.ForbiddenUserNodeLabels()

	for label, value := range node.GetLabels() {
		var forbidden, matched bool
		forbidden = forbiddenLabels.ExactMatch(label)
		matched = forbiddenLabels.RegexMatch(label)

		if forbidden || matched {
			forbiddenNodeLabels[label] = value
		}
	}

	return forbiddenNodeLabels
}

func (r *userMetadataHandler) getForbiddenNodeAnnotations(node *corev1.Node) map[string]string {
	forbiddenNodeAnnotations := make(map[string]string)

	forbiddenAnnotations := r.configuration.ForbiddenUserNodeAnnotations()

	for annotation, value := range node.GetAnnotations() {
		var forbidden, matched bool
		forbidden = forbiddenAnnotations.ExactMatch(annotation)
		matched = forbiddenAnnotations.RegexMatch(annotation)

		if forbidden || matched {
			forbiddenNodeAnnotations[annotation] = value
		}
	}

	return forbiddenNodeAnnotations
}

func (r *userMetadataHandler) OnUpdate(_ client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
	return func(_ context.Context, req admission.Request) *admission.Response {
		nodeWebhookSupported, _ := utils.NodeWebhookSupported(r.version)

		if !nodeWebhookSupported {
			return nil
		}

		oldNode := &corev1.Node{}
		if err := decoder.DecodeRaw(req.OldObject, oldNode); err != nil {
			return utils.ErroredResponse(err)
		}

		newNode := &corev1.Node{}
		if err := decoder.Decode(req, newNode); err != nil {
			return utils.ErroredResponse(err)
		}

		if r.configuration.ForbiddenUserNodeLabels() != nil {
			oldNodeForbiddenLabels := r.getForbiddenNodeLabels(oldNode)
			newNodeForbiddenLabels := r.getForbiddenNodeLabels(newNode)

			if !reflect.DeepEqual(oldNodeForbiddenLabels, newNodeForbiddenLabels) {
				recorder.Eventf(newNode, corev1.EventTypeWarning, "ForbiddenNodeLabel", "Denied modifying forbidden labels on node")

				response := admission.Denied(NewNodeLabelForbiddenError(r.configuration.ForbiddenUserNodeLabels()).Error())

				return &response
			}
		}

		if r.configuration.ForbiddenUserNodeAnnotations() != nil {
			oldNodeForbiddenAnnotations := r.getForbiddenNodeAnnotations(oldNode)
			newNodeForbiddenAnnotations := r.getForbiddenNodeAnnotations(newNode)

			if !reflect.DeepEqual(oldNodeForbiddenAnnotations, newNodeForbiddenAnnotations) {
				recorder.Eventf(newNode, corev1.EventTypeWarning, "ForbiddenNodeLabel", "Denied modifying forbidden annotations on node")

				response := admission.Denied(NewNodeAnnotationForbiddenError(r.configuration.ForbiddenUserNodeAnnotations()).Error())

				return &response
			}
		}

		return nil
	}
}
