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

package v1beta1

import (
	"fmt"

	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"

	vmcptypes "github.com/stacklok/toolhive/pkg/vmcp"
	"github.com/stacklok/toolhive/pkg/vmcp/config"
)

// VirtualMCPServerSpec defines the desired state of VirtualMCPServer
//
// +kubebuilder:validation:XValidation:rule="!has(self.config) || !has(self.config.rateLimiting) || (has(self.sessionStorage) && self.sessionStorage.provider == 'redis')",message="config.rateLimiting requires sessionStorage with provider 'redis'"
// +kubebuilder:validation:XValidation:rule="!(has(self.config) && has(self.config.rateLimiting) && has(self.config.rateLimiting.perUser)) || (has(self.incomingAuth) && self.incomingAuth.type == 'oidc')",message="config.rateLimiting.perUser requires incomingAuth.type oidc"
// +kubebuilder:validation:XValidation:rule="!has(self.config) || !has(self.config.rateLimiting) || !has(self.config.rateLimiting.tools) || self.config.rateLimiting.tools.all(t, !has(t.perUser)) || (has(self.incomingAuth) && self.incomingAuth.type == 'oidc')",message="per-tool perUser rate limiting requires incomingAuth.type oidc"
//
//nolint:lll // CEL validation rules exceed line length limit
type VirtualMCPServerSpec struct {
	// IncomingAuth configures authentication for clients connecting to the Virtual MCP server.
	// Must be explicitly set - use "anonymous" type when no authentication is required.
	// This field takes precedence over config.IncomingAuth and should be preferred because it
	// supports Kubernetes-native secret references (SecretKeyRef, ConfigMapRef) for secure
	// dynamic discovery of credentials, rather than requiring secrets to be embedded in config.
	// +kubebuilder:validation:Required
	IncomingAuth *IncomingAuthConfig `json:"incomingAuth"`

	// OutgoingAuth configures authentication from Virtual MCP to backend MCPServers.
	// This field takes precedence over config.OutgoingAuth and should be preferred because it
	// supports Kubernetes-native secret references (SecretKeyRef, ConfigMapRef) for secure
	// dynamic discovery of credentials, rather than requiring secrets to be embedded in config.
	// +optional
	OutgoingAuth *OutgoingAuthConfig `json:"outgoingAuth,omitempty"`

	// ServiceType specifies the Kubernetes service type for the Virtual MCP server
	// +kubebuilder:validation:Enum=ClusterIP;NodePort;LoadBalancer
	// +kubebuilder:default=ClusterIP
	// +optional
	ServiceType string `json:"serviceType,omitempty"`

	// SessionAffinity controls whether the Service routes repeated client connections to the same pod.
	// MCP protocols (SSE, streamable-http) are stateful, so ClientIP is the default.
	// Set to "None" for stateless servers or when using an external load balancer with its own affinity.
	// +kubebuilder:validation:Enum=ClientIP;None
	// +kubebuilder:default=ClientIP
	// +optional
	SessionAffinity string `json:"sessionAffinity,omitempty"`

	// ServiceAccount is the name of an already existing service account to use by the Virtual MCP server.
	// If not specified, a ServiceAccount will be created automatically and used by the Virtual MCP server.
	// +optional
	ServiceAccount *string `json:"serviceAccount,omitempty"`

	// PodTemplateSpec defines the pod template to use for the Virtual MCP server
	// This allows for customizing the pod configuration beyond what is provided by the other fields.
	// Note that to modify the specific container the Virtual MCP server runs in, you must specify
	// the 'vmcp' container name in the PodTemplateSpec.
	// This field accepts a PodTemplateSpec object as JSON/YAML.
	// +optional
	// +kubebuilder:pruning:PreserveUnknownFields
	// +kubebuilder:validation:Type=object
	PodTemplateSpec *runtime.RawExtension `json:"podTemplateSpec,omitempty"`

	// GroupRef references the MCPGroup that defines backend workloads.
	// The referenced MCPGroup must exist in the same namespace.
	// +kubebuilder:validation:Required
	GroupRef *MCPGroupRef `json:"groupRef"`

	// Config is the Virtual MCP server configuration.
	// The audit config from here is also supported, but not required.
	// +optional
	Config config.Config `json:"config,omitempty"`

	// TelemetryConfigRef references an MCPTelemetryConfig resource for shared telemetry configuration.
	// The referenced MCPTelemetryConfig must exist in the same namespace as this VirtualMCPServer.
	// Cross-namespace references are not supported for security and isolation reasons.
	// +optional
	TelemetryConfigRef *MCPTelemetryConfigReference `json:"telemetryConfigRef,omitempty"`

	// EmbeddingServerRef references an existing EmbeddingServer resource by name.
	// When the optimizer is enabled, this field is required to point to a ready EmbeddingServer
	// that provides embedding capabilities.
	// The referenced EmbeddingServer must exist in the same namespace and be ready.
	// +optional
	EmbeddingServerRef *EmbeddingServerRef `json:"embeddingServerRef,omitempty"`

	// AuthServerConfig configures an embedded OAuth authorization server.
	// When set, the vMCP server acts as an OIDC issuer, drives users through
	// upstream IDPs, and issues ToolHive JWTs. The embedded AS becomes the
	// IncomingAuth OIDC provider — its issuer must match IncomingAuth.OIDCConfigRef
	// so that tokens it issues are accepted by the vMCP's incoming auth middleware.
	// When nil, IncomingAuth uses an external IDP and behavior is unchanged.
	// +optional
	AuthServerConfig *EmbeddedAuthServerConfig `json:"authServerConfig,omitempty"`

	// Replicas is the desired number of vMCP pod replicas.
	// VirtualMCPServer creates a single Deployment for the vMCP aggregator process,
	// so there is only one replicas field (unlike MCPServer which has separate
	// Replicas and BackendReplicas for its two Deployments).
	// When nil, the operator does not set Deployment.Spec.Replicas, leaving replica
	// management to an HPA or other external controller.
	// +kubebuilder:validation:Minimum=0
	// +optional
	Replicas *int32 `json:"replicas,omitempty"`

	// SessionStorage configures session storage for stateful horizontal scaling.
	// When nil, no session storage is configured.
	// +optional
	SessionStorage *SessionStorageConfig `json:"sessionStorage,omitempty"`

	// ImagePullSecrets allows specifying image pull secrets for the vMCP workload.
	// These are applied to both the vMCP Deployment's PodSpec.ImagePullSecrets
	// and to the operator-managed ServiceAccount the vMCP server runs as, so private
	// images are pullable through either path.
	//
	// Merge semantics with PodTemplateSpec:
	// The deployed PodSpec.ImagePullSecrets is the Kubernetes-native strategic-merge
	// union of this field and spec.podTemplateSpec.spec.imagePullSecrets, merged by
	// the patchStrategy:"merge" / patchMergeKey:"name" tags on corev1.PodSpec.
	//   - This field is rendered first as the controller-generated default.
	//   - spec.podTemplateSpec.spec.imagePullSecrets is then strategic-merge-patched
	//     on top, keyed by Name. Distinct names from the two sources are unioned in
	//     the resulting list; entries with the same Name are deduplicated and the
	//     PodTemplateSpec entry wins on overlap (user override).
	//   - Order in the resulting list is not guaranteed and should not be relied on:
	//     strategic merge by name is order-insensitive.
	//   - The operator-managed ServiceAccount's imagePullSecrets list is populated
	//     ONLY from this field. spec.podTemplateSpec.spec.imagePullSecrets does not
	//     reach the ServiceAccount because PodTemplateSpec has no notion of a
	//     ServiceAccount. To make a secret usable via the ServiceAccount path
	//     (e.g. for sidecars or init containers that pull images independently),
	//     list it here rather than under spec.podTemplateSpec.
	//
	// Note on cross-CRD consistency:
	// MCPRegistry currently uses an atomic-replace strategy for its imagePullSecrets
	// (the user-provided value replaces the controller-generated list rather than
	// being merged on top). VirtualMCPServer follows the Kubernetes-native
	// strategic-merge-by-name behavior described above. Aligning the two is tracked
	// as a separate follow-up; until then, manifests that set imagePullSecrets on
	// both CRDs will see different override behavior between them.
	//
	// +listType=atomic
	// +optional
	ImagePullSecrets []corev1.LocalObjectReference `json:"imagePullSecrets,omitempty"`
}

// EmbeddingServerRef references an existing EmbeddingServer resource by name.
// This follows the same pattern as ExternalAuthConfigRef and ToolConfigRef.
type EmbeddingServerRef struct {
	// Name is the name of the EmbeddingServer resource
	// +kubebuilder:validation:Required
	Name string `json:"name"`
}

// IncomingAuthConfig configures authentication for clients connecting to the Virtual MCP server
//
// +kubebuilder:validation:XValidation:rule="self.type == 'oidc' ? has(self.oidcConfigRef) : true",message="spec.incomingAuth.oidcConfigRef is required when type is oidc"
//
//nolint:lll // CEL validation rules exceed line length limit
type IncomingAuthConfig struct {
	// Type defines the authentication type: anonymous or oidc
	// When no authentication is required, explicitly set this to "anonymous"
	// +kubebuilder:validation:Enum=anonymous;oidc
	// +kubebuilder:validation:Required
	Type string `json:"type"`

	// OIDCConfigRef references a shared MCPOIDCConfig resource for OIDC authentication.
	// The referenced MCPOIDCConfig must exist in the same namespace as this VirtualMCPServer.
	// Per-server overrides (audience, scopes) are specified here; shared provider config
	// lives in the MCPOIDCConfig resource.
	// +optional
	OIDCConfigRef *MCPOIDCConfigReference `json:"oidcConfigRef,omitempty"`

	// AuthzConfig defines authorization policy configuration
	// Reuses MCPServer authz patterns
	// +optional
	AuthzConfig *AuthzConfigRef `json:"authzConfig,omitempty"`
}

// OutgoingAuthConfig configures authentication from Virtual MCP to backend MCPServers
type OutgoingAuthConfig struct {
	// Source defines how backend authentication configurations are determined
	// - discovered: Automatically discover from backend's MCPServer.spec.externalAuthConfigRef
	// - inline: Explicit per-backend configuration in VirtualMCPServer
	// +kubebuilder:validation:Enum=discovered;inline
	// +kubebuilder:default=discovered
	// +optional
	Source string `json:"source,omitempty"`

	// Default defines default behavior for backends without explicit auth config
	// +optional
	Default *BackendAuthConfig `json:"default,omitempty"`

	// Backends defines per-backend authentication overrides
	// Works in all modes (discovered, inline)
	// +optional
	Backends map[string]BackendAuthConfig `json:"backends,omitempty"`
}

// BackendAuthConfig defines authentication configuration for a backend MCPServer
type BackendAuthConfig struct {
	// Type defines the authentication type
	// +kubebuilder:validation:Enum=discovered;externalAuthConfigRef
	// +kubebuilder:validation:Required
	Type string `json:"type"`

	// ExternalAuthConfigRef references an MCPExternalAuthConfig resource
	// Only used when Type is "externalAuthConfigRef"
	// +optional
	ExternalAuthConfigRef *ExternalAuthConfigRef `json:"externalAuthConfigRef,omitempty"`
}

// OperationalConfig defines operational settings

// Backend status constants for DiscoveredBackend.Status
// These are the user-facing values stored in VirtualMCPServer.Status.DiscoveredBackends.
// Use BackendHealthStatus.ToCRDStatus() to convert from internal health status.
const (
	BackendStatusReady           = "ready"
	BackendStatusUnavailable     = "unavailable"
	BackendStatusDegraded        = "degraded"
	BackendStatusUnknown         = "unknown"
	BackendStatusUnauthenticated = "unauthenticated"
)

// DiscoveredBackend is an alias to the canonical definition in pkg/vmcp/types.go
// This provides a local name for use in the CRD status.
type DiscoveredBackend = vmcptypes.DiscoveredBackend

// VirtualMCPServerStatus defines the observed state of VirtualMCPServer
type VirtualMCPServerStatus struct {
	// Conditions represent the latest available observations of the VirtualMCPServer's state
	// +listType=map
	// +listMapKey=type
	// +optional
	Conditions []metav1.Condition `json:"conditions,omitempty"`

	// ObservedGeneration is the most recent generation observed for this VirtualMCPServer
	// +optional
	ObservedGeneration int64 `json:"observedGeneration,omitempty"`

	// Phase is the current phase of the VirtualMCPServer
	// +optional
	// +kubebuilder:default=Pending
	Phase VirtualMCPServerPhase `json:"phase,omitempty"`

	// Message provides additional information about the current phase
	// +optional
	Message string `json:"message,omitempty"`

	// URL is the URL where the Virtual MCP server can be accessed
	// +optional
	URL string `json:"url,omitempty"`

	// DiscoveredBackends lists discovered backend configurations from the MCPGroup
	// +listType=map
	// +listMapKey=name
	// +optional
	DiscoveredBackends []DiscoveredBackend `json:"discoveredBackends,omitempty"`

	// BackendCount is the number of routable backends (ready + unauthenticated).
	// Excludes unavailable, degraded, and unknown backends.
	// +optional
	BackendCount int32 `json:"backendCount,omitempty"`

	// OIDCConfigHash is the hash of the referenced MCPOIDCConfig spec for change detection.
	// Only populated when IncomingAuth.OIDCConfigRef is set.
	// +optional
	OIDCConfigHash string `json:"oidcConfigHash,omitempty"`

	// TelemetryConfigHash is the hash of the referenced MCPTelemetryConfig spec for change detection.
	// Only populated when TelemetryConfigRef is set.
	// +optional
	TelemetryConfigHash string `json:"telemetryConfigHash,omitempty"`
}

// VirtualMCPServerPhase represents the lifecycle phase of a VirtualMCPServer
// +kubebuilder:validation:Enum=Pending;Ready;Degraded;Failed
type VirtualMCPServerPhase string

const (
	// VirtualMCPServerPhasePending indicates the VirtualMCPServer is being initialized
	VirtualMCPServerPhasePending VirtualMCPServerPhase = "Pending"

	// VirtualMCPServerPhaseReady indicates the VirtualMCPServer is ready and serving requests
	VirtualMCPServerPhaseReady VirtualMCPServerPhase = "Ready"

	// VirtualMCPServerPhaseDegraded indicates the VirtualMCPServer is running but some backends are unavailable
	VirtualMCPServerPhaseDegraded VirtualMCPServerPhase = "Degraded"

	// VirtualMCPServerPhaseFailed indicates the VirtualMCPServer has failed
	VirtualMCPServerPhaseFailed VirtualMCPServerPhase = "Failed"
)

// Condition types for VirtualMCPServer
// Note: ConditionTypeAuthConfigured is shared with MCPRemoteProxy and defined in mcpremoteproxy_types.go
const (
	// ConditionTypeVirtualMCPServerReady indicates whether the VirtualMCPServer is ready
	ConditionTypeVirtualMCPServerReady = "Ready"

	// ConditionTypeVirtualMCPServerGroupRefValidated indicates whether the GroupRef is valid
	ConditionTypeVirtualMCPServerGroupRefValidated = "GroupRefValidated"

	// ConditionTypeCompositeToolRefsValidated indicates whether the CompositeToolRefs are valid
	ConditionTypeCompositeToolRefsValidated = "CompositeToolRefsValidated"
	// ConditionTypeVirtualMCPServerPodTemplateSpecValid indicates whether the PodTemplateSpec is valid
	ConditionTypeVirtualMCPServerPodTemplateSpecValid = "PodTemplateSpecValid"

	// ConditionTypeVirtualMCPServerBackendsDiscovered indicates whether backends have been discovered
	ConditionTypeVirtualMCPServerBackendsDiscovered = "BackendsDiscovered"

	// ConditionTypeEmbeddingServerReady indicates whether the EmbeddingServer is ready
	ConditionTypeEmbeddingServerReady = "EmbeddingServerReady"

	// ConditionTypeAuthServerConfigValidated indicates whether the AuthServerConfig has been validated
	ConditionTypeAuthServerConfigValidated = "AuthServerConfigValidated"

	// ConditionTypeAuthzUpstreamSelectionWarning is an advisory condition set to True when
	// multiple AuthServerConfig.UpstreamProviders are configured alongside AuthzConfig.
	// Only the first upstream is authoritative for Cedar claim resolution; this warns the
	// operator that the auto-selection has taken effect and names the selected upstream.
	ConditionTypeAuthzUpstreamSelectionWarning = "AuthzUpstreamSelectionWarning"

	// ConditionTypeVirtualMCPServerTelemetryConfigRefValidated indicates whether the TelemetryConfigRef is valid
	ConditionTypeVirtualMCPServerTelemetryConfigRefValidated = "TelemetryConfigRefValidated"
)

// Condition reasons for VirtualMCPServer
const (
	// ConditionReasonIncomingAuthValid indicates incoming auth is valid
	ConditionReasonIncomingAuthValid = "IncomingAuthValid"

	// ConditionReasonIncomingAuthInvalid indicates incoming auth is invalid
	ConditionReasonIncomingAuthInvalid = "IncomingAuthInvalid"

	// ConditionReasonGroupRefValid indicates the GroupRef is valid
	ConditionReasonVirtualMCPServerGroupRefValid = "GroupRefValid"

	// ConditionReasonGroupRefNotFound indicates the referenced MCPGroup was not found
	ConditionReasonVirtualMCPServerGroupRefNotFound = "GroupRefNotFound"

	// ConditionReasonGroupRefNotReady indicates the referenced MCPGroup is not ready
	ConditionReasonVirtualMCPServerGroupRefNotReady = "GroupRefNotReady"

	// ConditionReasonCompositeToolRefsValid indicates the CompositeToolRefs are valid
	ConditionReasonCompositeToolRefsValid = "CompositeToolRefsValid"

	// ConditionReasonCompositeToolRefNotFound indicates a referenced VirtualMCPCompositeToolDefinition was not found
	ConditionReasonCompositeToolRefNotFound = "CompositeToolRefNotFound"

	// ConditionReasonCompositeToolRefInvalid indicates a referenced VirtualMCPCompositeToolDefinition is invalid
	ConditionReasonCompositeToolRefInvalid = "CompositeToolRefInvalid"

	// ConditionReasonVirtualMCPServerPodTemplateSpecValid indicates PodTemplateSpec validation succeeded
	ConditionReasonVirtualMCPServerPodTemplateSpecValid = "PodTemplateSpecValid"

	// ConditionReasonVirtualMCPServerPodTemplateSpecInvalid indicates PodTemplateSpec validation failed
	ConditionReasonVirtualMCPServerPodTemplateSpecInvalid = "InvalidPodTemplateSpec"

	// ConditionReasonVirtualMCPServerBackendsDiscoveredSuccessfully indicates backends were discovered successfully
	ConditionReasonVirtualMCPServerBackendsDiscoveredSuccessfully = "BackendsDiscoveredSuccessfully"

	// ConditionReasonVirtualMCPServerBackendDiscoveryFailed indicates backend discovery failed
	ConditionReasonVirtualMCPServerBackendDiscoveryFailed = "BackendDiscoveryFailed"

	// ConditionReasonVirtualMCPServerDeploymentFailed indicates the deployment failed
	ConditionReasonVirtualMCPServerDeploymentFailed = "DeploymentFailed"

	// ConditionReasonVirtualMCPServerDeploymentReady indicates the deployment is ready
	ConditionReasonVirtualMCPServerDeploymentReady = "DeploymentReady"

	// ConditionReasonVirtualMCPServerDeploymentNotReady indicates the deployment is not ready
	ConditionReasonVirtualMCPServerDeploymentNotReady = "DeploymentNotReady"

	// ConditionReasonEmbeddingServerReady indicates the EmbeddingServer is ready
	ConditionReasonEmbeddingServerReady = "EmbeddingServerReady"

	// ConditionReasonEmbeddingServerNotFound indicates the referenced EmbeddingServer was not found
	ConditionReasonEmbeddingServerNotFound = "EmbeddingServerNotFound"

	// ConditionReasonEmbeddingServerNotReady indicates the referenced EmbeddingServer is not ready
	ConditionReasonEmbeddingServerNotReady = "EmbeddingServerNotReady"

	// ConditionReasonAuthServerConfigValid indicates the AuthServerConfig is valid
	ConditionReasonAuthServerConfigValid = "AuthServerConfigValid"

	// ConditionReasonAuthServerConfigInvalid indicates the AuthServerConfig is invalid
	ConditionReasonAuthServerConfigInvalid = "AuthServerConfigInvalid"

	// ConditionReasonAuthzRequiresUpstream indicates that authorization policies are
	// configured but no upstream IDP is available to source claims from. Without an
	// upstream, Cedar evaluates against the ToolHive-issued AS token, whose claim
	// namespace (sub, aud, tsid) can overlap upstream claims and silently authorize
	// against the wrong identity.
	ConditionReasonAuthzRequiresUpstream = "AuthzRequiresUpstream"

	// ConditionReasonAuthzUpstreamAutoSelected is set when authorization is configured
	// alongside multiple upstream providers and the first upstream has been chosen as
	// the Cedar claim source. The advisory message names the selected upstream.
	ConditionReasonAuthzUpstreamAutoSelected = "AuthzUpstreamAutoSelected"

	// ConditionReasonAuthzUpstreamUnknown indicates that
	// spec.incomingAuth.authzConfig.inline.primaryUpstreamProvider names an upstream
	// IDP that is not declared on spec.authServerConfig.upstreamProviders. Cedar
	// would otherwise deny every request at runtime; reject at admission instead.
	ConditionReasonAuthzUpstreamUnknown = "AuthzUpstreamUnknown"

	// ConditionReasonAuthzPrimaryProviderRequiresAuthServer indicates that
	// spec.incomingAuth.authzConfig.inline.primaryUpstreamProvider is set but
	// spec.authServerConfig is not configured. The field names an upstream IDP
	// on the embedded auth server, which is required for it to take effect.
	// Distinct from AuthzUpstreamUnknown so tooling (alertmanager rules,
	// dashboards) can route the two misconfigurations separately.
	ConditionReasonAuthzPrimaryProviderRequiresAuthServer = "AuthzPrimaryProviderRequiresAuthServer"

	// ConditionReasonVirtualMCPServerTelemetryConfigRefValid indicates the referenced MCPTelemetryConfig is valid
	ConditionReasonVirtualMCPServerTelemetryConfigRefValid = "TelemetryConfigRefValid"

	// ConditionReasonVirtualMCPServerTelemetryConfigRefNotFound indicates the referenced MCPTelemetryConfig was not found
	ConditionReasonVirtualMCPServerTelemetryConfigRefNotFound = "TelemetryConfigRefNotFound"

	// ConditionReasonVirtualMCPServerTelemetryConfigRefInvalid indicates the referenced MCPTelemetryConfig is not valid
	ConditionReasonVirtualMCPServerTelemetryConfigRefInvalid = "TelemetryConfigRefInvalid"

	// ConditionReasonVirtualMCPServerTelemetryConfigRefFetchError indicates a transient error occurred fetching the config
	ConditionReasonVirtualMCPServerTelemetryConfigRefFetchError = "TelemetryConfigRefFetchError"
)

// Backend authentication types
const (
	// BackendAuthTypeDiscovered automatically discovers from backend's externalAuthConfigRef
	BackendAuthTypeDiscovered = "discovered"

	// BackendAuthTypeExternalAuthConfigRef references an MCPExternalAuthConfig resource
	BackendAuthTypeExternalAuthConfigRef = "externalAuthConfigRef"
)

// Workflow step types
const (
	// WorkflowStepTypeToolCall calls a backend tool
	WorkflowStepTypeToolCall = "tool"

	// WorkflowStepTypeElicitation requests user input
	WorkflowStepTypeElicitation = "elicitation"
)

// Error handling actions
const (
	// ErrorActionAbort aborts the workflow on error
	ErrorActionAbort = "abort"

	// ErrorActionContinue continues the workflow on error
	ErrorActionContinue = "continue"

	// ErrorActionRetry retries the step on error
	ErrorActionRetry = "retry"
)

//+kubebuilder:object:root=true
//+kubebuilder:storageversion
//+kubebuilder:subresource:status
//+kubebuilder:resource:shortName=vmcp;virtualmcp,categories=toolhive
//+kubebuilder:printcolumn:name="Phase",type="string",JSONPath=".status.phase",description="The phase of the VirtualMCPServer"
//+kubebuilder:printcolumn:name="URL",type="string",JSONPath=".status.url",description="Virtual MCP server URL"
//+kubebuilder:printcolumn:name="Backends",type="integer",JSONPath=".status.backendCount",description="Discovered backends count"
//+kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="Age"
//+kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type=='Ready')].status"

// VirtualMCPServer is the Schema for the virtualmcpservers API
// VirtualMCPServer aggregates multiple backend MCPServers into a unified endpoint
type VirtualMCPServer struct {
	metav1.TypeMeta   `json:",inline"` // nolint:revive
	metav1.ObjectMeta `json:"metadata,omitempty"`

	Spec   VirtualMCPServerSpec   `json:"spec,omitempty"`
	Status VirtualMCPServerStatus `json:"status,omitempty"`
}

//+kubebuilder:object:root=true

// VirtualMCPServerList contains a list of VirtualMCPServer
type VirtualMCPServerList struct {
	metav1.TypeMeta `json:",inline"` // nolint:revive
	metav1.ListMeta `json:"metadata,omitempty"`
	Items           []VirtualMCPServer `json:"items"`
}

// GetProxyPort returns the proxy port for the VirtualMCPServer.
// vMCP uses port 4483 by default.
func (*VirtualMCPServer) GetProxyPort() int32 {
	return 4483
}

// ResolveGroupName returns the group name from spec.groupRef.
func (r *VirtualMCPServer) ResolveGroupName() string {
	return r.Spec.GroupRef.GetName()
}

// Validate performs validation for VirtualMCPServer
// This method is called by the controller during reconciliation
func (r *VirtualMCPServer) Validate() error {
	// Validate Group is set — spec.groupRef.name is required
	// Note: CEL cannot validate embedded types from other packages
	if r.Spec.GroupRef.GetName() == "" {
		return fmt.Errorf("spec.groupRef.name is required")
	}

	// Note: IncomingAuth validation is handled by kubebuilder markers and CEL rules

	// Validate OutgoingAuth backend configurations
	if r.Spec.OutgoingAuth != nil {
		for backendName, backendAuth := range r.Spec.OutgoingAuth.Backends {
			if err := r.validateBackendAuth(backendName, backendAuth); err != nil {
				return err
			}
		}
	}

	// Validate Aggregation configuration
	if r.Spec.Config.Aggregation != nil {
		if err := r.validateAggregation(); err != nil {
			return err
		}
	}

	// Validate CompositeTools
	if len(r.Spec.Config.CompositeTools) > 0 {
		if err := r.validateCompositeTools(); err != nil {
			return err
		}
	}

	// Note: AuthServerConfig validation is handled by the reconciler (validateAuthServerConfig)
	// so it can set the AuthServerConfigValidated condition on failure.

	// Validate EmbeddingServer / EmbeddingServerRef
	return r.validateEmbeddingServer()
}

// validateEmbeddingServer validates EmbeddingServerRef and Optimizer configuration.
// Rules:
// - embeddingServerRef.name must be non-empty when ref is provided
// - optimizer requires either embeddingServerRef or a manually set embeddingService
// - if embeddingServerRef is set without optimizer, auto-populate optimizer with defaults
//
// The controller handles the remaining cases at runtime (event emission, URL population).
func (r *VirtualMCPServer) validateEmbeddingServer() error {
	// Validate ref name is non-empty
	if r.Spec.EmbeddingServerRef != nil && r.Spec.EmbeddingServerRef.Name == "" {
		return fmt.Errorf("spec.embeddingServerRef.name is required")
	}

	hasOptimizer := r.Spec.Config.Optimizer != nil
	hasRef := r.Spec.EmbeddingServerRef != nil
	hasManualService := hasOptimizer && r.Spec.Config.Optimizer.EmbeddingService != ""

	// Optimizer configured without any embedding source is an error.
	// The user must either set embeddingServerRef or manually set optimizer.embeddingService.
	if hasOptimizer && !hasRef && !hasManualService {
		return fmt.Errorf(
			"spec.config.optimizer requires an embedding service: " +
				"set spec.embeddingServerRef (recommended) or spec.config.optimizer.embeddingService")
	}

	// EmbeddingServerRef is set but optimizer is not configured: auto-populate
	// optimizer with default values so the embedding server is actually used.
	// The controller emits a Kubernetes event for this case.
	if hasRef && !hasOptimizer {
		r.Spec.Config.Optimizer = &config.OptimizerConfig{}
	}

	return nil
}

// validateBackendAuth validates a single backend auth configuration
func (*VirtualMCPServer) validateBackendAuth(backendName string, auth BackendAuthConfig) error {
	// Validate type is set
	if auth.Type == "" {
		return fmt.Errorf("spec.outgoingAuth.backends[%s].type is required", backendName)
	}

	// Validate type-specific configurations
	switch auth.Type {
	case BackendAuthTypeExternalAuthConfigRef:
		if auth.ExternalAuthConfigRef == nil {
			return fmt.Errorf(
				"spec.outgoingAuth.backends[%s].externalAuthConfigRef is required when type is externalAuthConfigRef",
				backendName)
		}
		if auth.ExternalAuthConfigRef.Name == "" {
			return fmt.Errorf("spec.outgoingAuth.backends[%s].externalAuthConfigRef.name is required", backendName)
		}

	case BackendAuthTypeDiscovered:
		// No additional validation needed

	default:
		return fmt.Errorf(
			"spec.outgoingAuth.backends[%s].type must be one of: discovered, externalAuthConfigRef",
			backendName)
	}

	return nil
}

// validateAggregation validates Aggregation configuration
func (r *VirtualMCPServer) validateAggregation() error {
	agg := r.Spec.Config.Aggregation

	// Validate conflict resolution strategy
	if agg.ConflictResolution != "" {
		validStrategies := map[vmcptypes.ConflictResolutionStrategy]bool{
			vmcptypes.ConflictStrategyPrefix:   true,
			vmcptypes.ConflictStrategyPriority: true,
			vmcptypes.ConflictStrategyManual:   true,
		}
		if !validStrategies[agg.ConflictResolution] {
			return fmt.Errorf("config.aggregation.conflictResolution must be one of: prefix, priority, manual")
		}
	}

	// Validate conflict resolution config based on strategy
	if agg.ConflictResolutionConfig != nil {
		resConfig := agg.ConflictResolutionConfig

		switch agg.ConflictResolution {
		case vmcptypes.ConflictStrategyPrefix:
			// Prefix strategy uses PrefixFormat if specified, otherwise defaults
			// No additional validation required

		case vmcptypes.ConflictStrategyPriority:
			if len(resConfig.PriorityOrder) == 0 {
				return fmt.Errorf("config.aggregation.conflictResolutionConfig.priorityOrder is required when conflictResolution is priority")
			}

		case vmcptypes.ConflictStrategyManual:
			// For manual resolution, tools must define explicit overrides
			// This will be validated at runtime when conflicts are detected
		}
	}

	// Validate per-workload tool configurations
	for i, toolConfig := range agg.Tools {
		if toolConfig.Workload == "" {
			return fmt.Errorf("config.aggregation.tools[%d].workload is required", i)
		}

		// If ToolConfigRef is specified, ensure it has a name
		if toolConfig.ToolConfigRef != nil && toolConfig.ToolConfigRef.Name == "" {
			return fmt.Errorf("config.aggregation.tools[%d].toolConfigRef.name is required when toolConfigRef is specified", i)
		}
	}

	return nil
}

// validateCompositeTools validates composite tool definitions in spec.config.compositeTools.
// Uses shared validation from pkg/vmcp/config/composite_validation.go.
func (r *VirtualMCPServer) validateCompositeTools() error {
	toolNames := make(map[string]bool)

	for i := range r.Spec.Config.CompositeTools {
		tool := &r.Spec.Config.CompositeTools[i]

		// Check for duplicate tool names
		if toolNames[tool.Name] {
			return fmt.Errorf("spec.config.compositeTools[%d].name %q is duplicated", i, tool.Name)
		}
		toolNames[tool.Name] = true

		// Use shared validation
		if err := config.ValidateCompositeToolConfig(
			fmt.Sprintf("spec.config.compositeTools[%d]", i), tool,
		); err != nil {
			return err
		}
	}

	return nil
}

func init() {
	SchemeBuilder.Register(&VirtualMCPServer{}, &VirtualMCPServerList{})
}
