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

package tenant

import (
	"context"
	"errors"
	"fmt"
	"maps"
	"time"

	"github.com/go-logr/logr"
	"golang.org/x/sync/errgroup"
	corev1 "k8s.io/api/core/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/util/retry"
	"k8s.io/utils/ptr"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"

	capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
	"github.com/projectcapsule/capsule/pkg/api/meta"
	"github.com/projectcapsule/capsule/pkg/tenant"
)

const namespaceCascadingCleanupGracePeriod = 10 * time.Second

// Ensuring all annotations are applied to each Namespace handled by the Tenant.
func (r *Manager) reconcileNamespaces(
	ctx context.Context,
	log logr.Logger,
	tnt *capsulev1beta2.Tenant,
) error {
	if tnt.DeletionTimestamp != nil {
		return r.reconcileDeletingTenantNamespaces(ctx, log, tnt)
	}

	reconcileErr := r.reconcileActiveTenantNamespaces(ctx, log, tnt)
	garbageCollectionErr := r.runGarbageCollection(ctx, tnt, &capsulev1beta2.RuleStatus{})

	return errors.Join(reconcileErr, garbageCollectionErr)
}

func (r *Manager) reconcileDeletingTenantNamespaces(
	ctx context.Context,
	log logr.Logger,
	tnt *capsulev1beta2.Tenant,
) error {
	templateTenant := tnt.DeepCopy()

	group, ctx := errgroup.WithContext(ctx)
	group.SetLimit(8)

	results := make(chan *capsulev1beta2.TenantStatusNamespaceItem, len(tnt.Status.Spaces))
	removed := make(chan string, len(tnt.Status.Spaces))
	errs := make(chan error, len(tnt.Status.Spaces))

	for i := range tnt.Status.Spaces {
		statusNamespace := tnt.Status.Spaces[i]

		group.Go(func() error {
			namespace := &corev1.Namespace{}

			// Deletion decisions must not depend on informer-cache freshness. A
			// direct read observes NotFound and deletionTimestamp promptly, which
			// lets the Tenant finalizer advance without waiting for cache delivery.
			err := r.reader.Get(ctx, client.ObjectKey{Name: statusNamespace.Name}, namespace)
			if apierrors.IsNotFound(err) {
				removed <- statusNamespace.Name

				return nil
			}

			if err != nil {
				errs <- fmt.Errorf("get namespace %q: %w", statusNamespace.Name, err)

				return nil
			}

			if namespace.DeletionTimestamp == nil {
				if err := r.Delete(ctx, namespace, &client.DeleteOptions{
					PropagationPolicy: ptr.To(metav1.DeletePropagationBackground),
				}); err != nil && !apierrors.IsNotFound(err) {
					log.Error(err, "unable to delete tenant namespace",
						"tenant", tnt.GetName(),
						"namespace", namespace.GetName(),
					)

					errs <- fmt.Errorf("delete namespace %q: %w", namespace.Name, err)

					return nil
				}

				latest := &corev1.Namespace{}

				err := r.reader.Get(ctx, client.ObjectKey{Name: namespace.Name}, latest)
				if apierrors.IsNotFound(err) {
					removed <- statusNamespace.Name

					return nil
				}

				if err != nil {
					errs <- fmt.Errorf("get namespace %q after delete: %w", namespace.Name, err)

					return nil
				}

				namespace = latest
			}

			stat, err := r.reconcileNamespace(ctx, log, namespace, tnt, templateTenant)
			if stat != nil {
				results <- stat
			}

			if err != nil {
				log.Error(err, "failed to reconcile deleting namespace",
					"tenant", tnt.GetName(),
					"namespace", namespace.GetName(),
				)

				errs <- fmt.Errorf("namespace %q: %w", namespace.Name, err)
			}

			return nil
		})
	}

	_ = group.Wait()

	close(results)
	close(removed)
	close(errs)

	for name := range removed {
		r.Metrics.DeleteAllMetricsForNamespace(name)

		tnt.Status.RemoveInstance(&capsulev1beta2.TenantStatusNamespaceItem{
			Name: name,
		})
	}

	for stat := range results {
		if stat == nil {
			continue
		}

		tnt.Status.UpdateInstance(stat)
	}

	var joined []error
	for itemErr := range errs {
		joined = append(joined, itemErr)
	}

	tnt.Status.Size = uint(len(tnt.Status.Spaces))

	return errors.Join(joined...)
}

func (r *Manager) reconcileActiveTenantNamespaces(
	ctx context.Context,
	log logr.Logger,
	tnt *capsulev1beta2.Tenant,
) error {
	list := &corev1.NamespaceList{}
	if err := r.List(ctx, list, client.MatchingFields{".metadata.ownerReferences[*].capsule": tnt.GetName()}); err != nil {
		return err
	}

	// Rule rendering reads the complete Tenant, including status. Keep one
	// immutable snapshot for all namespace workers so their status updates do
	// not race with template conversion.
	templateTenant := tnt.DeepCopy()

	oldStatus := make(map[string]struct{}, len(tnt.Status.Spaces))
	for i := range tnt.Status.Spaces {
		oldStatus[tnt.Status.Spaces[i].Name] = struct{}{}
	}

	group, ctx := errgroup.WithContext(ctx)
	group.SetLimit(8)

	results := make(chan *capsulev1beta2.TenantStatusNamespaceItem, len(list.Items))
	errs := make(chan error, len(list.Items))

	for i := range list.Items {
		ns := list.Items[i].DeepCopy()

		group.Go(func() error {
			stat, err := r.reconcileNamespace(ctx, log, ns, tnt, templateTenant)
			if stat != nil {
				results <- stat
			}

			if err != nil {
				log.Error(err, "failed to reconcile namespace",
					"tenant", tnt.GetName(),
					"namespace", ns.GetName(),
				)

				errs <- fmt.Errorf("namespace %q: %w", ns.Name, err)
			}

			return nil
		})
	}

	_ = group.Wait()

	close(results)
	close(errs)

	var joined []error
	for itemErr := range errs {
		joined = append(joined, itemErr)
	}

	err := errors.Join(joined...)

	desiredStatus := make(map[string]struct{}, len(list.Items))

	for stat := range results {
		if stat == nil {
			continue
		}

		tnt.Status.UpdateInstance(stat)

		desiredStatus[stat.Name] = struct{}{}
	}

	for name := range oldStatus {
		if _, keep := desiredStatus[name]; keep {
			continue
		}

		r.Metrics.DeleteAllMetricsForNamespace(name)

		tnt.Status.RemoveInstance(&capsulev1beta2.TenantStatusNamespaceItem{Name: name})
	}

	tnt.Status.Size = uint(len(tnt.Status.Spaces))

	tnt.AssignNamespaces(list.Items)

	return err
}

func (r *Manager) reconcileNamespace(
	ctx context.Context,
	log logr.Logger,
	namespace *corev1.Namespace,
	tnt *capsulev1beta2.Tenant,
	templateTenant *capsulev1beta2.Tenant,
) (
	stat *capsulev1beta2.TenantStatusNamespaceItem,
	err error,
) {
	terminating := false

	stat = &capsulev1beta2.TenantStatusNamespaceItem{
		Name: namespace.GetName(),
		UID:  namespace.GetUID(),
	}

	metaStatus := &capsulev1beta2.TenantStatusNamespaceMetadata{}

	instance := tnt.Status.GetInstance(stat)
	if instance != nil {
		stat = instance
	}

	// Always update tenant status condition after reconciliation.
	defer func() {
		readCondition := meta.NewReadyCondition(namespace)

		switch {
		case terminating:
			readCondition.Status = metav1.ConditionFalse
			readCondition.Reason = meta.TerminatingReason
			readCondition.Message = "Namespace is terminating"
		case err != nil:
			readCondition.Status = metav1.ConditionFalse
			readCondition.Reason = meta.FailedReason
			readCondition.Message = fmt.Sprintf("Failed to reconcile: %v", err)

			if instance != nil && instance.Metadata != nil {
				stat.Metadata = instance.Metadata
			}
		default:
			if metaStatus != nil {
				stat.Metadata = metaStatus
			}
		}

		stat.Conditions.UpdateConditionByType(readCondition)

		cordonedCondition := meta.NewCordonedCondition(namespace)

		if namespace.Labels[meta.CordonedLabel] == meta.ValueTrue {
			cordonedCondition.Reason = meta.CordonedReason
			cordonedCondition.Message = "namespace is cordoned"
			cordonedCondition.Status = metav1.ConditionTrue
		}

		stat.Conditions.UpdateConditionByType(cordonedCondition)
	}()

	// Verify if namespace is still active or terminating.
	//nolint:nestif
	if namespace.DeletionTimestamp != nil {
		terminating = true

		terminatingState := meta.NewTerminatingConditionReason(namespace)

		pending, err := tenant.NamespaceIsPendingPodTerminating(ctx, r.Client, namespace)
		if err != nil {
			terminatingState.Reason = meta.FailedReason
			terminatingState.Status = metav1.ConditionFalse
			terminatingState.Message = err.Error()
			stat.Conditions.UpdateConditionByType(terminatingState)

			return stat, err
		}

		if pending {
			terminatingState.Reason = meta.PendingUnmanagedContentReason
			terminatingState.Status = metav1.ConditionFalse
			terminatingState.Message = "waiting for pods to finalize"
			stat.Conditions.UpdateConditionByType(terminatingState)

			return stat, nil
		}

		// Give Kubernetes' namespace controller time to perform its normal
		// deletion before issuing a discovery-wide forced cleanup. Most namespaces
		// disappear during this window, avoiding hundreds of list requests and
		// substantial API pressure during Tenant teardown. Stuck namespaces still
		// receive the existing finalizer cleanup after the grace period.
		if time.Since(namespace.DeletionTimestamp.Time) < namespaceCascadingCleanupGracePeriod {
			terminatingState.Reason = meta.TerminatingReason
			terminatingState.Status = metav1.ConditionFalse
			terminatingState.Message = "waiting for namespace finalization"
			stat.Conditions.UpdateConditionByType(terminatingState)

			return stat, nil
		}

		cleaned, err := tenant.NamespacedCascadingCleanup(ctx, r.Client, r.DiscoveryClient, &r.discoveryCache, r.DynamicClient, namespace)
		if err != nil {
			terminatingState.Reason = meta.FailedReason
			terminatingState.Status = metav1.ConditionFalse
			terminatingState.Message = err.Error()
			stat.Conditions.UpdateConditionByType(terminatingState)

			return stat, err
		}

		if cleaned {
			terminatingState.Reason = meta.PendingUnmanagedContentReason
			terminatingState.Status = metav1.ConditionFalse
			terminatingState.Message = "performing cascading deletion"
			stat.Conditions.UpdateConditionByType(terminatingState)

			return stat, nil
		}

		terminatingState.Reason = meta.TerminatingReason
		terminatingState.Status = metav1.ConditionFalse
		terminatingState.Message = "waiting for namespace finalization"
		stat.Conditions.UpdateConditionByType(terminatingState)

		return stat, nil
	}

	// Collect Rules for namespace.
	err = r.reconcileRuleStatus(ctx, log, tnt, templateTenant, namespace)
	if err != nil {
		return stat, err
	}

	err = retry.RetryOnConflict(retry.DefaultBackoff, func() (conflictErr error) {
		_, conflictErr = controllerutil.CreateOrUpdate(ctx, r.Client, namespace, func() error {
			metaStatus, err = r.reconcileNamespaceMetadata(ctx, namespace, tnt, stat)

			return err
		})

		return conflictErr
	})

	return stat, err
}

//nolint:nestif
func (r *Manager) reconcileNamespaceMetadata(
	ctx context.Context,
	ns *corev1.Namespace,
	tnt *capsulev1beta2.Tenant,
	stat *capsulev1beta2.TenantStatusNamespaceItem,
) (
	managed *capsulev1beta2.TenantStatusNamespaceMetadata,
	err error,
) {
	originLabels := ns.GetLabels()
	if originLabels == nil {
		originLabels = make(map[string]string)
	}

	originAnnotations := ns.GetAnnotations()
	if originAnnotations == nil {
		originAnnotations = make(map[string]string)
	}

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

	managedMetadataOnly := tnt.Spec.NamespaceOptions != nil && tnt.Spec.NamespaceOptions.ManagedMetadataOnly

	// Handle User-Defined Metadata, if allowed.
	if !managedMetadataOnly {
		if originLabels != nil {
			maps.Copy(originLabels, managedLabels)
		}

		if originAnnotations != nil {
			maps.Copy(originAnnotations, managedAnnotations)
		}

		// Cleanup old Metadata.
		instance := tnt.Status.GetInstance(stat)
		if instance != nil && instance.Metadata != nil {
			for label := range instance.Metadata.Labels {
				if _, ok := managedLabels[label]; ok {
					continue
				}

				delete(originLabels, label)
			}

			for annotation := range instance.Metadata.Annotations {
				if _, ok := managedAnnotations[annotation]; ok {
					continue
				}

				delete(originAnnotations, annotation)
			}
		}

		managed = &capsulev1beta2.TenantStatusNamespaceMetadata{
			Labels:      managedLabels,
			Annotations: managedAnnotations,
		}
	} else {
		originLabels = managedLabels
		originAnnotations = managedAnnotations
	}

	tenant.AddNamespaceNameLabels(originLabels, ns)
	tenant.AddTenantNameLabel(originLabels, tnt)

	ns.SetLabels(originLabels)
	ns.SetAnnotations(originAnnotations)

	return managed, err
}
