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

package validation

import (
	"context"
	"maps"

	"github.com/pkg/errors"
	corev1 "k8s.io/api/core/v1"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"

	capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
	"github.com/projectcapsule/capsule/pkg/api"
	ad "github.com/projectcapsule/capsule/pkg/runtime/admission"
	"github.com/projectcapsule/capsule/pkg/runtime/events"
	"github.com/projectcapsule/capsule/pkg/runtime/handlers"
	"github.com/projectcapsule/capsule/pkg/tenant"
	"github.com/projectcapsule/capsule/pkg/users"
)

type userMetadataHandler struct{}

func UserMetadataHandler() handlers.TypedHandlerWithTenantUser[*corev1.Namespace] {
	return &userMetadataHandler{}
}

func (h *userMetadataHandler) OnCreate(
	_ client.Client,
	_ client.Reader,
	user users.AdmissionUser,
	ns *corev1.Namespace,
	_ admission.Decoder,
	recorder events.EventRecorder,
	tnt *capsulev1beta2.Tenant,
) handlers.Func {
	return func(ctx context.Context, req admission.Request) *admission.Response {
		if tnt.Spec.NamespaceOptions != nil && user.IsCapsule() {
			labels, annotations, err := userMetadataForValidation(ns, nil, tnt)
			if err != nil {
				return ad.ErroredResponse(err)
			}

			if response := validateUserMetadata(
				ctx,
				req,
				tnt,
				ns,
				labels,
				annotations,
				tnt.Spec.NamespaceOptions,
				recorder,
			); response != nil {
				return response
			}
		}

		return nil
	}
}

func (h *userMetadataHandler) OnUpdate(
	_ client.Client,
	_ client.Reader,
	user users.AdmissionUser,
	newNs *corev1.Namespace,
	oldNs *corev1.Namespace,
	_ admission.Decoder,
	recorder events.EventRecorder,
	tnt *capsulev1beta2.Tenant,
) handlers.Func {
	return func(ctx context.Context, req admission.Request) *admission.Response {
		if len(tnt.Spec.NodeSelector) > 0 {
			v, ok := newNs.GetAnnotations()["scheduler.alpha.kubernetes.io/node-selector"]
			if !ok {
				msg := "the annotation scheduler.alpha.kubernetes.io/node-selector is enforced via tenant, cannot be removed"

				recorder.LabeledEvent(
					oldNs,
					corev1.EventTypeWarning,
					events.ReasonForbiddenNodeSelectorUpdate,
					events.ActionValidationDenied,
					msg,
				).
					WithRelated(tnt).
					WithTenantLabel(tnt).
					WithRequestAnnotations(req).
					Emit(ctx)

				return ad.Deny(msg)
			}

			if v != oldNs.GetAnnotations()["scheduler.alpha.kubernetes.io/node-selector"] {
				msg := "the annotation scheduler.alpha.kubernetes.io/node-selector is enforced via tenant, cannot be updated"

				recorder.LabeledEvent(
					oldNs,
					corev1.EventTypeWarning,
					events.ReasonForbiddenNodeSelectorUpdate,
					events.ActionValidationDenied,
					msg,
				).
					WithRelated(tnt).
					WithTenantLabel(tnt).
					WithRequestAnnotations(req).
					Emit(ctx)

				return ad.Deny(msg)
			}
		}

		if tnt.Spec.NamespaceOptions != nil && user.IsCapsule() {
			labels, annotations, err := userMetadataForValidation(newNs, oldNs, tnt)
			if err != nil {
				return ad.ErroredResponse(err)
			}

			if response := validateUserMetadata(
				ctx,
				req,
				tnt,
				oldNs,
				labels,
				annotations,
				tnt.Spec.NamespaceOptions,
				recorder,
			); response != nil {
				return response
			}
		}

		return nil
	}
}

func (h *userMetadataHandler) OnDelete(
	client.Client,
	client.Reader,
	users.AdmissionUser,
	*corev1.Namespace,
	admission.Decoder,
	events.EventRecorder,
	*capsulev1beta2.Tenant,
) handlers.Func {
	return func(context.Context, admission.Request) *admission.Response {
		return nil
	}
}

//nolint:staticcheck
func validateUserMetadata(
	ctx context.Context,
	req admission.Request,
	tnt *capsulev1beta2.Tenant,
	ns *corev1.Namespace,
	labels map[string]string,
	annotations map[string]string,
	options *capsulev1beta2.NamespaceOptions,
	recorder events.EventRecorder,
) *admission.Response {
	err := api.ValidateForbidden(annotations, options.ForbiddenAnnotations)
	if err != nil {
		err = errors.Wrap(err, "namespace annotations validation failed")

		recorder.LabeledEvent(
			ns,
			corev1.EventTypeWarning,
			events.ReasonForbiddenAnnotation,
			events.ActionValidationDenied,
			err.Error(),
		).
			WithRelated(tnt).
			WithTenantLabel(tnt).
			WithRequestAnnotations(req).
			Emit(ctx)

		return ad.Deny(err.Error())
	}

	err = api.ValidateForbidden(labels, options.ForbiddenLabels)
	if err != nil {
		err = errors.Wrap(err, "namespace labels validation failed")

		recorder.LabeledEvent(
			ns,
			corev1.EventTypeWarning,
			events.ReasonForbiddenLabel,
			events.ActionValidationDenied,
			err.Error(),
		).
			WithRelated(tnt).
			WithTenantLabel(tnt).
			WithRequestAnnotations(req).
			Emit(ctx)

		return ad.Deny(err.Error())
	}

	return nil
}

func userMetadataForValidation(
	newNs *corev1.Namespace,
	oldNs *corev1.Namespace,
	tnt *capsulev1beta2.Tenant,
) (map[string]string, map[string]string, error) {
	labels := metadataForValidation(newNs.GetLabels(), nil)
	annotations := metadataForValidation(newNs.GetAnnotations(), nil)

	// On update, validate only metadata that was added or changed by the request.
	if oldNs != nil {
		labels = metadataForValidation(newNs.GetLabels(), oldNs.GetLabels())
		annotations = metadataForValidation(newNs.GetAnnotations(), oldNs.GetAnnotations())
	}

	managedLabels, managedAnnotations, err := tenant.BuildNamespaceMetadataForTenant(newNs, tnt)
	if err != nil {
		return nil, nil, err
	}

	tenant.AddNamespaceNameLabels(managedLabels, newNs)
	tenant.AddTenantNameLabel(managedLabels, tnt)

	removeManagedMetadata(labels, managedLabels)
	removeManagedMetadata(annotations, managedAnnotations)

	return labels, annotations, nil
}

func metadataForValidation(newMetadata, oldMetadata map[string]string) map[string]string {
	if oldMetadata == nil {
		return maps.Clone(newMetadata)
	}

	metadata := make(map[string]string)

	for key, newValue := range newMetadata {
		oldValue, ok := oldMetadata[key]
		if !ok || oldValue != newValue {
			metadata[key] = newValue
		}
	}

	return metadata
}

func removeManagedMetadata(metadata map[string]string, managed map[string]string) {
	for key, managedValue := range managed {
		value, ok := metadata[key]
		if !ok {
			continue
		}

		// Only ignore metadata Capsule itself would manage.
		// Same key with a different value is still user-controlled and must be validated.
		if value == managedValue {
			delete(metadata, key)
		}
	}
}
