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

package validation

import (
	"context"
	"fmt"
	"reflect"

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

	capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
	webhookutils "github.com/projectcapsule/capsule/internal/webhook/utils"
	ad "github.com/projectcapsule/capsule/pkg/runtime/admission"
	"github.com/projectcapsule/capsule/pkg/runtime/configuration"
	"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"
)

func NamespaceHandler(configuration configuration.Configuration, hndlers ...handlers.TypedHandlerWithTenantUser[*corev1.Namespace]) handlers.Handler {
	return &handler{
		cfg:      configuration,
		handlers: hndlers,
	}
}

type handler struct {
	cfg      configuration.Configuration
	handlers []handlers.TypedHandlerWithTenantUser[*corev1.Namespace]
}

func (h *handler) OnCreate(
	c client.Client,
	reader client.Reader,
	decoder admission.Decoder,
	recorder events.EventRecorder,
) handlers.Func {
	return func(ctx context.Context, req admission.Request) *admission.Response {
		reader = webhookutils.NewTenantCachingReader(reader)

		user := handlers.ResolveAdmissionUser(ctx, c, req, h.cfg)

		ns := &corev1.Namespace{}
		if err := decoder.Decode(req, ns); err != nil {
			return ad.ErroredResponse(err)
		}

		if !user.IsAdmin() && !user.IsCapsule() && !tenant.HasTenantReference(ns) {
			return nil
		}

		tnt, err := tenant.ResolveNamespaceTenant(ctx, reader, ns)
		if err != nil {
			return ad.ErroredResponse(err)
		}

		if !user.IsAdmin() && !user.IsCapsule() && tnt != nil {
			return ad.Deny("only tenant owners can create tenant-owned namespaces")
		}

		if tnt == nil {
			return nil
		}

		if terminating := h.rejectOnTermination(ns, tnt); terminating != nil {
			return terminating
		}

		for _, hndl := range h.handlers {
			if response := hndl.OnCreate(c, reader, user, ns, decoder, recorder, tnt)(ctx, req); response != nil {
				return response
			}
		}

		return nil
	}
}

func (h *handler) OnDelete(
	c client.Client,
	reader client.Reader,
	decoder admission.Decoder,
	recorder events.EventRecorder,
) handlers.Func {
	return func(ctx context.Context, req admission.Request) *admission.Response {
		oldNs := &corev1.Namespace{}
		if err := decoder.DecodeRaw(req.OldObject, oldNs); err != nil {
			return ad.ErroredResponse(err)
		}

		// A namespace which is already terminating must be allowed to complete
		// deletion even if its Tenant metadata became inconsistent meanwhile.
		// Kubernetes authorization has already guarded the original delete.
		if oldNs.DeletionTimestamp != nil || oldNs.Status.Phase == corev1.NamespaceTerminating {
			return nil
		}

		reader = webhookutils.NewTenantCachingReader(reader)

		tnt, err := tenant.ResolveNamespaceTenant(ctx, reader, oldNs)
		if apierrors.IsNotFound(err) {
			// Kubernetes authorization already controls namespace deletion.
			// A stale Tenant reference must not make a namespace undeletable.
			return nil
		}

		user := handlers.ResolveAdmissionUser(ctx, c, req, h.cfg)

		if err != nil && !user.IsAdmin() {
			return ad.ErroredResponse(err)
		}

		if tnt == nil {
			return nil
		}

		for _, hndl := range h.handlers {
			if response := hndl.OnDelete(c, reader, user, oldNs, decoder, recorder, tnt)(ctx, req); response != nil {
				return response
			}
		}

		return nil
	}
}

func (h *handler) OnUpdate(
	c client.Client,
	reader client.Reader,
	decoder admission.Decoder,
	recorder events.EventRecorder,
) handlers.Func {
	return func(ctx context.Context, req admission.Request) *admission.Response {
		ns := &corev1.Namespace{}
		if err := decoder.Decode(req, ns); err != nil {
			return ad.ErroredResponse(err)
		}

		oldNs := &corev1.Namespace{}
		if err := decoder.DecodeRaw(req.OldObject, oldNs); err != nil {
			return ad.ErroredResponse(err)
		}

		if response, stop := validateTerminatingNamespaceUpdate(req, oldNs, ns); stop {
			return response
		}

		reader = webhookutils.NewTenantCachingReader(reader)

		user := handlers.ResolveAdmissionUser(ctx, c, req, h.cfg)

		if response, stop := validateNamespaceTenantReferenceTransition(user, oldNs, ns); stop {
			return response
		}

		oldTenant, err := tenant.ResolveNamespaceTenant(ctx, reader, oldNs)
		if err != nil && !user.IsAdmin() {
			return ad.ErroredResponse(err)
		}

		newTenant, err := tenant.ResolveNamespaceTenant(ctx, reader, ns)
		if err != nil {
			return ad.ErroredResponse(err)
		}

		if !user.IsAdmin() {
			if oldTenant == nil || newTenant == nil {
				return ad.Deny("namespace tenant ownership is incomplete")
			}

			if oldTenant.GetName() != newTenant.GetName() || oldTenant.GetUID() != newTenant.GetUID() {
				return ad.Deny("namespace can not be migrated between tenants")
			}

			if user.IsCapsule() && !tenant.NamespaceIsOwned(ctx, c, h.cfg, oldNs, oldTenant, user) {
				recorder.LabeledEvent(
					ns,
					corev1.EventTypeWarning,
					events.ReasonNamespaceHijack,
					events.ActionValidationDenied,
					"namespace can not be patched",
				).
					WithRequestAnnotations(req).
					Emit(ctx)

				return ad.Deny("denied patch request for this namespace")
			}
		}

		adminTenantTransition := user.IsAdmin() && namespaceTenantChanged(oldTenant, newTenant)

		tnt := newTenant
		if !user.IsAdmin() {
			tnt = oldTenant
		}

		if tnt == nil {
			return nil
		}

		if terminating := h.rejectOnTermination(ns, newTenant); terminating != nil {
			return terminating
		}

		if adminTenantTransition {
			return nil
		}

		for _, hndl := range h.handlers {
			if response := hndl.OnUpdate(c, reader, user, ns, oldNs, decoder, recorder, tnt)(ctx, req); response != nil {
				return response
			}
		}

		return nil
	}
}

func namespaceTenantChanged(oldTenant, newTenant *capsulev1beta2.Tenant) bool {
	if oldTenant == nil || newTenant == nil {
		return oldTenant != newTenant
	}

	return oldTenant.GetName() != newTenant.GetName() || oldTenant.GetUID() != newTenant.GetUID()
}

func isTerminatingNamespaceUpdate(
	req admission.Request,
	oldNs, newNs *corev1.Namespace,
) bool {
	return req.SubResource == "finalize" ||
		newNs.DeletionTimestamp != nil ||
		oldNs.DeletionTimestamp != nil ||
		newNs.Status.Phase == corev1.NamespaceTerminating ||
		oldNs.Status.Phase == corev1.NamespaceTerminating
}

func validateTerminatingNamespaceUpdate(
	req admission.Request,
	oldNs, newNs *corev1.Namespace,
) (*admission.Response, bool) {
	if !isTerminatingNamespaceUpdate(req, oldNs, newNs) {
		return nil, false
	}

	if namespaceTenantAssignmentChanged(oldNs, newNs) {
		return ad.Deny("namespace tenant ownership can not change during termination"), true
	}

	return nil, true
}

func validateNamespaceTenantReferenceTransition(
	user users.AdmissionUser,
	oldNs, newNs *corev1.Namespace,
) (*admission.Response, bool) {
	if user.IsAdmin() {
		if !tenant.HasConsistentTenantReference(newNs) {
			return ad.Deny("tenant label and ownerReference must both be set consistently or both be absent"), true
		}

		return nil, false
	}

	oldHasTenantReference := tenant.HasTenantReference(oldNs)
	newHasTenantReference := tenant.HasTenantReference(newNs)

	switch {
	case !oldHasTenantReference && newHasTenantReference:
		return ad.Deny("namespace can not be patched into a tenant"), true
	case oldHasTenantReference && !newHasTenantReference:
		return ad.Deny("namespace can not remove tenant ownership"), true
	case !oldHasTenantReference && !newHasTenantReference:
		return nil, true
	default:
		return nil, false
	}
}

func namespaceTenantAssignmentChanged(oldNs, newNs *corev1.Namespace) bool {
	if tenant.TenanLabelValue(oldNs) != tenant.TenanLabelValue(newNs) {
		return true
	}

	return !reflect.DeepEqual(
		tenant.TenantOwnerReferences(oldNs),
		tenant.TenantOwnerReferences(newNs),
	)
}

func (h *handler) rejectOnTermination(
	ns *corev1.Namespace,
	t *capsulev1beta2.Tenant,
) *admission.Response {
	if t == nil {
		return nil
	}

	if t.DeletionTimestamp == nil {
		return nil
	}

	instance := t.Status.GetInstance(&capsulev1beta2.TenantStatusNamespaceItem{
		Name: ns.GetName(),
		UID:  ns.GetUID(),
	})

	if instance != nil {
		return nil
	}

	err := fmt.Errorf("tenant is terminating and does not accept new namespaces")

	return ad.ErroredResponse(err)
}
