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

package tenant

import (
	"context"
	"errors"
	"fmt"
	"slices"
	"sync"
	"time"

	"github.com/go-logr/logr"
	corev1 "k8s.io/api/core/v1"
	networkingv1 "k8s.io/api/networking/v1"
	nodev1 "k8s.io/api/node/v1"
	rbacv1 "k8s.io/api/rbac/v1"
	resourcesv1 "k8s.io/api/resource/v1"
	schedulingv1 "k8s.io/api/scheduling/v1"
	storagev1 "k8s.io/api/storage/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/apiserver/pkg/authentication/serviceaccount"
	"k8s.io/client-go/discovery"
	"k8s.io/client-go/dynamic"
	"k8s.io/client-go/rest"
	"k8s.io/client-go/tools/events"
	"k8s.io/client-go/util/workqueue"
	"sigs.k8s.io/cluster-api/util/patch"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/builder"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/event"
	"sigs.k8s.io/controller-runtime/pkg/handler"
	"sigs.k8s.io/controller-runtime/pkg/predicate"
	"sigs.k8s.io/controller-runtime/pkg/reconcile"
	gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"

	capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
	"github.com/projectcapsule/capsule/internal/cache"
	"github.com/projectcapsule/capsule/internal/controllers/utils"
	"github.com/projectcapsule/capsule/internal/metrics"
	caperrors "github.com/projectcapsule/capsule/pkg/api/errors"
	"github.com/projectcapsule/capsule/pkg/api/rbac"
	"github.com/projectcapsule/capsule/pkg/runtime/configuration"
	"github.com/projectcapsule/capsule/pkg/runtime/gvk"
	"github.com/projectcapsule/capsule/pkg/runtime/predicates"
)

type Manager struct {
	client.Client

	reader client.Reader

	DiscoveryClient discovery.DiscoveryInterface
	DynamicClient   dynamic.Interface

	Metrics       *metrics.TenantRecorder
	Log           logr.Logger
	Recorder      events.EventRecorder
	Configuration configuration.Configuration
	RESTConfig    *rest.Config
	classes       supportedClasses

	discoveryCache cache.DiscoveryNamespacedResourceCache

	resourceQuotaSyncMu sync.Mutex
	resourceQuotaSyncs  map[string]*tenantResourceQuotaSync
}

type supportedClasses struct {
	device  bool
	gateway bool
}

func (r *Manager) SetupWithManager(mgr ctrl.Manager, ctrlConfig utils.ControllerOptions) error {
	r.reader = mgr.GetAPIReader()
	r.discoveryCache = cache.NewDiscoveryNamespacedResourceCache()

	ctrlBuilder := ctrl.NewControllerManagedBy(mgr).
		Named("capsule/tenants").
		For(
			&capsulev1beta2.Tenant{},
			builder.WithPredicates(predicates.ClassChanged()),
		).
		Owns(&networkingv1.NetworkPolicy{}, builder.WithPredicates(predicates.TenantManagedResourceChangedPredicate{})).
		Owns(&corev1.LimitRange{}, builder.WithPredicates(predicates.TenantManagedResourceChangedPredicate{})).
		Owns(
			&capsulev1beta2.GlobalResourceQuota{},
			builder.WithPredicates(predicate.Or(
				predicate.GenerationChangedPredicate{},
				predicates.UpdatedMetadataPredicate{},
				predicates.DeletionChangedPredicate{},
			)),
		).
		Owns(&rbacv1.RoleBinding{}, builder.WithPredicates(predicates.TenantManagedResourceChangedPredicate{})).
		Watches(
			&capsulev1beta2.CapsuleConfiguration{},
			handler.EnqueueRequestsFromMapFunc(r.enqueueAllTenants),
			builder.WithPredicates(
				predicates.CapsuleConfigSpecAdministratorsChangedPredicate{},
				predicates.NamesMatchingPredicate{Names: []string{ctrlConfig.ConfigurationName}},
			),
		).
		Watches(
			&corev1.Namespace{},
			handler.EnqueueRequestForOwner(mgr.GetScheme(), mgr.GetRESTMapper(), &capsulev1beta2.Tenant{}),
			builder.WithPredicates(predicates.NamespaceTenantStateChangedPredicate{}),
		).
		Watches(
			&capsulev1beta2.RuleStatus{},
			handler.EnqueueRequestForOwner(mgr.GetScheme(), mgr.GetRESTMapper(), &capsulev1beta2.Tenant{}),
			builder.WithPredicates(predicates.ClassChanged()),
		).
		Watches(
			&storagev1.StorageClass{},
			r.tenantClassEventHandler(r.collectAvailableStorageClasses),
			builder.WithPredicates(predicates.ClassChanged()),
		).
		Watches(
			&schedulingv1.PriorityClass{},
			r.tenantClassEventHandler(r.collectAvailablePriorityClasses),
			builder.WithPredicates(predicates.ClassChanged()),
		).
		Watches(
			&nodev1.RuntimeClass{},
			r.tenantClassEventHandler(r.collectAvailableRuntimeClasses),
			builder.WithPredicates(predicates.ClassChanged()),
		).
		Watches(
			&capsulev1beta2.TenantOwner{},
			handler.TypedFuncs[client.Object, ctrl.Request]{
				CreateFunc: func(
					ctx context.Context,
					e event.TypedCreateEvent[client.Object],
					q workqueue.TypedRateLimitingInterface[reconcile.Request],
				) {
					r.enqueueForTenantsWithCondition(
						ctx,
						e.Object,
						q,
						func(tnt *capsulev1beta2.Tenant, c client.Object) bool {
							return len(tnt.Spec.Permissions.MatchOwners) > 0
						})
				},
				UpdateFunc: func(
					ctx context.Context,
					e event.TypedUpdateEvent[client.Object],
					q workqueue.TypedRateLimitingInterface[reconcile.Request],
				) {
					r.enqueueForTenantsWithCondition(
						ctx,
						e.ObjectNew,
						q,
						func(tnt *capsulev1beta2.Tenant, c client.Object) bool {
							return len(tnt.Spec.Permissions.MatchOwners) > 0
						})
				},

				DeleteFunc: func(
					ctx context.Context,
					e event.TypedDeleteEvent[client.Object],
					q workqueue.TypedRateLimitingInterface[reconcile.Request],
				) {
					r.enqueueForTenantsWithCondition(
						ctx,
						e.Object,
						q,
						func(tnt *capsulev1beta2.Tenant, _ client.Object) bool {
							return len(tnt.Spec.Permissions.MatchOwners) > 0
						},
					)
				},
			},
		).
		Watches(
			&corev1.ServiceAccount{},
			handler.TypedFuncs[client.Object, ctrl.Request]{
				CreateFunc: func(
					ctx context.Context,
					e event.TypedCreateEvent[client.Object],
					q workqueue.TypedRateLimitingInterface[reconcile.Request],
				) {
					r.enqueueForTenantsWithCondition(ctx, e.Object, q, func(tnt *capsulev1beta2.Tenant, c client.Object) bool {
						return slices.Contains(tnt.Status.Namespaces, c.GetNamespace())
					})
				},
				UpdateFunc: func(
					ctx context.Context,
					e event.TypedUpdateEvent[client.Object],
					q workqueue.TypedRateLimitingInterface[reconcile.Request],
				) {
					r.enqueueForTenantsWithCondition(ctx, e.ObjectNew, q, func(tnt *capsulev1beta2.Tenant, c client.Object) bool {
						return slices.Contains(tnt.Status.Namespaces, c.GetNamespace())
					})
				},
				DeleteFunc: func(
					ctx context.Context,
					e event.TypedDeleteEvent[client.Object],
					q workqueue.TypedRateLimitingInterface[reconcile.Request],
				) {
					r.enqueueForTenantsWithCondition(ctx, e.Object, q, func(tnt *capsulev1beta2.Tenant, c client.Object) bool {
						_, found := tnt.Status.Owners.FindOwner(
							serviceaccount.ServiceAccountUsernamePrefix+c.GetNamespace()+":"+c.GetName(),
							rbac.ServiceAccountOwner,
						)

						return found
					})
				},
			},
			builder.WithPredicates(predicates.PromotedServiceaccountPredicate{}),
		).
		WithOptions(ctrlConfig.Runtime.ToControllerOptions())

	// GatewayClass is Optional
	r.classes.gateway = gvk.HasGVK(mgr.GetRESTMapper(), schema.GroupVersionKind{
		Group:   "gateway.networking.k8s.io",
		Version: "v1",
		Kind:    "GatewayClass",
	})

	if r.classes.gateway {
		ctrlBuilder = ctrlBuilder.Watches(
			&gatewayv1.GatewayClass{},
			r.tenantClassEventHandler(r.collectAvailableGatewayClasses),
			builder.WithPredicates(predicates.ClassChanged()),
		)
	}

	// DeviceClass is Optional
	r.classes.device = gvk.HasGVK(mgr.GetRESTMapper(), schema.GroupVersionKind{
		Group:   "resource.k8s.io",
		Version: "v1",
		Kind:    "DeviceClass",
	})

	if r.classes.device {
		ctrlBuilder = ctrlBuilder.Watches(
			&resourcesv1.DeviceClass{},
			r.tenantClassEventHandler(r.collectAvailableDeviceClasses),
			builder.WithPredicates(predicates.ClassChanged()),
		)
	}

	if err := ctrlBuilder.Complete(r); err != nil {
		return err
	}

	return r.setupResourceQuotaController(mgr, ctrlConfig)
}

func (r *Manager) Reconcile(ctx context.Context, request ctrl.Request) (result ctrl.Result, err error) {
	log := r.Log.WithValues("Request.Name", request.Name)

	// Fetch the Tenant instance
	instance := &capsulev1beta2.Tenant{}
	if err = r.Get(ctx, client.ObjectKey{Name: request.Name}, instance); err != nil {
		if apierrors.IsNotFound(err) {
			log.V(5).Info("request object not found, could have been deleted after reconcile request")

			// If tenant was deleted or cannot be found, clean up metrics
			r.Metrics.DeleteAllMetricsForTenant(request.Name)

			return reconcile.Result{}, nil
		}

		return result, err
	}

	if updateErr := r.updateReconcilingStatus(ctx, instance); updateErr != nil {
		if apierrors.IsNotFound(updateErr) {
			return reconcile.Result{}, nil
		}

		return reconcile.Result{}, updateErr
	}

	// Create the patch helper after the initial status has been established and
	// copied back into instance. Otherwise its baseline may contain a nil status
	// condition list and the subsequent patch can fail CRD validation.
	patchHelper, err := patch.NewHelper(instance, r.Client)
	if err != nil {
		return reconcile.Result{}, err
	}

	patchBaselineStatus := *instance.Status.DeepCopy()

	reconcileError := r.reconcile(ctx, log, instance)

	defer func() {
		if statusErr := r.updateTenantStatus(ctx, instance, reconcileError); statusErr != nil {
			statusErr = fmt.Errorf("cannot update tenant status: %w", statusErr)

			if err == nil {
				err = statusErr
			} else {
				err = errors.Join(err, statusErr)
			}

			return
		}

		r.syncTenantStatusMetrics(instance)
	}()

	// Status has a dedicated retrying writer in the deferred update above.
	// Cluster API's patch helper patches status automatically, so temporarily
	// restore its baseline to ensure this call can only persist metadata/spec.
	// This prevents a stale cached status (including nil conditions) from racing
	// the authoritative status update.
	desiredStatus := *instance.Status.DeepCopy()
	instance.Status = patchBaselineStatus
	e := patchHelper.Patch(ctx, instance)
	instance.Status = desiredStatus

	if e != nil {
		if caperrors.IgnoreGone(e) {
			err = nil

			return result, err
		}

		return reconcile.Result{}, e
	}

	// Available class collection is irrelevant while deleting and can involve
	// several cluster-wide lists. Keep the termination path focused on namespace
	// cleanup and finalizer removal.
	if instance.DeletionTimestamp == nil {
		if err = r.collectAvailableResources(ctx, log, instance); err != nil {
			err = fmt.Errorf("cannot collect available resources: %w", err)

			return reconcile.Result{}, err
		}
	}

	if instance.DeletionTimestamp != nil && len(instance.Status.Spaces) > 0 {
		return reconcile.Result{RequeueAfter: 2 * time.Second}, nil
	}

	return reconcile.Result{}, reconcileError
}

//nolint:staticcheck
func (r *Manager) reconcile(ctx context.Context, log logr.Logger, instance *capsulev1beta2.Tenant) (err error) {
	var errs []error

	if instance.DeletionTimestamp != nil {
		if err = r.reconcileNamespaces(ctx, log, instance); err != nil {
			errs = append(errs, fmt.Errorf("namespace(s) had reconciliation errors: %w", err))
		}

		// The managed-resource webhook intentionally denies deletion by the
		// garbage collector. Remove rule-generated cluster-scoped children as
		// the Capsule controller before releasing the Tenant finalizer.
		if err = r.pruneGlobalResourceQuotas(ctx, instance, map[string]struct{}{}); err != nil {
			errs = append(errs, fmt.Errorf("cannot delete rule global resource quotas: %w", err))

			return errors.Join(errs...)
		}

		if err = r.ensureMetadata(ctx, instance); err != nil {
			errs = append(errs, fmt.Errorf("cannot ensure metadata: %w", err))
		}

		return errors.Join(errs...)
	}

	// Collect Ownership/Promotions for Status
	if err = r.collectRBAC(ctx, instance); err != nil {
		errs = append(errs, fmt.Errorf("cannot collect available rbac: %w", err))
	}

	log.V(4).Info("starting processing of Namespaces", "items", len(instance.Status.Namespaces))

	if err = r.reconcileNamespaces(ctx, log, instance); err != nil {
		errs = append(errs, fmt.Errorf("namespace(s) had reconciliation errors: %w", err))
	}

	// Ensuring Metadata.
	err = r.ensureMetadata(ctx, instance)
	if err != nil {
		errs = append(errs, fmt.Errorf("cannot ensure metadata: %w", err))
	}

	log.V(4).Info("ensuring limit resources count is updated")

	if err = r.syncCustomResourceQuotaUsages(ctx, instance); err != nil {
		errs = append(errs, fmt.Errorf("cannot count limited resources: %w", err))
	}

	log.V(4).Info("starting processing of Network Policies")

	if err = r.syncNetworkPolicies(ctx, log, instance); err != nil {
		errs = append(errs, fmt.Errorf("cannot sync networkPolicy items: %w", err))
	}

	//nolint:staticcheck
	log.V(4).Info("starting processing of Limit Ranges", "items", len(instance.Spec.LimitRanges.Items))

	if err = r.syncLimitRanges(ctx, log, instance); err != nil {
		errs = append(errs, fmt.Errorf("cannot sync limitrange items: %w", err))
	}

	log.V(4).Info("starting processing of Resource Quotas", "items", len(instance.Spec.ResourceQuota.Items))

	if err = r.syncResourceQuotas(ctx, log, instance); err != nil {
		errs = append(errs, fmt.Errorf("cannot sync resourcequota items: %w", err))
	}

	log.V(4).Info("starting processing of rule GlobalResourceQuotas")

	if err = r.syncGlobalResourceQuotas(ctx, instance); err != nil {
		errs = append(errs, fmt.Errorf("cannot sync rule global resource quotas: %w", err))
	}

	log.V(4).Info("ensuring RoleBindings for Owners and Tenant")

	if err = r.syncRoleBindings(ctx, log, instance); err != nil {
		errs = append(errs, fmt.Errorf("cannot sync rolebindings items: %w", err))
	}

	if err = errors.Join(errs...); err != nil {
		return err
	}

	log.V(4).Info("Tenant reconciling completed")

	return err
}
