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

package rulestatus

import (
	"context"
	"fmt"
	"reflect"

	"github.com/go-logr/logr"
	corev1 "k8s.io/api/core/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	k8smeta "k8s.io/apimachinery/pkg/api/meta"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/client-go/rest"
	"k8s.io/client-go/tools/events"
	"k8s.io/client-go/util/retry"
	"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/predicate"
	"sigs.k8s.io/controller-runtime/pkg/reconcile"

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

type Manager struct {
	client.Client

	reader client.Reader

	Metrics       *metrics.RuleStatusRecorder
	Log           logr.Logger
	Recorder      events.EventRecorder
	Configuration configuration.Configuration
	RESTConfig    *rest.Config
	RESTMapper    k8smeta.RESTMapper
}

func (r *Manager) SetupWithManager(mgr ctrl.Manager, ctrlConfig utils.ControllerOptions) error {
	r.reader = mgr.GetAPIReader()
	r.RESTMapper = mgr.GetRESTMapper()

	ctrlBuilder := ctrl.NewControllerManagedBy(mgr).
		Named("capsule/rule-status").
		For(
			&capsulev1beta2.RuleStatus{},
			builder.WithPredicates(
				predicate.Or(
					predicate.GenerationChangedPredicate{},
					predicates.UpdatedMetadataPredicate{},
				),
			),
		).
		WithOptions(ctrlConfig.Runtime.ToControllerOptions())

	return ctrlBuilder.Complete(r)
}

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

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

			r.Metrics.DeleteMetrics(request.Name, request.Namespace)

			return reconcile.Result{}, nil
		}

		log.Error(err, "error reading the object")

		return result, err
	}

	patchHelper, err := patch.NewHelper(instance, r.Client)
	if err != nil {
		return reconcile.Result{}, err
	}

	defer func() {
		if e := r.updateStatus(ctx, instance, err); e != nil {
			if apierrors.IsNotFound(err) || apierrors.HasStatusCause(err, corev1.NamespaceTerminatingCause) {
				err = nil

				return
			}

			err = fmt.Errorf("cannot update status: %w", e)

			return
		}

		r.Metrics.RecordConditions(instance)

		if e := patchHelper.Patch(ctx, instance); e != nil {
			if apierrors.IsNotFound(e) || apierrors.HasStatusCause(e, corev1.NamespaceTerminatingCause) {
				err = nil

				return
			}

			err = fmt.Errorf("cannot patch: %w", e)

			return
		}

		// Controller-Runtime should never receive error
		err = nil
	}()

	// Best-Effort for Updating the status
	if updateErr := r.updateReconcilingStatus(ctx, instance); updateErr != nil {
		if caperrors.IgnoreGone(updateErr) {
			return reconcile.Result{}, nil
		}

		log.Error(updateErr, "failed to update status")
	}

	// Reconcile
	if err = r.reconcile(ctx, instance); err != nil {
		err = fmt.Errorf("cannot collect available resources: %w", err)

		return result, err
	}

	var reconcileError error
	if err != nil {
		reconcileError = fmt.Errorf("had errors reconciling")
	}

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

	return ctrl.Result{}, reconcileError
}

func (r Manager) reconcile(ctx context.Context, instance *capsulev1beta2.RuleStatus) error {
	previousRules := instance.Status.Rules
	hadManagedMetadata := hasManagedMetadata(previousRules)
	ruleStatus := make([]*rules.NamespaceRuleBodyNamespace, 0, len(instance.Spec))

	for _, rule := range instance.Spec {
		if rule == nil || rule.Enforce == nil {
			continue
		}

		statusRule := rule.DeepCopy()
		// RuleStatus is an enforcement cache. Quota definitions are reconciled
		// independently as GlobalResourceQuotas and may include legacy entries
		// which predate stable quota names.
		statusRule.Quota = nil

		enforce := rule.Enforce.DeepCopy()
		for i := range enforce.Metadata {
			enforce.Metadata[i].APIGroups = enforce.Metadata[i].StatusAPIGroups()
		}

		statusRule.Enforce = enforce
		ruleStatus = append(ruleStatus, statusRule)
	}

	instance.Status.Rules = ruleStatus
	//nolint:staticcheck
	instance.Status.Rule = rules.NamespaceRuleBodyNamespace{}

	if hadManagedMetadata || hasManagedMetadata(ruleStatus) {
		if err := r.publishRulesStatus(ctx, instance); err != nil {
			return fmt.Errorf("publish rules before managed metadata reconciliation: %w", err)
		}

		if err := r.reconcileManagedMetadata(ctx, instance, previousRules, ruleStatus); err != nil {
			return fmt.Errorf("reconcile managed metadata: %w", err)
		}
	}

	return nil
}

func (r *Manager) publishRulesStatus(ctx context.Context, instance *capsulev1beta2.RuleStatus) error {
	return retry.RetryOnConflict(retry.DefaultBackoff, func() error {
		latest := &capsulev1beta2.RuleStatus{}
		if err := r.reader.Get(ctx, client.ObjectKeyFromObject(instance), latest); err != nil {
			return err
		}

		latest.Status.Rules = instance.Status.Rules
		//nolint:staticcheck
		latest.Status.Rule = instance.Status.Rule

		return r.Client.Status().Update(ctx, latest)
	})
}

func (r *Manager) updateStatus(ctx context.Context, instance *capsulev1beta2.RuleStatus, reconcileError error) error {
	return retry.RetryOnConflict(retry.DefaultBackoff, func() (err error) {
		latest := &capsulev1beta2.RuleStatus{}
		if err = r.reader.Get(ctx, types.NamespacedName{Name: instance.GetName(), Namespace: instance.GetNamespace()}, latest); err != nil {
			if apierrors.IsNotFound(err) {
				return nil
			}

			return err
		}

		originalStatus := latest.Status.DeepCopy()

		latest.Status = instance.Status
		latest.Status.ObservedGeneration = instance.GetGeneration()

		// Set Ready Condition
		readyCondition := meta.NewReadyCondition(instance)
		if reconcileError != nil {
			readyCondition.Message = reconcileError.Error()
			readyCondition.Status = metav1.ConditionFalse
			readyCondition.Reason = meta.FailedReason
		}

		latest.Status.Conditions.UpdateConditionByType(readyCondition)

		if reflect.DeepEqual(*originalStatus, latest.Status) {
			return nil
		}

		if err := r.Client.Status().Update(ctx, latest); err != nil {
			return err
		}

		// Keep the in-memory object aligned with what we just wrote.
		instance.Status = latest.Status

		return nil
	})
}

func (r *Manager) updateReconcilingStatus(ctx context.Context, instance *capsulev1beta2.RuleStatus) error {
	return retry.RetryOnConflict(retry.DefaultBackoff, func() (err error) {
		latest := &capsulev1beta2.RuleStatus{}
		if err = r.reader.Get(ctx, types.NamespacedName{Name: instance.GetName(), Namespace: instance.GetNamespace()}, latest); err != nil {
			return err
		}

		cleanedQuota := removeQuotaDefinitions(&latest.Status)
		if latest.Status.ObservedGeneration == instance.GetGeneration() {
			if !cleanedQuota {
				return nil
			}

			return r.Status().Update(ctx, latest)
		}

		latest.Status.Conditions.UpdateConditionByType(meta.NewReadyConditionReconcilingReason(instance))

		return r.Status().Update(ctx, latest)
	})
}

//nolint:staticcheck // The deprecated flattened Rule must be cleaned for objects written by older Capsule versions.
func removeQuotaDefinitions(status *capsulev1beta2.RuleStatusStatus) bool {
	if status == nil {
		return false
	}

	changed := len(status.Rule.Quota) > 0

	for _, rule := range status.Rules {
		if rule != nil && len(rule.Quota) > 0 {
			changed = true
			rule.Quota = nil
		}
	}

	status.Rule.Quota = nil

	return changed
}
