// SPDX-FileCopyrightText: Copyright 2025 Stacklok, Inc.
// SPDX-License-Identifier: Apache-2.0

package controllers

import (
	"context"
	"fmt"
	"time"

	"k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/api/meta"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/types"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
	"sigs.k8s.io/controller-runtime/pkg/handler"
	"sigs.k8s.io/controller-runtime/pkg/log"
	"sigs.k8s.io/controller-runtime/pkg/reconcile"

	mcpv1beta1 "github.com/stacklok/toolhive/cmd/thv-operator/api/v1beta1"
	ctrlutil "github.com/stacklok/toolhive/cmd/thv-operator/pkg/controllerutil"
)

const (
	// ExternalAuthConfigFinalizerName is the name of the finalizer for MCPExternalAuthConfig
	ExternalAuthConfigFinalizerName = "mcpexternalauthconfig.toolhive.stacklok.dev/finalizer"

	// externalAuthConfigRequeueDelay is the delay before requeuing after adding a finalizer
	externalAuthConfigRequeueDelay = 500 * time.Millisecond

	// authServerRefKindMCPExternalAuthConfig is the Kind value on a TypedLocalObjectReference
	// that identifies the ref as pointing to an MCPExternalAuthConfig resource.
	authServerRefKindMCPExternalAuthConfig = "MCPExternalAuthConfig"
)

// MCPExternalAuthConfigReconciler reconciles a MCPExternalAuthConfig object
type MCPExternalAuthConfigReconciler struct {
	client.Client
	Scheme *runtime.Scheme
}

// +kubebuilder:rbac:groups=toolhive.stacklok.dev,resources=mcpexternalauthconfigs,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=toolhive.stacklok.dev,resources=mcpexternalauthconfigs/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=toolhive.stacklok.dev,resources=mcpexternalauthconfigs/finalizers,verbs=update
// +kubebuilder:rbac:groups=toolhive.stacklok.dev,resources=mcpservers,verbs=get;list;watch;update;patch

// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state.
func (r *MCPExternalAuthConfigReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	logger := log.FromContext(ctx)

	// Fetch the MCPExternalAuthConfig instance
	externalAuthConfig := &mcpv1beta1.MCPExternalAuthConfig{}
	err := r.Get(ctx, req.NamespacedName, externalAuthConfig)
	if err != nil {
		if errors.IsNotFound(err) {
			// Object not found, could have been deleted after reconcile request.
			// Return and don't requeue
			logger.Info("MCPExternalAuthConfig resource not found. Ignoring since object must be deleted")
			return ctrl.Result{}, nil
		}
		// Error reading the object - requeue the request.
		logger.Error(err, "Failed to get MCPExternalAuthConfig")
		return ctrl.Result{}, err
	}

	// Check if the MCPExternalAuthConfig is being deleted
	if !externalAuthConfig.DeletionTimestamp.IsZero() {
		return r.handleDeletion(ctx, externalAuthConfig)
	}

	// Add finalizer if it doesn't exist
	if !controllerutil.ContainsFinalizer(externalAuthConfig, ExternalAuthConfigFinalizerName) {
		controllerutil.AddFinalizer(externalAuthConfig, ExternalAuthConfigFinalizerName)
		if err := r.Update(ctx, externalAuthConfig); err != nil {
			logger.Error(err, "Failed to add finalizer")
			return ctrl.Result{}, err
		}
		// Requeue to continue processing after finalizer is added
		return ctrl.Result{RequeueAfter: externalAuthConfigRequeueDelay}, nil
	}

	// Compute the IdentitySynthesized advisory upfront, before validation.
	// The advisory is a pure function of the upstream provider field shape
	// (specifically, which OAuth2 upstreams have nil userInfo) and does not
	// depend on issuer URL validity or other Validate() concerns. Computing
	// it before validation ensures the advisory tracks the current spec on
	// every reconcile — including the validation-failure path — so a broken
	// edit cannot leave a stale True/upstream-name dangling.
	syntheticChanged := r.applyIdentitySynthesizedCondition(externalAuthConfig)

	// Validate spec configuration early
	if err := externalAuthConfig.Validate(); err != nil {
		logger.Error(err, "MCPExternalAuthConfig spec validation failed")
		// Update status with validation error. The synthesis condition mutated
		// above is part of the same in-memory Conditions slice and will land
		// in this same write.
		meta.SetStatusCondition(&externalAuthConfig.Status.Conditions, metav1.Condition{
			Type:               mcpv1beta1.ConditionTypeValid,
			Status:             metav1.ConditionFalse,
			Reason:             "ValidationFailed",
			Message:            err.Error(),
			ObservedGeneration: externalAuthConfig.Generation,
		})
		if updateErr := r.Status().Update(ctx, externalAuthConfig); updateErr != nil {
			logger.Error(updateErr, "Failed to update status after validation error")
		}
		return ctrl.Result{}, nil // Don't requeue on validation errors - user must fix spec
	}

	// Validation succeeded - set Valid=True condition
	conditionChanged := meta.SetStatusCondition(&externalAuthConfig.Status.Conditions, metav1.Condition{
		Type:               mcpv1beta1.ConditionTypeValid,
		Status:             metav1.ConditionTrue,
		Reason:             "ValidationSucceeded",
		Message:            "Spec validation passed",
		ObservedGeneration: externalAuthConfig.Generation,
	})
	if syntheticChanged {
		conditionChanged = true
	}

	// Calculate the hash of the current configuration
	configHash := r.calculateConfigHash(externalAuthConfig.Spec)

	// Check if the hash has changed
	hashChanged := externalAuthConfig.Status.ConfigHash != configHash
	if hashChanged {
		return r.handleConfigHashChange(ctx, externalAuthConfig, configHash)
	}

	// Update condition if it changed (even without hash change)
	if conditionChanged {
		if err := r.Status().Update(ctx, externalAuthConfig); err != nil {
			logger.Error(err, "Failed to update MCPExternalAuthConfig status after condition change")
			return ctrl.Result{}, err
		}
	}

	// Even when hash hasn't changed, update referencing workloads list.
	// This ensures ReferencingWorkloads is updated when MCPServers are created or deleted.
	return r.updateReferencingWorkloads(ctx, externalAuthConfig)
}

// calculateConfigHash calculates a hash of the MCPExternalAuthConfig spec using Kubernetes utilities
func (*MCPExternalAuthConfigReconciler) calculateConfigHash(spec mcpv1beta1.MCPExternalAuthConfigSpec) string {
	return ctrlutil.CalculateConfigHash(spec)
}

// applyIdentitySynthesizedCondition sets ConditionTypeIdentitySynthesized
// True when any OAuth2 upstream has nil userInfo, False when every upstream
// has userInfo configured, and removes it for non-embeddedAuthServer types
// where the question is moot. Returns true if the in-memory condition list
// changed so the caller can fold this into the next status write.
func (*MCPExternalAuthConfigReconciler) applyIdentitySynthesizedCondition(
	cfg *mcpv1beta1.MCPExternalAuthConfig,
) bool {
	if cfg.Spec.Type != mcpv1beta1.ExternalAuthTypeEmbeddedAuthServer || cfg.Spec.EmbeddedAuthServer == nil {
		return meta.RemoveStatusCondition(&cfg.Status.Conditions, mcpv1beta1.ConditionTypeIdentitySynthesized)
	}

	syntheticUpstreams := cfg.Spec.EmbeddedAuthServer.SyntheticIdentityUpstreams()
	if len(syntheticUpstreams) == 0 {
		return meta.SetStatusCondition(&cfg.Status.Conditions, metav1.Condition{
			Type:               mcpv1beta1.ConditionTypeIdentitySynthesized,
			Status:             metav1.ConditionFalse,
			Reason:             mcpv1beta1.ConditionReasonIdentitySynthesizedInactive,
			Message:            "All OAuth2 upstreams have userInfo configured; user identity is resolved from the upstream",
			ObservedGeneration: cfg.Generation,
		})
	}

	return meta.SetStatusCondition(&cfg.Status.Conditions, metav1.Condition{
		Type:   mcpv1beta1.ConditionTypeIdentitySynthesized,
		Status: metav1.ConditionTrue,
		Reason: mcpv1beta1.ConditionReasonIdentitySynthesizedActive,
		Message: fmt.Sprintf(
			"OAuth2 upstream(s) %v have no userInfo configured; the embedded auth server will "+
				"synthesize a non-PII subject from the access token (no Name/Email claims). "+
				"If a userInfo endpoint exists for these upstreams, configure it to resolve real identity.",
			syntheticUpstreams,
		),
		ObservedGeneration: cfg.Generation,
	})
}

// handleConfigHashChange handles the logic when the config hash changes
func (r *MCPExternalAuthConfigReconciler) handleConfigHashChange(
	ctx context.Context,
	externalAuthConfig *mcpv1beta1.MCPExternalAuthConfig,
	configHash string,
) (ctrl.Result, error) {
	logger := log.FromContext(ctx)
	logger.Info("MCPExternalAuthConfig configuration changed",
		"oldHash", externalAuthConfig.Status.ConfigHash,
		"newHash", configHash)

	// Update the status with the new hash
	externalAuthConfig.Status.ConfigHash = configHash
	externalAuthConfig.Status.ObservedGeneration = externalAuthConfig.Generation

	// Find all MCPServers that reference this MCPExternalAuthConfig
	referencingServers, err := r.findReferencingMCPServers(ctx, externalAuthConfig)
	if err != nil {
		logger.Error(err, "Failed to find referencing MCPServers")
		return ctrl.Result{}, fmt.Errorf("failed to find referencing MCPServers: %w", err)
	}

	// Update the status with the list of referencing workloads
	refs := make([]mcpv1beta1.WorkloadReference, 0, len(referencingServers))
	for _, server := range referencingServers {
		refs = append(refs, mcpv1beta1.WorkloadReference{Kind: mcpv1beta1.WorkloadKindMCPServer, Name: server.Name})
	}
	ctrlutil.SortWorkloadRefs(refs)
	externalAuthConfig.Status.ReferencingWorkloads = refs

	// Update the MCPExternalAuthConfig status
	if err := r.Status().Update(ctx, externalAuthConfig); err != nil {
		logger.Error(err, "Failed to update MCPExternalAuthConfig status")
		return ctrl.Result{}, err
	}

	// Trigger reconciliation of all referencing MCPServers
	for _, server := range referencingServers {
		logger.Info("Triggering reconciliation of MCPServer due to MCPExternalAuthConfig change",
			"mcpserver", server.Name, "externalAuthConfig", externalAuthConfig.Name)

		// Add an annotation to the MCPServer to trigger reconciliation.
		if err := ctrlutil.MutateAndPatchSpec(ctx, r.Client, &server, func(m *mcpv1beta1.MCPServer) {
			if m.Annotations == nil {
				m.Annotations = make(map[string]string)
			}
			m.Annotations["toolhive.stacklok.dev/externalauthconfig-hash"] = configHash
		}); err != nil {
			logger.Error(err, "Failed to patch MCPServer annotation", "mcpserver", server.Name)
			// Continue with other servers even if one fails
		}
	}

	return ctrl.Result{}, nil
}

// handleDeletion handles the deletion of a MCPExternalAuthConfig
func (r *MCPExternalAuthConfigReconciler) handleDeletion(
	ctx context.Context,
	externalAuthConfig *mcpv1beta1.MCPExternalAuthConfig,
) (ctrl.Result, error) {
	logger := log.FromContext(ctx)

	if controllerutil.ContainsFinalizer(externalAuthConfig, ExternalAuthConfigFinalizerName) {
		// Check if any workloads still reference this MCPExternalAuthConfig
		referencingWorkloads, err := r.findReferencingWorkloads(ctx, externalAuthConfig)
		if err != nil {
			logger.Error(err, "Failed to check referencing workloads during deletion")
			return ctrl.Result{}, err
		}

		if len(referencingWorkloads) > 0 {
			logger.Info("MCPExternalAuthConfig is still referenced by workloads, blocking deletion",
				"externalAuthConfig", externalAuthConfig.Name,
				"referencingWorkloads", referencingWorkloads)

			meta.SetStatusCondition(&externalAuthConfig.Status.Conditions, metav1.Condition{
				Type:               mcpv1beta1.ConditionTypeDeletionBlocked,
				Status:             metav1.ConditionTrue,
				Reason:             "ReferencedByWorkloads",
				Message:            fmt.Sprintf("Cannot delete: referenced by workloads: %v", referencingWorkloads),
				ObservedGeneration: externalAuthConfig.Generation,
			})
			externalAuthConfig.Status.ReferencingWorkloads = referencingWorkloads
			if updateErr := r.Status().Update(ctx, externalAuthConfig); updateErr != nil {
				logger.Error(updateErr, "Failed to update status during deletion block")
			}

			// Requeue to check again later
			return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
		}

		// No references, safe to remove finalizer and allow deletion
		controllerutil.RemoveFinalizer(externalAuthConfig, ExternalAuthConfigFinalizerName)
		if err := r.Update(ctx, externalAuthConfig); err != nil {
			logger.Error(err, "Failed to remove finalizer")
			return ctrl.Result{}, err
		}
		logger.Info("Removed finalizer from MCPExternalAuthConfig", "externalAuthConfig", externalAuthConfig.Name)
	}

	return ctrl.Result{}, nil
}

// findReferencingMCPServers finds all MCPServers that reference the given MCPExternalAuthConfig
// via either externalAuthConfigRef or authServerRef.
// It queries separately for each ref field and merges with deduplication, so a server
// that has externalAuthConfigRef pointing to config "A" and authServerRef pointing to
// config "B" will be found when reconciling either config.
func (r *MCPExternalAuthConfigReconciler) findReferencingMCPServers(
	ctx context.Context,
	externalAuthConfig *mcpv1beta1.MCPExternalAuthConfig,
) ([]mcpv1beta1.MCPServer, error) {
	byExtAuth, err := ctrlutil.FindReferencingMCPServers(ctx, r.Client, externalAuthConfig.Namespace, externalAuthConfig.Name,
		func(server *mcpv1beta1.MCPServer) *string {
			if server.Spec.ExternalAuthConfigRef != nil {
				return &server.Spec.ExternalAuthConfigRef.Name
			}
			return nil
		})
	if err != nil {
		return nil, err
	}

	byAuthServer, err := ctrlutil.FindReferencingMCPServers(ctx, r.Client, externalAuthConfig.Namespace, externalAuthConfig.Name,
		func(server *mcpv1beta1.MCPServer) *string {
			if server.Spec.AuthServerRef != nil && server.Spec.AuthServerRef.Kind == authServerRefKindMCPExternalAuthConfig {
				return &server.Spec.AuthServerRef.Name
			}
			return nil
		})
	if err != nil {
		return nil, err
	}

	// Merge and deduplicate
	seen := make(map[string]struct{}, len(byExtAuth))
	result := make([]mcpv1beta1.MCPServer, 0, len(byExtAuth)+len(byAuthServer))
	for _, s := range byExtAuth {
		seen[s.Name] = struct{}{}
		result = append(result, s)
	}
	for _, s := range byAuthServer {
		if _, ok := seen[s.Name]; !ok {
			result = append(result, s)
		}
	}
	return result, nil
}

// findReferencingMCPRemoteProxies finds all MCPRemoteProxies that reference the given MCPExternalAuthConfig
// via either externalAuthConfigRef or authServerRef.
// It queries separately for each ref field and merges with deduplication, so a proxy
// that has externalAuthConfigRef pointing to config "A" and authServerRef pointing to
// config "B" will be found when reconciling either config.
func (r *MCPExternalAuthConfigReconciler) findReferencingMCPRemoteProxies(
	ctx context.Context,
	externalAuthConfig *mcpv1beta1.MCPExternalAuthConfig,
) ([]mcpv1beta1.MCPRemoteProxy, error) {
	byExtAuth, err := ctrlutil.FindReferencingMCPRemoteProxies(
		ctx, r.Client, externalAuthConfig.Namespace, externalAuthConfig.Name,
		func(proxy *mcpv1beta1.MCPRemoteProxy) *string {
			if proxy.Spec.ExternalAuthConfigRef != nil {
				return &proxy.Spec.ExternalAuthConfigRef.Name
			}
			return nil
		})
	if err != nil {
		return nil, err
	}

	byAuthServer, err := ctrlutil.FindReferencingMCPRemoteProxies(
		ctx, r.Client, externalAuthConfig.Namespace, externalAuthConfig.Name,
		func(proxy *mcpv1beta1.MCPRemoteProxy) *string {
			if proxy.Spec.AuthServerRef != nil && proxy.Spec.AuthServerRef.Kind == authServerRefKindMCPExternalAuthConfig {
				return &proxy.Spec.AuthServerRef.Name
			}
			return nil
		})
	if err != nil {
		return nil, err
	}

	// Merge and deduplicate
	seen := make(map[string]struct{}, len(byExtAuth))
	result := make([]mcpv1beta1.MCPRemoteProxy, 0, len(byExtAuth)+len(byAuthServer))
	for _, p := range byExtAuth {
		seen[p.Name] = struct{}{}
		result = append(result, p)
	}
	for _, p := range byAuthServer {
		if _, ok := seen[p.Name]; !ok {
			result = append(result, p)
		}
	}
	return result, nil
}

// findReferencingWorkloads returns the workload resources (MCPServer and MCPRemoteProxy)
// that reference this MCPExternalAuthConfig via their ExternalAuthConfigRef or AuthServerRef field.
// It queries separately for each ref field and merges the results, so both fields are always checked.
func (r *MCPExternalAuthConfigReconciler) findReferencingWorkloads(
	ctx context.Context,
	externalAuthConfig *mcpv1beta1.MCPExternalAuthConfig,
) ([]mcpv1beta1.WorkloadReference, error) {
	servers, err := r.findReferencingMCPServers(ctx, externalAuthConfig)
	if err != nil {
		return nil, err
	}
	refs := make([]mcpv1beta1.WorkloadReference, 0, len(servers))
	for _, server := range servers {
		refs = append(refs, mcpv1beta1.WorkloadReference{Kind: mcpv1beta1.WorkloadKindMCPServer, Name: server.Name})
	}

	proxies, err := r.findReferencingMCPRemoteProxies(ctx, externalAuthConfig)
	if err != nil {
		return nil, err
	}
	for _, proxy := range proxies {
		refs = append(refs, mcpv1beta1.WorkloadReference{Kind: mcpv1beta1.WorkloadKindMCPRemoteProxy, Name: proxy.Name})
	}

	ctrlutil.SortWorkloadRefs(refs)
	return refs, nil
}

// SetupWithManager sets up the controller with the Manager.
// Watches MCPServer and MCPRemoteProxy changes to maintain accurate ReferencingWorkloads status.
func (r *MCPExternalAuthConfigReconciler) SetupWithManager(mgr ctrl.Manager) error {
	return ctrl.NewControllerManagedBy(mgr).
		For(&mcpv1beta1.MCPExternalAuthConfig{}).
		Watches(
			&mcpv1beta1.MCPServer{},
			handler.EnqueueRequestsFromMapFunc(r.mapMCPServerToExternalAuthConfig),
		).
		Watches(
			&mcpv1beta1.MCPRemoteProxy{},
			handler.EnqueueRequestsFromMapFunc(r.mapMCPRemoteProxyToExternalAuthConfig),
		).
		Complete(r)
}

// mapMCPServerToExternalAuthConfig maps MCPServer changes to MCPExternalAuthConfig reconciliation requests.
// Enqueues both the currently-referenced config(s) and any config that still lists this
// MCPServer in ReferencingWorkloads (handles ref-removal / deletion).
func (r *MCPExternalAuthConfigReconciler) mapMCPServerToExternalAuthConfig(
	ctx context.Context, obj client.Object,
) []reconcile.Request {
	server, ok := obj.(*mcpv1beta1.MCPServer)
	if !ok {
		return nil
	}

	seen := make(map[types.NamespacedName]struct{})
	var requests []reconcile.Request

	// Enqueue the currently-referenced MCPExternalAuthConfig (if any)
	if server.Spec.ExternalAuthConfigRef != nil {
		nn := types.NamespacedName{
			Name:      server.Spec.ExternalAuthConfigRef.Name,
			Namespace: server.Namespace,
		}
		seen[nn] = struct{}{}
		requests = append(requests, reconcile.Request{NamespacedName: nn})
	}

	// Enqueue the MCPExternalAuthConfig referenced via authServerRef (if any)
	if server.Spec.AuthServerRef != nil && server.Spec.AuthServerRef.Kind == authServerRefKindMCPExternalAuthConfig {
		nn := types.NamespacedName{
			Name:      server.Spec.AuthServerRef.Name,
			Namespace: server.Namespace,
		}
		if _, already := seen[nn]; !already {
			seen[nn] = struct{}{}
			requests = append(requests, reconcile.Request{NamespacedName: nn})
		}
	}

	// Also enqueue any MCPExternalAuthConfig that still lists this server in
	// ReferencingWorkloads — handles ref-removal and server-deletion cases.
	extAuthConfigList := &mcpv1beta1.MCPExternalAuthConfigList{}
	if err := r.List(ctx, extAuthConfigList, client.InNamespace(server.Namespace)); err != nil {
		log.FromContext(ctx).Error(err, "Failed to list MCPExternalAuthConfigs for MCPServer watch")
		return requests
	}
	for _, cfg := range extAuthConfigList.Items {
		nn := types.NamespacedName{Name: cfg.Name, Namespace: cfg.Namespace}
		if _, already := seen[nn]; already {
			continue
		}
		for _, ref := range cfg.Status.ReferencingWorkloads {
			if ref.Kind == mcpv1beta1.WorkloadKindMCPServer && ref.Name == server.Name {
				requests = append(requests, reconcile.Request{NamespacedName: nn})
				break
			}
		}
	}

	return requests
}

// mapMCPRemoteProxyToExternalAuthConfig maps MCPRemoteProxy changes to MCPExternalAuthConfig reconciliation requests.
// Enqueues both the currently-referenced config(s) and any config that still lists this
// MCPRemoteProxy in ReferencingWorkloads (handles ref-removal / deletion).
func (r *MCPExternalAuthConfigReconciler) mapMCPRemoteProxyToExternalAuthConfig(
	ctx context.Context, obj client.Object,
) []reconcile.Request {
	proxy, ok := obj.(*mcpv1beta1.MCPRemoteProxy)
	if !ok {
		return nil
	}

	seen := make(map[types.NamespacedName]struct{})
	var requests []reconcile.Request

	// Enqueue the currently-referenced MCPExternalAuthConfig via externalAuthConfigRef (if any)
	if proxy.Spec.ExternalAuthConfigRef != nil {
		nn := types.NamespacedName{
			Name:      proxy.Spec.ExternalAuthConfigRef.Name,
			Namespace: proxy.Namespace,
		}
		seen[nn] = struct{}{}
		requests = append(requests, reconcile.Request{NamespacedName: nn})
	}

	// Enqueue the MCPExternalAuthConfig referenced via authServerRef (if any)
	if proxy.Spec.AuthServerRef != nil && proxy.Spec.AuthServerRef.Kind == authServerRefKindMCPExternalAuthConfig {
		nn := types.NamespacedName{
			Name:      proxy.Spec.AuthServerRef.Name,
			Namespace: proxy.Namespace,
		}
		if _, already := seen[nn]; !already {
			seen[nn] = struct{}{}
			requests = append(requests, reconcile.Request{NamespacedName: nn})
		}
	}

	// Also enqueue any MCPExternalAuthConfig that still lists this proxy in
	// ReferencingWorkloads — handles ref-removal and proxy-deletion cases.
	extAuthConfigList := &mcpv1beta1.MCPExternalAuthConfigList{}
	if err := r.List(ctx, extAuthConfigList, client.InNamespace(proxy.Namespace)); err != nil {
		log.FromContext(ctx).Error(err, "Failed to list MCPExternalAuthConfigs for MCPRemoteProxy watch")
		return requests
	}
	for _, cfg := range extAuthConfigList.Items {
		nn := types.NamespacedName{Name: cfg.Name, Namespace: cfg.Namespace}
		if _, already := seen[nn]; already {
			continue
		}
		for _, ref := range cfg.Status.ReferencingWorkloads {
			if ref.Kind == mcpv1beta1.WorkloadKindMCPRemoteProxy && ref.Name == proxy.Name {
				requests = append(requests, reconcile.Request{NamespacedName: nn})
				break
			}
		}
	}

	return requests
}

// updateReferencingWorkloads finds referencing workloads and updates the status if the list changed
func (r *MCPExternalAuthConfigReconciler) updateReferencingWorkloads(
	ctx context.Context,
	externalAuthConfig *mcpv1beta1.MCPExternalAuthConfig,
) (ctrl.Result, error) {
	refs, err := r.findReferencingWorkloads(ctx, externalAuthConfig)
	if err != nil {
		logger := log.FromContext(ctx)
		logger.Error(err, "Failed to find referencing workloads")
		return ctrl.Result{}, fmt.Errorf("failed to find referencing workloads: %w", err)
	}

	if !ctrlutil.WorkloadRefsEqual(externalAuthConfig.Status.ReferencingWorkloads, refs) {
		externalAuthConfig.Status.ReferencingWorkloads = refs
		if err := r.Status().Update(ctx, externalAuthConfig); err != nil {
			logger := log.FromContext(ctx)
			logger.Error(err, "Failed to update MCPExternalAuthConfig status")
			return ctrl.Result{}, err
		}
	}

	return ctrl.Result{}, nil
}

// GetExternalAuthConfigForMCPServer retrieves the MCPExternalAuthConfig referenced by an MCPServer.
// This function is exported for use by the MCPServer controller (Phase 5 integration).
func GetExternalAuthConfigForMCPServer(
	ctx context.Context,
	c client.Client,
	mcpServer *mcpv1beta1.MCPServer,
) (*mcpv1beta1.MCPExternalAuthConfig, error) {
	if mcpServer.Spec.ExternalAuthConfigRef == nil {
		// We throw an error because in this case you assume there is a ExternalAuthConfig
		// but there isn't one referenced.
		return nil, fmt.Errorf("MCPServer %s does not reference a MCPExternalAuthConfig", mcpServer.Name)
	}

	externalAuthConfig := &mcpv1beta1.MCPExternalAuthConfig{}
	err := c.Get(ctx, types.NamespacedName{
		Name:      mcpServer.Spec.ExternalAuthConfigRef.Name,
		Namespace: mcpServer.Namespace, // Same namespace as MCPServer
	}, externalAuthConfig)

	if err != nil {
		if errors.IsNotFound(err) {
			return nil, fmt.Errorf("MCPExternalAuthConfig %s not found in namespace %s",
				mcpServer.Spec.ExternalAuthConfigRef.Name, mcpServer.Namespace)
		}
		return nil, fmt.Errorf("failed to get MCPExternalAuthConfig: %w", err)
	}

	return externalAuthConfig, nil
}
