// Package operator implements Kubernetes workload management for the container-mom system.
// It handles deployment of resources across multiple clusters and regions.
package operator

import (
	"bytes"
	"context"
	"fmt"
	"io"
	"os"
	"os/exec"
	"regexp"
	"strconv"
	"strings"
	"time"

	"github.com/google/uuid"
	appsv1 "k8s.io/api/apps/v1"
	corev1 "k8s.io/api/core/v1"
	networkingv1 "k8s.io/api/networking/v1"
	"k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/api/resource"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/apimachinery/pkg/util/intstr"
	"k8s.io/client-go/rest"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/cluster"
	"sigs.k8s.io/controller-runtime/pkg/controller"

	v1alpha1 "github.com/pfeifferj/container-mom-go/pkg/apis/containermom/v1alpha1"
	"github.com/pfeifferj/container-mom-go/pkg/argocd"
	"github.com/pfeifferj/container-mom-go/pkg/builder"
	"github.com/pfeifferj/container-mom-go/pkg/forgejo"
	"github.com/pfeifferj/container-mom-go/pkg/templates"
)

// WorkloadController reconciles ContainerMomDeployment objects into actual Kubernetes workloads
type WorkloadController struct {
	client      client.Client
	scheme      *runtime.Scheme
	clusters    map[string]cluster.Cluster // Map of region/cluster name to cluster client
	templateSvc *templates.TemplateService
	argoSvc     *argocd.ArgoService
}

// NewWorkloadController creates a new controller with multi-cluster support
func NewWorkloadController(mgr ctrl.Manager, clusterConfigs map[string]*rest.Config) (*WorkloadController, error) {
	ctrl.Log.Info("Initializing WorkloadController with cluster configurations",
		"configCount", len(clusterConfigs),
		"clusterNames", fmt.Sprintf("%v", getClusterNames(clusterConfigs)))

	// Create Template service
	templateSvc := templates.NewTemplateService(mgr.GetClient())

	// Create ArgoCD service with default config
	argoSvc := argocd.NewArgoService(mgr.GetClient(), argocd.Config{
		ArgocdNamespace: "argocd",
		ForgejoURL:      "https://forgejo.container.mom",
		ForgejoOrg:      "container-mom",
	})

	c := &WorkloadController{
		client:      mgr.GetClient(),
		scheme:      mgr.GetScheme(),
		clusters:    make(map[string]cluster.Cluster),
		templateSvc: templateSvc,
		argoSvc:     argoSvc,
	}

	// Initialize connections to all clusters
	for region, config := range clusterConfigs {
		clusterClient, err := cluster.New(config, func(o *cluster.Options) {
			o.Scheme = mgr.GetScheme()
		})
		if err != nil {
			return nil, fmt.Errorf("failed to create cluster client for region %s: %w", region, err)
		}
		c.clusters[region] = clusterClient
		ctrl.Log.Info("Added cluster client", "region", region)

		// Start the cluster client
		go func(r string) {
			if err := clusterClient.Start(context.Background()); err != nil {
				ctrl.Log.Error(err, "failed to start cluster client", "region", r)
			}
		}(region)
	}

	// Add at least one cluster for local development
	if len(c.clusters) == 0 {
		ctrl.Log.Info("No clusters configured, adding local cluster")
		// Use the manager's client as the local cluster
		c.clusters["local"] = cluster.Cluster(mgr)
	}

	// Use Builder pattern for controller setup which is more stable across API versions
	ctrl.Log.Info("Setting up controller with builder")
	builder := ctrl.NewControllerManagedBy(mgr).
		For(&v1alpha1.ContainerMomDeployment{}).
		WithOptions(controller.Options{
			MaxConcurrentReconciles: 1,
		})
		// Removing the GenerationChangedPredicate to reconcile more often

	err := builder.Complete(c)
	if err != nil {
		return nil, fmt.Errorf("failed to build controller: %w", err)
	}

	ctrl.Log.Info("WorkloadController setup complete")
	return c, nil
}

// Helper function to get cluster names for logging
func getClusterNames(configs map[string]*rest.Config) []string {
	names := make([]string, 0, len(configs))
	for name := range configs {
		names = append(names, name)
	}
	return names
}

// Reconcile handles the reconciliation of ContainerMomDeployment resources
func (c *WorkloadController) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	log := ctrl.LoggerFrom(ctx)

	log.Info("Starting reconciliation",
		"name", req.Name,
		"namespace", req.Namespace,
		"availableClusters", len(c.clusters))

	// Fetch the ContainerMomDeployment
	var deployment v1alpha1.ContainerMomDeployment
	if err := c.client.Get(ctx, req.NamespacedName, &deployment); err != nil {
		log.Error(err, "Failed to get ContainerMomDeployment", "name", req.Name)
		return ctrl.Result{}, client.IgnoreNotFound(err)
	}

	// IMPORTANT: Log all annotations for debugging
	log.Info("CR annotations",
		"name", deployment.Name,
		"annotations", fmt.Sprintf("%v", deployment.Annotations))

	// Check if this CR already has a namespace annotation
	hasNamespaceAnnotation := false
	var existingNamespace string
	if deployment.Annotations != nil {
		if ns, ok := deployment.Annotations["containermom.mom/namespace"]; ok && ns != "" {
			hasNamespaceAnnotation = true
			existingNamespace = ns
			log.Info("CR already has namespace annotation",
				"name", deployment.Name,
				"namespace", deployment.Namespace,
				"appNamespace", existingNamespace)
		}
	}

	if !hasNamespaceAnnotation {
		log.Info("CR does not have namespace annotation, will generate one during reconciliation",
			"name", deployment.Name,
			"namespace", deployment.Namespace)
	}

	// IMPORTANT: Check for reconciliation trigger
	if deployment.Annotations != nil {
		if _, ok := deployment.Annotations["containermom.mom/reconcile-trigger"]; ok {
			log.Info("Found reconcile-trigger annotation, forcing reconciliation",
				"name", deployment.Name)
		}
	}

	log.Info("Reconciling deployment across clusters",
		"name", deployment.Name,
		"clusters", deployment.Spec.Clusters,
		"annotations", deployment.Annotations)

	// Track any errors during multi-cluster deployment
	var errs []error

	// Keep track of which clusters we actually deployed to
	deployedClusters := make(map[string]bool)

	// Deploy to each specified cluster
	for _, clusterRef := range deployment.Spec.Clusters {
		// Check for local cluster
		if clusterRef == "local" {
			// Use the local cluster
			localCluster, localExists := c.clusters["local"]

			if localExists {
				log.Info("Deploying to local cluster",
					"clusterRef", clusterRef,
					"namespace", deployment.Namespace)

				if err := c.reconcileInCluster(ctx, localCluster.GetClient(), &deployment); err != nil {
					log.Error(err, "failed to reconcile in local cluster")
					errs = append(errs, fmt.Errorf("failed in local cluster: %w", err))
				} else {
					deployedClusters["local"] = true
					log.Info("Successfully deployed to local cluster",
						"namespace", deployment.Namespace)
				}
			} else {
				// Fallback to using the operator's client for local deployment
				log.Info("Using operator's client for local cluster deployment",
					"clusterRef", clusterRef,
					"namespace", deployment.Namespace)

				if err := c.reconcileInCluster(ctx, c.client, &deployment); err != nil {
					log.Error(err, "failed to reconcile in local cluster using operator client")
					errs = append(errs, fmt.Errorf("failed in local cluster: %w", err))
				} else {
					deployedClusters["local"] = true
					log.Info("Successfully deployed to local cluster using operator client",
						"namespace", deployment.Namespace)
				}
			}

			// Skip to the next cluster reference
			continue
		}

		// First try to find the cluster by exact key
		cluster, ok := c.clusters[clusterRef]

		// If not found, try region-cluster format (used by LoadClustersFromSecrets)
		if !ok {
			// First try exact match with region-cluster format
			// Check if we're looking for a region key like "euw" but need to match "euw-wkl1"
			for key, cl := range c.clusters {
				// Only consider combined keys (containing a hyphen)
				if !strings.Contains(key, "-") {
					continue
				}

				parts := strings.Split(key, "-")
				if len(parts) == 2 {
					regionPart, clusterPart := parts[0], parts[1]

					// If the reference matches either the region or cluster part exactly
					if clusterRef == regionPart || clusterRef == clusterPart {
						log.Info("Matched cluster via region or cluster component",
							"requested", clusterRef,
							"matched", key,
							"matchType", fmt.Sprintf("matched %s in %s", clusterRef, key))
						cluster = cl
						ok = true
						clusterRef = key // Update the reference to the full key for logging
						break
					}
				}
			}

			// If still not found, try a contains approach as fallback (but log it as potentially ambiguous)
			if !ok {
				for key, cl := range c.clusters {
					if strings.Contains(key, clusterRef) {
						log.Info("Matched cluster by substring (potentially ambiguous)",
							"requested", clusterRef,
							"matched", key,
							"warning", "This match is ambiguous and may not be consistent")
						cluster = cl
						ok = true
						clusterRef = key // Update the reference to the full key for logging
						break
					}
				}
			}
		}

		if !ok {
			log.Error(nil, "cluster not configured", "cluster", clusterRef)
			errs = append(errs, fmt.Errorf("cluster %s not configured", clusterRef))
			continue
		}

		// Create/update resources in this cluster
		if err := c.reconcileInCluster(ctx, cluster.GetClient(), &deployment); err != nil {
			log.Error(err, "failed to reconcile in cluster", "cluster", clusterRef)
			errs = append(errs, fmt.Errorf("failed in cluster %s: %w", clusterRef, err))
		} else {
			deployedClusters[clusterRef] = true
		}
	}

	// Update deployment status with results
	status := v1alpha1.ContainerMomDeploymentStatus{
		Phase: v1alpha1.DeploymentPhaseSucceeded,
	}

	if len(errs) > 0 {
		status.Phase = v1alpha1.DeploymentPhaseFailed
		status.Conditions = append(status.Conditions, v1alpha1.Condition{
			Type:    "Failed",
			Status:  "True",
			Message: fmt.Sprintf("Failed in clusters: %v", errs),
		})
	}

	// Add information about which clusters were deployed to
	clustersStr := ""
	for cluster := range deployedClusters {
		if clustersStr != "" {
			clustersStr += ", "
		}
		clustersStr += cluster
	}
	status.Conditions = append(status.Conditions, v1alpha1.Condition{
		Type:    "DeployedTo",
		Status:  "True",
		Message: clustersStr,
	})

	// Get the namespace from annotation if it exists
	if ns, ok := deployment.Annotations["containermom.mom/namespace"]; ok && ns != "" {
		status.Conditions = append(status.Conditions, v1alpha1.Condition{
			Type:    "Namespace",
			Status:  "True",
			Message: ns,
		})
	}

	// Add a condition with the last reconciled timestamp
	status.LastUpdateTime = metav1.Now()

	// Get a fresh copy of the deployment before updating status
	// This avoids conflicts with other controllers that might have modified the CR
	freshDeployment := &v1alpha1.ContainerMomDeployment{}
	err := c.client.Get(ctx, req.NamespacedName, freshDeployment)
	if err != nil {
		if client.IgnoreNotFound(err) != nil {
			log.Error(err, "failed to get fresh deployment before updating status")
			return ctrl.Result{RequeueAfter: 1 * time.Minute}, err
		}
		// Deployment was deleted, don't update status
		log.Info("Deployment was deleted before status update could be applied")
		return ctrl.Result{}, nil
	}

	// Update the status on the fresh copy
	freshDeployment.Status = status

	// Apply the status update
	if err := c.client.Status().Update(ctx, freshDeployment); err != nil {
		log.Error(err, "failed to update deployment status")

		// If the error is a conflict, requeue after a short delay
		if errors.IsConflict(err) {
			log.Info("Status update conflict, will retry shortly", "deployment", req.Name)
			return ctrl.Result{RequeueAfter: 2 * time.Second}, nil
		}

		return ctrl.Result{RequeueAfter: 1 * time.Minute}, err
	}

	log.Info("Successfully updated deployment status",
		"deployment", req.Name,
		"phase", status.Phase,
		"clusters", clustersStr)

	// Check if we need a namespace to be created but it wasn't yet
	if !hasNamespaceAnnotation {
		// For the missing cluster configuration case, we need to return a 5-minute requeue
		// This is important for tests - if we encountered cluster configuration errors earlier,
		// use a longer requeue time
		if len(errs) > 0 {
			// Requeue after 5 minutes for cluster configuration errors
			return ctrl.Result{RequeueAfter: 5 * time.Minute}, nil
		}

		// Requeue quickly to ensure namespace setup is complete
		log.Info("Requeuing quickly to ensure namespace setup is complete",
			"name", deployment.Name)
		return ctrl.Result{RequeueAfter: 15 * time.Second}, nil
	}

	// Requeue after some time to ensure continuous reconciliation
	return ctrl.Result{RequeueAfter: 5 * time.Minute}, nil
}

// generateNamespaceName creates a unique, valid namespace name following the pattern:
// wkl-<customer-name>-<app-name>-<short-uuid>
// It ensures the namespace name:
// 1. Doesn't exceed Kubernetes' 63 character limit
// 2. Follows the DNS-1123 label naming convention (lowercase alphanumeric and '-', starting and ending with alphanumeric)
// 3. Is unique with a short UUID
func generateNamespaceName(customerName, appName string) string {
	// DNS-1123 label validation regex
	namespaceRegex := regexp.MustCompile("^[a-z0-9]([-a-z0-9]*[a-z0-9])?$")

	// Clean and normalize the inputs
	customerName = strings.ToLower(customerName)
	appName = strings.ToLower(appName)

	// Replace invalid characters with '-'
	customerName = regexp.MustCompile("[^a-z0-9-]").ReplaceAllString(customerName, "-")
	appName = regexp.MustCompile("[^a-z0-9-]").ReplaceAllString(appName, "-")

	// Truncate customer and app names if needed to leave room for prefix, UUID and separators
	// Maximum namespace length is 63 characters
	// Format: wkl-customer-app-uuid
	// wkl- = 4 chars, 3 hyphens = 3 chars, uuid (8 chars)
	// Remaining space for customer and app names: 63 - 4 - 3 - 8 = 48 chars
	maxPartLength := 24 // Split the 48 chars evenly

	if len(customerName) > maxPartLength {
		customerName = customerName[:maxPartLength]
	}

	if len(appName) > maxPartLength {
		appName = appName[:maxPartLength]
	}

	// Ensure neither part ends with a hyphen
	customerName = strings.TrimRight(customerName, "-")
	appName = strings.TrimRight(appName, "-")

	// Ensure neither part starts with a hyphen
	customerName = strings.TrimLeft(customerName, "-")
	appName = strings.TrimLeft(appName, "-")

	// Generate a short UUID (first 8 characters)
	shortUUID := uuid.New().String()[:8]

	// Create the namespace name
	nsName := fmt.Sprintf("wkl-%s-%s-%s", customerName, appName, shortUUID)

	// Final check against DNS-1123 label rules
	if !namespaceRegex.MatchString(nsName) {
		// In case of any validation issues, fall back to a simpler but guaranteed valid name
		timestamp := time.Now().Unix()
		nsName = fmt.Sprintf("wkl-%s-%d", shortUUID, timestamp)
	}

	return nsName
}

// buildImageFromSource builds a container image from the given Git repository
// and returns the image name in the OpenShift internal registry format
func (c *WorkloadController) buildImageFromSource(ctx context.Context, deployment *v1alpha1.ContainerMomDeployment) (string, error) {
	log := ctrl.LoggerFrom(ctx)

	if deployment.Spec.Source.GitURL == "" {
		return "", fmt.Errorf("no Git URL specified")
	}

	// Extract repo name from Git URL (e.g., "repo" from "https://github.com/user/repo")
	parts := strings.Split(deployment.Spec.Source.GitURL, "/")
	if len(parts) == 0 {
		return "", fmt.Errorf("invalid Git URL format")
	}

	// Get the last part and remove .git suffix if present
	repoName := parts[len(parts)-1]
	repoName = strings.TrimSuffix(repoName, ".git")

	// Create a unique build ID
	buildID := fmt.Sprintf("%s-%s", repoName, uuid.New().String()[:8])
	log.Info("Starting source-to-image build",
		"buildID", buildID,
		"gitURL", deployment.Spec.Source.GitURL)

	// Update deployment status to indicate build in progress
	deploymentCopy := deployment.DeepCopy()
	if deploymentCopy.Status.Conditions == nil {
		deploymentCopy.Status.Conditions = []v1alpha1.Condition{}
	}

	buildingCondition := v1alpha1.Condition{
		Type:    "Building",
		Status:  "True",
		Reason:  "BuildInProgress",
		Message: fmt.Sprintf("Building image from source: %s", deployment.Spec.Source.GitURL),
	}

	// Find and update or add the Building condition
	conditionUpdated := false
	for i, condition := range deploymentCopy.Status.Conditions {
		if condition.Type == "Building" {
			deploymentCopy.Status.Conditions[i] = buildingCondition
			conditionUpdated = true
			break
		}
	}

	if !conditionUpdated {
		deploymentCopy.Status.Conditions = append(deploymentCopy.Status.Conditions, buildingCondition)
	}

	// Update the deployment status
	if err := c.client.Status().Update(ctx, deploymentCopy); err != nil {
		log.Error(err, "Failed to update deployment status for build",
			"deploymentName", deployment.Name)
		// Continue with the build even if status update fails
	}

	// Get namespace for image storage
	namespace := deployment.Namespace
	if ns, ok := deployment.Annotations["containermom.mom/namespace"]; ok && ns != "" {
		namespace = ns
	}

	// Determine builder image name if specified in BuildConfig
	builderImageName := ""
	if deployment.Spec.Source.BuildConfig != nil && deployment.Spec.Source.BuildConfig.BuilderImage != "" {
		builderImageName = deployment.Spec.Source.BuildConfig.BuilderImage
	}

	// Convert BuildArgs if specified
	buildArgs := make(map[string]*string)
	if deployment.Spec.Source.BuildConfig != nil && deployment.Spec.Source.BuildConfig.BuildArgs != nil {
		for k, v := range deployment.Spec.Source.BuildConfig.BuildArgs {
			value := v // Create a copy to avoid referencing the loop variable
			buildArgs[k] = &value
		}
	}

	// Prepare builder configuration with OpenShift registry settings
	buildConfig := &builder.BuildConfig{
		GitURL:      deployment.Spec.Source.GitURL,
		BuilderName: builderImageName,
		UseCache:    true,
		BuildArgs:   buildArgs,
		Registry:    "image-registry.openshift-image-registry.svc:5000",
		ImageTag:    "latest",
		Namespace:   namespace,
	}

	// Create a builder instance
	b, err := builder.NewBuilder(buildConfig)
	if err != nil {
		log.Error(err, "Failed to create builder",
			"deploymentName", deployment.Name,
			"gitURL", deployment.Spec.Source.GitURL)

		// Update status with build failure
		c.updateBuildStatus(ctx, deployment, false, err.Error())

		// For static HTML sites, use the nginx-unprivileged fallback and proceed
		if isStaticHTMLSite(ctx, deployment) {
			fallbackImage := "quay.io/nginx/nginx-unprivileged:latest"
			log.Info("Detected static HTML site, using fallback image",
				"image", fallbackImage)

			// Update status to indicate we're using a fallback image
			c.updateBuildStatus(ctx, deployment, true,
				fmt.Sprintf("Using fallback image for static HTML: %s", fallbackImage))

			return fallbackImage, nil
		}

		return "", err
	}

	// Perform the build - this will now also push to the OpenShift registry
	imageName, err := b.Build(ctx)
	if err != nil {
		log.Error(err, "Failed to build/push image",
			"deploymentName", deployment.Name,
			"gitURL", deployment.Spec.Source.GitURL)

		// Update status with build failure
		c.updateBuildStatus(ctx, deployment, false, err.Error())

		// For static HTML sites, use the nginx-unprivileged fallback and proceed
		if isStaticHTMLSite(ctx, deployment) {
			fallbackImage := "quay.io/nginx/nginx-unprivileged:latest"
			log.Info("Image build failed for static HTML site, using fallback image",
				"image", fallbackImage)

			// Update status to indicate we're using a fallback image
			c.updateBuildStatus(ctx, deployment, true,
				fmt.Sprintf("Using fallback image for static HTML: %s", fallbackImage))

			return fallbackImage, nil
		}

		return "", err
	}

	// Update status with build success
	c.updateBuildStatus(ctx, deployment, true, fmt.Sprintf("Successfully built and pushed image: %s", imageName))

	log.Info("Successfully built and pushed image from source",
		"deploymentName", deployment.Name,
		"gitURL", deployment.Spec.Source.GitURL,
		"imageName", imageName)

	return imageName, nil
}

// isStaticHTMLSite determines if the deployment is likely a static HTML site
// by examining its Git repository or annotations
func isStaticHTMLSite(ctx context.Context, deployment *v1alpha1.ContainerMomDeployment) bool {
	log := ctrl.LoggerFrom(ctx)

	// Check annotations for explicit static site indicator
	if deployment.Annotations != nil {
		if val, ok := deployment.Annotations["containermom.mom/static-site"]; ok && val == "true" {
			log.Info("Static HTML site detected from annotation",
				"deploymentName", deployment.Name)
			return true
		}
	}

	// Check if the repository name suggests a static site
	if deployment.Spec.Source.GitURL != "" {
		parts := strings.Split(deployment.Spec.Source.GitURL, "/")
		if len(parts) > 0 {
			repoName := strings.ToLower(parts[len(parts)-1])
			repoName = strings.TrimSuffix(repoName, ".git")

			// Check for common static site repository names
			if strings.Contains(repoName, "html") ||
				strings.Contains(repoName, "static") ||
				strings.Contains(repoName, "website") ||
				strings.Contains(repoName, "site") ||
				strings.Contains(repoName, "www") ||
				strings.Contains(repoName, "web") {
				log.Info("Static HTML site detected from repository name",
					"deploymentName", deployment.Name,
					"repoName", repoName)
				return true
			}
		}
	}

	// Default assumption: if no specific language was detected in BuildConfig, it's likely static
	if deployment.Spec.Source.BuildConfig == nil ||
		deployment.Spec.Source.BuildConfig.BuilderImage == "" {
		log.Info("Assuming static HTML site due to lack of specific builder configuration",
			"deploymentName", deployment.Name)
		return true
	}

	return false
}

// updateBuildStatus updates the deployment status with build results
func (c *WorkloadController) updateBuildStatus(ctx context.Context, deployment *v1alpha1.ContainerMomDeployment,
	success bool, message string) {

	log := ctrl.LoggerFrom(ctx)

	// Always use the original control namespace for the CR, not the target namespace
	// even if the deployment object has the target namespace set
	controlNamespace := deployment.Namespace

	// The CR should always exist in the OPERATOR_NAMESPACE or WATCH_NAMESPACE which is container-mom-system
	// not in the target deployment namespace
	if ns, exists := os.LookupEnv("OPERATOR_NAMESPACE"); exists && ns != "" {
		controlNamespace = ns
	} else if ns, exists := os.LookupEnv("WATCH_NAMESPACE"); exists && ns != "" {
		controlNamespace = ns
	}

	// Implement a retry loop for getting the deployment
	var freshDeployment *v1alpha1.ContainerMomDeployment
	var err error
	maxRetries := 3
	retryDelay := 500 * time.Millisecond

	for attempt := 0; attempt < maxRetries; attempt++ {
		// Get fresh copy of deployment to avoid conflicts
		freshDeployment = &v1alpha1.ContainerMomDeployment{}
		err = c.client.Get(ctx, types.NamespacedName{
			Name:      deployment.Name,
			Namespace: controlNamespace,
		}, freshDeployment)

		if err == nil {
			// Successfully got the deployment, break out of retry loop
			break
		}

		if errors.IsNotFound(err) {
			log.Info("Deployment not found during status update, retrying...",
				"deploymentName", deployment.Name,
				"namespace", controlNamespace,
				"attempt", attempt+1,
				"maxRetries", maxRetries)
		} else {
			log.Error(err, "Error getting deployment for status update, retrying...",
				"deploymentName", deployment.Name,
				"namespace", controlNamespace,
				"attempt", attempt+1,
				"maxRetries", maxRetries)
		}

		// If this was not the last attempt, wait before retrying
		if attempt < maxRetries-1 {
			select {
			case <-time.After(retryDelay):
				// Increase delay for next retry
				retryDelay *= 2
			case <-ctx.Done():
				// Context cancelled, exit retry loop
				log.Info("Context cancelled during deployment status update retries",
					"deploymentName", deployment.Name)
				return
			}
		}
	}

	if err != nil {
		log.Error(err, "Failed to update deployment status after build completion",
			"deploymentName", deployment.Name,
			"controlNamespace", controlNamespace,
			"targetNamespace", deployment.Namespace,
			"success", success)
		return
	}

	// Update status conditions
	conditionType := "Building"
	conditionStatus := "False"
	conditionReason := "BuildSucceeded"

	if !success {
		conditionReason = "BuildFailed"
	}

	buildCondition := v1alpha1.Condition{
		Type:    conditionType,
		Status:  conditionStatus,
		Reason:  conditionReason,
		Message: message,
	}

	// Find and update or add the Building condition
	conditionUpdated := false
	for i, condition := range freshDeployment.Status.Conditions {
		if condition.Type == conditionType {
			freshDeployment.Status.Conditions[i] = buildCondition
			conditionUpdated = true
			break
		}
	}

	if !conditionUpdated {
		freshDeployment.Status.Conditions = append(freshDeployment.Status.Conditions, buildCondition)
	}

	// Update the deployment with the new status - also retry this operation
	for statusAttempt := 0; statusAttempt < maxRetries; statusAttempt++ {
		if err := c.client.Status().Update(ctx, freshDeployment); err != nil {
			if errors.IsConflict(err) || errors.IsNotFound(err) {
				log.Info("Conflict or not found error updating status, retrying...",
					"deploymentName", deployment.Name,
					"namespace", controlNamespace,
					"attempt", statusAttempt+1,
					"maxRetries", maxRetries,
					"error", err.Error())

				// Wait before retrying
				if statusAttempt < maxRetries-1 {
					time.Sleep(retryDelay)
				}
				continue
			}

			log.Error(err, "Failed to update deployment status after build completion",
				"deploymentName", deployment.Name,
				"controlNamespace", controlNamespace,
				"targetNamespace", deployment.Namespace,
				"success", success)
			return
		}

		// Success, exit retry loop
		log.Info("Successfully updated deployment status after build completion",
			"deploymentName", deployment.Name,
			"controlNamespace", controlNamespace,
			"success", success)
		return
	}
}

// getImageForDeployment returns the image to use, falling back to a derived image if needed
func (c *WorkloadController) getImageForDeployment(ctx context.Context, deployment *v1alpha1.ContainerMomDeployment) string {
	log := ctrl.LoggerFrom(ctx)

	// Check for explicit image specified in the CR
	if deployment.Spec.Source.Image != "" && deployment.Spec.Source.Image != "placeholder" {
		return deployment.Spec.Source.Image
	}

	// Check if this deployment is based on a template
	template, isFromTemplate, err := c.templateSvc.GetTemplateForDeployment(ctx, deployment)
	if err == nil && isFromTemplate {
		// Successfully found the template
		log.Info("Found template for deployment",
			"deploymentName", deployment.Name,
			"templateName", template.Name)

		// Get the appropriate component
		component, err := c.templateSvc.GetTemplateComponent(template, "")
		if err == nil {
			// Get the appropriate image for this component
			image := c.templateSvc.GetComponentImage(component)
			log.Info("Using image from template component",
				"deploymentName", deployment.Name,
				"component", component.Name,
				"image", image)
			return image
		} else {
			log.Error(err, "Failed to get component from template",
				"deploymentName", deployment.Name,
				"templateName", template.Name)
		}
	}

	// If we have a Git URL, build an image from source
	if deployment.Spec.Source.GitURL != "" {
		// Check if a rebuild has been requested via annotation
		forceRebuild := false
		if _, ok := deployment.Annotations["containermom.mom/force-rebuild"]; ok {
			forceRebuild = true
			log.Info("Force rebuild annotation detected, will rebuild image",
				"deploymentName", deployment.Name)

			// Remove the annotation after detecting it to prevent endless rebuilds
			if err := c.removeForceRebuildAnnotation(ctx, deployment); err != nil {
				log.Error(err, "Failed to remove force-rebuild annotation",
					"deploymentName", deployment.Name)
				// Continue with the rebuild even if annotation removal fails
			}
		}

		// Use previously built image unless a rebuild is requested
		if !forceRebuild && deployment.Status.Image != "" {
			log.Info("Using previously built image from status",
				"deploymentName", deployment.Name,
				"image", deployment.Status.Image)
			return deployment.Status.Image
		}

		// Check if we already started a build recently (within the last minute)
		// by looking for a build timestamp annotation
		buildInProgress := false
		if timestamp, ok := deployment.Annotations["containermom.mom/last-build-start"]; ok {
			lastBuildTime, err := time.Parse(time.RFC3339, timestamp)
			if err == nil {
				// If the last build was started less than 60 seconds ago, don't start another one
				buildAge := time.Since(lastBuildTime)
				if buildAge < 60*time.Second {
					log.Info("Skipping build because one was started recently",
						"deploymentName", deployment.Name,
						"lastBuildTime", lastBuildTime,
						"buildAge", buildAge)
					buildInProgress = true

					// Use a derived image name as a fallback until the build completes
					parts := strings.Split(deployment.Spec.Source.GitURL, "/")
					if len(parts) > 0 {
						repoName := parts[len(parts)-1]
						repoName = strings.TrimSuffix(repoName, ".git")
						repoName = strings.ToLower(repoName)
						repoName = strings.ReplaceAll(repoName, "_", "-")
						repoName = strings.ReplaceAll(repoName, ".", "-")

						namespace := deployment.Namespace
						if ns, ok := deployment.Annotations["containermom.mom/namespace"]; ok && ns != "" {
							namespace = ns
						}

						fallbackImage := fmt.Sprintf("image-registry.openshift-image-registry.svc:5000/%s/%s:latest",
							namespace, repoName)

						return fallbackImage
					}
				}
			}
		}

		// If no build is in progress, start a new one and update the timestamp
		if !buildInProgress {
			// Update the build timestamp before starting the build
			deploymentCopy := deployment.DeepCopy()
			if deploymentCopy.Annotations == nil {
				deploymentCopy.Annotations = make(map[string]string)
			}
			deploymentCopy.Annotations["containermom.mom/last-build-start"] = time.Now().Format(time.RFC3339)

			// Get the right namespace for the CR
			controlNamespace := deployment.Namespace
			if ns, exists := os.LookupEnv("OPERATOR_NAMESPACE"); exists && ns != "" {
				controlNamespace = ns
			} else if ns, exists := os.LookupEnv("WATCH_NAMESPACE"); exists && ns != "" {
				controlNamespace = ns
			}

			// Update the CR with the build timestamp
			err := c.client.Get(ctx, types.NamespacedName{
				Name:      deployment.Name,
				Namespace: controlNamespace,
			}, &v1alpha1.ContainerMomDeployment{})

			if err == nil {
				if updateErr := c.client.Update(ctx, deploymentCopy); updateErr != nil {
					log.Error(updateErr, "Failed to update build timestamp annotation")
					// Continue with the build anyway
				}
			}

			// Build the image from source
			imageName, err := c.buildImageFromSource(ctx, deployment)
			if err != nil {
				log.Error(err, "Failed to build image from source, using derived name as fallback",
					"deploymentName", deployment.Name,
					"gitURL", deployment.Spec.Source.GitURL)

				// Extract repo name from Git URL as fallback
				parts := strings.Split(deployment.Spec.Source.GitURL, "/")
				if len(parts) > 0 {
					// Get the last part and remove .git suffix if present
					repoName := parts[len(parts)-1]
					repoName = strings.TrimSuffix(repoName, ".git")

					// Sanitize the repo name for use in image name
					repoName = strings.ToLower(repoName)
					repoName = strings.ReplaceAll(repoName, "_", "-")
					repoName = strings.ReplaceAll(repoName, ".", "-")

					// Generate a fallback image name with OpenShift internal registry format
					namespace := deployment.Namespace
					if ns, ok := deployment.Annotations["containermom.mom/namespace"]; ok && ns != "" {
						namespace = ns
					}

					fallbackImage := fmt.Sprintf("image-registry.openshift-image-registry.svc:5000/%s/%s:latest",
						namespace, repoName)

					return fallbackImage
				}
			}

			// Return the built image name
			if imageName != "" {
				// Update the status with the built image
				deploymentCopy = deployment.DeepCopy()
				deploymentCopy.Status.Image = imageName

				if err := c.client.Status().Update(ctx, deploymentCopy); err != nil {
					log.Error(err, "Failed to update deployment status with built image",
						"deploymentName", deployment.Name,
						"image", imageName)
				}

				return imageName
			}
		}
	}

	// If all else fails, use a default image
	return "nginx:latest"
}

// removeForceRebuildAnnotation removes the force-rebuild annotation from the deployment
func (c *WorkloadController) removeForceRebuildAnnotation(ctx context.Context, deployment *v1alpha1.ContainerMomDeployment) error {
	// Always use the original control namespace for the CR, not the target namespace
	// even if the deployment object has the target namespace set
	controlNamespace := deployment.Namespace

	// The CR should always exist in the OPERATOR_NAMESPACE or WATCH_NAMESPACE which is container-mom-system
	// not in the target deployment namespace
	if ns, exists := os.LookupEnv("OPERATOR_NAMESPACE"); exists && ns != "" {
		controlNamespace = ns
	} else if ns, exists := os.LookupEnv("WATCH_NAMESPACE"); exists && ns != "" {
		controlNamespace = ns
	}

	// Get a fresh copy of the deployment to avoid conflicts
	freshDeployment := &v1alpha1.ContainerMomDeployment{}
	err := c.client.Get(ctx, types.NamespacedName{
		Name:      deployment.Name,
		Namespace: controlNamespace,
	}, freshDeployment)

	if err != nil {
		return fmt.Errorf("failed to get deployment for annotation removal in namespace %s: %w", controlNamespace, err)
	}

	// Remove the force-rebuild annotation
	if freshDeployment.Annotations != nil {
		delete(freshDeployment.Annotations, "containermom.mom/force-rebuild")

		// Update the deployment
		if err := c.client.Update(ctx, freshDeployment); err != nil {
			return fmt.Errorf("failed to update deployment after removing annotation: %w", err)
		}
	}

	return nil
}

// reconcileInCluster handles deployment of resources to a specific cluster
func (c *WorkloadController) reconcileInCluster(ctx context.Context, client client.Client, deployment *v1alpha1.ContainerMomDeployment) error {
	log := ctrl.LoggerFrom(ctx)
	log.Info("Starting in-cluster reconciliation", "deployment", deployment.Name)

	// Create a dedicated namespace for each customer app with a unique name
	// Extract customer name from CR name or use a default
	customerName := deployment.Name
	appName := deployment.Name

	// If the CR has an owner reference, use that as the customer name
	if len(deployment.OwnerReferences) > 0 {
		customerName = deployment.OwnerReferences[0].Name
	}

	// Check if we already have a namespace for this deployment
	var appNamespace string
	existingNamespace, hasExisting := deployment.Annotations["containermom.mom/namespace"]
	if hasExisting && existingNamespace != "" {
		// Use the existing namespace instead of creating a new one
		appNamespace = existingNamespace
		log.Info("Using existing namespace from annotation",
			"namespace", appNamespace,
			"deployment", deployment.Name)
	} else {
		// Get a unique, valid namespace name
		appNamespace = generateNamespaceName(customerName, appName)
		log.Info("Generated new namespace name",
			"namespace", appNamespace,
			"customer", customerName,
			"app", appName)

		// Fetch the latest version of the CR to avoid conflicts
		var latestDeployment v1alpha1.ContainerMomDeployment
		err := c.client.Get(ctx, types.NamespacedName{
			Name:      deployment.Name,
			Namespace: deployment.Namespace,
		}, &latestDeployment)

		if err != nil {
			if errors.IsNotFound(err) {
				log.Error(err, "CR no longer exists - cannot update with namespace annotation",
					"deployment", deployment.Name)
				// Return early as there's no point continuing if the CR is gone
				return fmt.Errorf("CR %s no longer exists: %w", deployment.Name, err)
			}

			log.Error(err, "Failed to get latest CR version for namespace annotation",
				"deployment", deployment.Name)
			// Continue with the original CR
			latestDeployment = *deployment
		}

		// Store the new namespace name in the CR annotation
		// Deep copy the deployment to avoid modifying the original
		deploymentCopy := latestDeployment.DeepCopy()
		if deploymentCopy.Annotations == nil {
			deploymentCopy.Annotations = make(map[string]string)
		}
		deploymentCopy.Annotations["containermom.mom/namespace"] = appNamespace

		// Update the CR with the namespace annotation
		// Use the controller's client since this is the management cluster
		log.Info("Updating CR with namespace annotation",
			"deployment", deployment.Name,
			"namespace", appNamespace)

		// Use patch instead of update to reduce conflicts
		currentAnnotations := deploymentCopy.Annotations
		originalDeployment := &v1alpha1.ContainerMomDeployment{}
		if err := c.client.Get(ctx, types.NamespacedName{
			Name:      deployment.Name,
			Namespace: deployment.Namespace,
		}, originalDeployment); err != nil {
			if errors.IsNotFound(err) {
				log.Error(err, "CR no longer exists - cannot patch with namespace annotation",
					"deployment", deployment.Name)
				return fmt.Errorf("CR %s no longer exists: %w", deployment.Name, err)
			}
			log.Error(err, "Failed to get deployment for patching annotation",
				"deployment", deployment.Name)
			// Continue with regular update as fallback
		} else {
			// Create patch to only modify annotations
			if originalDeployment.Annotations == nil {
				originalDeployment.Annotations = make(map[string]string)
			}
			originalDeployment.Annotations["containermom.mom/namespace"] = appNamespace

			if err := c.client.Update(ctx, originalDeployment); err != nil {
				if errors.IsNotFound(err) {
					log.Error(err, "CR no longer exists during patch operation",
						"deployment", deployment.Name)
					return fmt.Errorf("CR %s no longer exists: %w", deployment.Name, err)
				} else if errors.IsConflict(err) {
					log.Error(err, "Conflict patching CR with namespace annotation - will retry later",
						"deployment", deployment.Name,
						"namespace", appNamespace)
					// Don't return error, just continue with generated namespace
				} else {
					log.Error(err, "Failed to patch CR with namespace annotation",
						"deployment", deployment.Name)
					// Continue anyway with the generated namespace
				}
			} else {
				log.Info("Successfully patched CR with namespace annotation",
					"deployment", deployment.Name,
					"namespace", appNamespace)
				// Update our reference too
				deployment.Annotations = currentAnnotations
			}
			// Skip the original update
			goto SkipOriginalUpdate
		}

		// Only run this if patch failed and we're falling back to regular update
		err = c.client.Update(ctx, deploymentCopy)
		if err != nil {
			if errors.IsNotFound(err) {
				log.Error(err, "CR no longer exists - cannot update with namespace annotation",
					"deployment", deployment.Name)
				// Return early as there's no point continuing if the CR is gone
				return fmt.Errorf("CR %s no longer exists: %w", deployment.Name, err)
			}

			if errors.IsConflict(err) {
				log.Error(err, "Conflict updating CR with namespace annotation - will use generated namespace for now",
					"deployment", deployment.Name,
					"namespace", appNamespace)
				// Continue with the generated namespace, it will be updated on next reconciliation
			} else {
				log.Error(err, "Failed to update CR with namespace annotation - proceeding with generated namespace",
					"deployment", deployment.Name,
					"namespace", appNamespace)
			}
			// Continue anyway, but we'll generate a new namespace next time
		} else {
			log.Info("Successfully updated CR with namespace annotation",
				"deployment", deployment.Name,
				"namespace", appNamespace)

			// Update our local reference to match
			deployment.Annotations = deploymentCopy.Annotations
		}

		// Label to mark the end of the update block for goto
	SkipOriginalUpdate:
	}

	// Ensure the namespace exists
	namespace := &corev1.Namespace{
		ObjectMeta: metav1.ObjectMeta{
			Name: appNamespace,
			Labels: map[string]string{
				"app.kubernetes.io/managed-by": "container-mom",
				"containermom.mom/customer":    customerName,
				"containermom.mom/app":         appName,
				"containermom.mom/deployment":  deployment.Name,
			},
		},
	}

	// Try to create the namespace - ignore if it already exists
	log.Info("Ensuring namespace exists", "namespace", appNamespace)
	err := client.Create(ctx, namespace)
	if err != nil {
		if errors.IsAlreadyExists(err) {
			log.Info("Namespace already exists, continuing with deployment",
				"namespace", appNamespace)

			// Check if we need to update labels on existing namespace
			existingNs := &corev1.Namespace{}
			if getErr := client.Get(ctx, types.NamespacedName{Name: appNamespace}, existingNs); getErr == nil {
				// Update labels if needed
				labelsChanged := false
				if existingNs.Labels == nil {
					existingNs.Labels = make(map[string]string)
				}

				// Ensure all our expected labels are present
				for k, v := range namespace.Labels {
					if existingNs.Labels[k] != v {
						existingNs.Labels[k] = v
						labelsChanged = true
					}
				}

				if labelsChanged {
					if updateErr := client.Update(ctx, existingNs); updateErr != nil {
						log.Error(updateErr, "Failed to update labels on existing namespace",
							"namespace", appNamespace)
					} else {
						log.Info("Updated labels on existing namespace",
							"namespace", appNamespace)
					}
				}
			}
		} else {
			log.Error(err, "Failed to create namespace - will try to continue deployment",
				"namespace", appNamespace,
				"error", err.Error())
		}
	} else {
		log.Info("Successfully created dedicated namespace for customer app",
			"namespace", appNamespace,
			"customer", customerName,
			"app", appName)
	}

	// Create ResourceQuota based on the deployment's resource requirements
	quota := &corev1.ResourceQuota{
		ObjectMeta: metav1.ObjectMeta{
			Name:      deployment.Name + "-quota",
			Namespace: appNamespace,
		},
		Spec: corev1.ResourceQuotaSpec{
			Hard: map[corev1.ResourceName]resource.Quantity{
				// Set pod count limit to prevent abuse
				corev1.ResourcePods: resource.MustParse("10"),
			},
		},
	}

	// Add CPU and memory quotas if specified
	if deployment.Spec.Resources.CPU != "" {
		// Set limit at 120% of requested for some overhead
		cpuRequest := deployment.Spec.Resources.CPU
		// Parse the CPU value and add 20% for the limit
		cpuRequestQuantity, err := resource.ParseQuantity(cpuRequest)
		if err == nil {
			quota.Spec.Hard[corev1.ResourceRequestsCPU] = cpuRequestQuantity

			// For the limit, create a new quantity with 20% more
			// We'll create it based on the string representation with a multiplier
			if strings.HasSuffix(cpuRequest, "m") {
				// Handle millicpu format (e.g., "500m")
				cpuValue, _ := strconv.Atoi(strings.TrimSuffix(cpuRequest, "m"))
				cpuLimitValue := cpuValue * 120 / 100 // 120% of the request
				quota.Spec.Hard[corev1.ResourceLimitsCPU] = resource.MustParse(fmt.Sprintf("%dm", cpuLimitValue))
			} else {
				// Handle whole CPU format (e.g., "1" or "0.5")
				cpuValue, _ := strconv.ParseFloat(cpuRequest, 64)
				cpuLimitValue := cpuValue * 1.2 // 120% of the request
				quota.Spec.Hard[corev1.ResourceLimitsCPU] = resource.MustParse(fmt.Sprintf("%g", cpuLimitValue))
			}
		}
	}

	if deployment.Spec.Resources.Memory != "" {
		// Set limit at 120% of requested for some overhead
		memRequest := deployment.Spec.Resources.Memory
		memRequestQuantity, err := resource.ParseQuantity(memRequest)
		if err == nil {
			quota.Spec.Hard[corev1.ResourceRequestsMemory] = memRequestQuantity

			// For memory limits, we need to parse the unit (Mi, Gi, etc.)
			// This is complex to do correctly, so instead we'll just add a fixed buffer
			// For example, add 128Mi to the request as a simple approach
			bufferMem := resource.MustParse("128Mi")
			memLimitQuantity := memRequestQuantity.DeepCopy()
			memLimitQuantity.Add(bufferMem)
			quota.Spec.Hard[corev1.ResourceLimitsMemory] = memLimitQuantity
		}
	}

	// Apply the quota
	if err := c.createOrUpdate(ctx, client, quota); err != nil {
		ctrl.Log.Error(err, "failed to create/update resource quota",
			"namespace", appNamespace)
		// Continue anyway - the deployment may still work
	} else {
		ctrl.Log.Info("Applied resource quota to namespace",
			"namespace", appNamespace,
			"cpu", deployment.Spec.Resources.CPU,
			"memory", deployment.Spec.Resources.Memory)
	}

	// Create a LimitRange to ensure default limits
	limitRange := &corev1.LimitRange{
		ObjectMeta: metav1.ObjectMeta{
			Name:      deployment.Name + "-limits",
			Namespace: appNamespace,
		},
		Spec: corev1.LimitRangeSpec{
			Limits: []corev1.LimitRangeItem{
				{
					Type:           corev1.LimitTypeContainer,
					Default:        map[corev1.ResourceName]resource.Quantity{},
					DefaultRequest: map[corev1.ResourceName]resource.Quantity{},
				},
			},
		},
	}

	// Add CPU and memory limits if specified
	if deployment.Spec.Resources.CPU != "" {
		cpuQuantity, err := resource.ParseQuantity(deployment.Spec.Resources.CPU)
		if err == nil {
			limitRange.Spec.Limits[0].Default[corev1.ResourceCPU] = cpuQuantity
			limitRange.Spec.Limits[0].DefaultRequest[corev1.ResourceCPU] = cpuQuantity
		}
	}

	if deployment.Spec.Resources.Memory != "" {
		memQuantity, err := resource.ParseQuantity(deployment.Spec.Resources.Memory)
		if err == nil {
			limitRange.Spec.Limits[0].Default[corev1.ResourceMemory] = memQuantity
			limitRange.Spec.Limits[0].DefaultRequest[corev1.ResourceMemory] = memQuantity
		}
	}

	// Only apply the limit range if we have at least one resource specified
	if len(limitRange.Spec.Limits[0].Default) > 0 {
		if err := c.createOrUpdate(ctx, client, limitRange); err != nil {
			ctrl.Log.Error(err, "failed to create/update limit range",
				"namespace", appNamespace)
			// Continue anyway
		}
	}

	// Create/update the Kubernetes Deployment in the customer namespace
	k8sDeployment := &appsv1.Deployment{
		ObjectMeta: metav1.ObjectMeta{
			Name:      deployment.Name,
			Namespace: appNamespace,
			Labels: map[string]string{
				"app":                          deployment.Name,
				"app.kubernetes.io/managed-by": "container-mom",
			},
		},
		Spec: appsv1.DeploymentSpec{
			Selector: &metav1.LabelSelector{
				MatchLabels: map[string]string{
					"app": deployment.Name,
				},
			},
			Template: corev1.PodTemplateSpec{
				ObjectMeta: metav1.ObjectMeta{
					Labels: map[string]string{
						"app": deployment.Name,
					},
				},
				Spec: corev1.PodSpec{
					Containers: []corev1.Container{
						{
							Name:  deployment.Name,
							Image: c.getImageForDeployment(ctx, deployment),
							Resources: corev1.ResourceRequirements{
								Requests: corev1.ResourceList{},
								Limits:   corev1.ResourceList{},
							},
							Env: convertToK8sEnvVars(deployment.Spec.Env),
							Ports: []corev1.ContainerPort{
								{
									ContainerPort: 8080,
									Name:          "http",
								},
							},
						},
					},
				},
			},
		},
	}

	// Ensure the container name matches the image name to avoid confusion
	k8sDeployment.Spec.Template.Spec.Containers[0].Name = deployment.Name

	// Make sure the image in the deployment is pointing to the OpenShift internal registry
	// Verify the image is not using a different registry
	imageName := k8sDeployment.Spec.Template.Spec.Containers[0].Image
	if !strings.Contains(imageName, "image-registry.openshift-image-registry.svc:5000") {
		// If it doesn't contain the internal registry URL, it's likely an external image reference
		// or possibly a reference to a different registry - we'll keep it as is
		log.Info("Using external image reference",
			"deploymentName", deployment.Name,
			"image", imageName)
	} else {
		log.Info("Using internal OpenShift registry image",
			"deploymentName", deployment.Name,
			"image", imageName)
	}

	// Handle container port configuration based on template
	template, isFromTemplate, err := c.templateSvc.GetTemplateForDeployment(ctx, deployment)
	if err == nil && isFromTemplate {
		// We found a valid template, get the component
		component, err := c.templateSvc.GetTemplateComponent(template, "")
		if err == nil {
			// Get the container port for this component
			containerPort := c.templateSvc.GetContainerPort(component)
			k8sDeployment.Spec.Template.Spec.Containers[0].Ports[0].ContainerPort = containerPort

			log.Info("Using template configuration for container",
				"template", template.Name,
				"component", component.Name,
				"port", containerPort,
				"image", k8sDeployment.Spec.Template.Spec.Containers[0].Image)
		}
	}

	// Add CPU and memory resource requirements if specified
	if deployment.Spec.Resources.CPU != "" {
		cpuQuantity, err := resource.ParseQuantity(deployment.Spec.Resources.CPU)
		if err == nil {
			k8sDeployment.Spec.Template.Spec.Containers[0].Resources.Requests[corev1.ResourceCPU] = cpuQuantity
			k8sDeployment.Spec.Template.Spec.Containers[0].Resources.Limits[corev1.ResourceCPU] = cpuQuantity
		}
	}

	if deployment.Spec.Resources.Memory != "" {
		memQuantity, err := resource.ParseQuantity(deployment.Spec.Resources.Memory)
		if err == nil {
			k8sDeployment.Spec.Template.Spec.Containers[0].Resources.Requests[corev1.ResourceMemory] = memQuantity
			k8sDeployment.Spec.Template.Spec.Containers[0].Resources.Limits[corev1.ResourceMemory] = memQuantity
		}
	}

	// Create/update the Service in the customer namespace
	service := &corev1.Service{
		ObjectMeta: metav1.ObjectMeta{
			Name:      deployment.Name,
			Namespace: appNamespace,
			Labels: map[string]string{
				"app":                          deployment.Name,
				"app.kubernetes.io/managed-by": "container-mom",
			},
		},
		Spec: corev1.ServiceSpec{
			Selector: map[string]string{
				"app": deployment.Name,
			},
			Ports: []corev1.ServicePort{
				{
					Port:       80,
					TargetPort: intstr.FromInt(8080),
					Name:       "http",
				},
			},
			Type: corev1.ServiceTypeClusterIP,
		},
	}

	// Apply the resources
	if err := c.createOrUpdate(ctx, client, k8sDeployment); err != nil {
		return fmt.Errorf("failed to create/update deployment: %w", err)
	}
	ctrl.Log.Info("Created/updated deployment",
		"namespace", appNamespace,
		"name", deployment.Name,
		"image", c.getImageForDeployment(ctx, deployment))

	if err := c.createOrUpdate(ctx, client, service); err != nil {
		return fmt.Errorf("failed to create/update service: %w", err)
	}
	ctrl.Log.Info("Created/updated service",
		"namespace", appNamespace,
		"name", deployment.Name,
		"image", c.getImageForDeployment(ctx, deployment))

	// Always create an OpenShift Route to expose the service
	routeName := deployment.Name
	serviceName := deployment.Name

	// Check if we're running in OpenShift by using the API to check for Routes
	isOpenShift := true
	cmd := exec.Command("oc", "api-resources", "--api-group=route.openshift.io")
	cmd.Stderr = io.Discard
	cmd.Stdout = io.Discard
	err = cmd.Run()
	if err != nil {
		isOpenShift = false
		ctrl.Log.Info("Not running in OpenShift, skipping Route creation")
	}

	if isOpenShift {
		// Create Route YAML
		ctrl.Log.Info("Creating/updating OpenShift Route",
			"namespace", appNamespace,
			"name", routeName,
			"service", serviceName)

		// First check if Route already exists
		checkRouteCmd := exec.Command("oc", "get", "route", routeName, "-n", appNamespace)
		checkRouteCmd.Stderr = io.Discard
		checkRouteCmd.Stdout = io.Discard
		routeExists := checkRouteCmd.Run() == nil

		var routeCmd *exec.Cmd
		if routeExists {
			// Update existing Route
			routeCmd = exec.Command("oc", "patch", "route", routeName,
				"-n", appNamespace,
				"--type", "merge",
				"--patch", fmt.Sprintf(`{"spec":{"to":{"kind":"Service","name":"%s"}}}`, serviceName))
			ctrl.Log.Info("Updating existing Route", "namespace", appNamespace, "name", routeName)
		} else {
			// Create new Route with edge TLS termination
			routeCmd = exec.Command("oc", "create", "route", "edge", routeName,
				"--service", serviceName,
				"-n", appNamespace)
			ctrl.Log.Info("Creating new Route with edge TLS termination", "namespace", appNamespace, "name", routeName)
		}

		var routeOutput bytes.Buffer
		routeCmd.Stdout = &routeOutput
		routeCmd.Stderr = &routeOutput

		err = routeCmd.Run()
		if err != nil {
			ctrl.Log.Error(err, "Failed to create/update OpenShift Route",
				"namespace", appNamespace,
				"name", routeName,
				"output", routeOutput.String())
		} else {
			ctrl.Log.Info("Successfully created/updated OpenShift Route",
				"namespace", appNamespace,
				"name", routeName)

			// Get the Route's hostname
			getRouteCmd := exec.Command("oc", "get", "route", routeName,
				"-n", appNamespace,
				"-o", "jsonpath={.spec.host}")
			var routeHost bytes.Buffer
			getRouteCmd.Stdout = &routeHost

			if err := getRouteCmd.Run(); err == nil {
				host := routeHost.String()
				ctrl.Log.Info("Application is available at", "url", fmt.Sprintf("https://%s", host))
			}
		}
	} else {
		// Standard Kubernetes - create Ingress if domain is specified
		if deployment.Spec.Domain.DomainName != "" {
			// Create a basic ingress if one is needed
			hostname := buildHostname(deployment.Spec.Domain)
			ctrl.Log.Info("Creating/updating Ingress", "hostname", hostname)

			pathType := networkingv1.PathTypePrefix
			ingress := &networkingv1.Ingress{
				ObjectMeta: metav1.ObjectMeta{
					Name:      deployment.Name,
					Namespace: appNamespace,
					Labels: map[string]string{
						"app":                          deployment.Name,
						"app.kubernetes.io/managed-by": "container-mom",
					},
					Annotations: map[string]string{
						"kubernetes.io/ingress.class": "nginx",
					},
				},
				Spec: networkingv1.IngressSpec{
					Rules: []networkingv1.IngressRule{
						{
							Host: hostname,
							IngressRuleValue: networkingv1.IngressRuleValue{
								HTTP: &networkingv1.HTTPIngressRuleValue{
									Paths: []networkingv1.HTTPIngressPath{
										{
											Path:     "/",
											PathType: &pathType,
											Backend: networkingv1.IngressBackend{
												Service: &networkingv1.IngressServiceBackend{
													Name: deployment.Name,
													Port: networkingv1.ServiceBackendPort{
														Number: 80,
													},
												},
											},
										},
									},
								},
							},
						},
					},
				},
			}

			if err := c.createOrUpdate(ctx, client, ingress); err != nil {
				return fmt.Errorf("failed to create/update ingress: %w", err)
			}
			ctrl.Log.Info("Created/updated ingress",
				"namespace", appNamespace,
				"name", deployment.Name,
				"hostname", hostname)
		}
	}

	// Handle ArgoCD integration if enabled for this deployment
	c.handleArgoIntegration(ctx, deployment, appNamespace)

	return nil
}

// handleArgoIntegration creates an ArgoCD Application CR for template-based deployments
func (c *WorkloadController) handleArgoIntegration(ctx context.Context, deployment *v1alpha1.ContainerMomDeployment, namespace string) {
	log := ctrl.LoggerFrom(ctx)

	// Check if deployment is based on a template
	template, isFromTemplate, err := c.templateSvc.GetTemplateForDeployment(ctx, deployment)
	if err != nil || !isFromTemplate {
		// Not a template-based deployment or error fetching template
		return
	}

	log.Info("Deployment is based on a template, setting up GitOps integration",
		"deploymentName", deployment.Name,
		"templateName", template.Name)

	// First attempt to use the Forgejo service to commit the application definition to the app-of-apps repo
	// We prefer this pattern as it better supports GitOps workflows
	forgejoConfigured := c.gitOpsIntegration(ctx, template, deployment, namespace)

	if !forgejoConfigured {
		// Fallback to direct ArgoCD Application creation if Forgejo integration is not configured
		log.Info("Falling back to direct ArgoCD Application creation",
			"deploymentName", deployment.Name)

		// Create ArgoCD Application that points to Helm chart
		if err := c.argoSvc.CreateApplicationForTemplate(ctx, template, deployment, namespace); err != nil {
			log.Error(err, "Failed to create ArgoCD Application for template-based deployment",
				"deploymentName", deployment.Name,
				"templateName", template.Name)
			// Continue with deployment, ArgoCD integration is not critical
		} else {
			log.Info("Successfully created ArgoCD Application for template-based deployment",
				"deploymentName", deployment.Name)
		}
	}
}

// gitOpsIntegration handles the GitOps-style integration with Forgejo and ArgoCD
func (c *WorkloadController) gitOpsIntegration(ctx context.Context, template *v1alpha1.Template, deployment *v1alpha1.ContainerMomDeployment, namespace string) bool {
	log := ctrl.LoggerFrom(ctx)

	// Check if we have a configured Forgejo service
	// Look for environment variables with configuration
	forgejoURL := os.Getenv("FORGEJO_URL")
	forgejoUser := os.Getenv("FORGEJO_USER")
	forgejoToken := os.Getenv("FORGEJO_TOKEN")
	forgejoPassword := os.Getenv("FORGEJO_PASSWORD")
	forgejoOrg := os.Getenv("FORGEJO_ORG")

	// If any of the required variables are missing, return false to indicate Forgejo is not configured
	if forgejoURL == "" || (forgejoUser == "" && forgejoToken == "" && forgejoPassword == "") {
		log.Info("Forgejo service not fully configured, skipping GitOps integration",
			"URL", forgejoURL != "",
			"Auth", (forgejoUser != "" || forgejoToken != "" || forgejoPassword != ""))
		return false
	}

	// Initialize the Forgejo service
	forgejoConfig := forgejo.Config{
		BaseURL:      forgejoURL,
		Token:        forgejoToken,
		Username:     forgejoUser,
		Password:     forgejoPassword,
		Organization: forgejoOrg,
	}

	forgejoSvc, err := forgejo.NewService(&forgejoConfig)
	if err != nil {
		log.Error(err, "Failed to initialize Forgejo service, GitOps integration will be disabled")
		return false
	}

	// Determine repository owner (use organization or fall back to authenticated user)
	repoOwner := forgejoOrg
	if repoOwner == "" {
		// The owner will be determined inside the Forgejo service methods
		// based on the organization or authenticated user
		repoOwner = ""
	}
	repoName := "container-mom-apps"

	// Ensure the app of apps chart structure exists
	err = forgejoSvc.CreateAppOfAppsChart(ctx, repoOwner, repoName)
	if err != nil {
		log.Error(err, "Failed to create app of apps chart structure",
			"repoOwner", repoOwner,
			"repoName", repoName)
		// Continue with the integration even if the chart structure creation fails
		// as the CommitArgoAppDefinition will still work without it
	}

	// Extract template values from deployment annotations
	templateValues := make(map[string]string)
	if deployment.Annotations != nil {
		for k, v := range deployment.Annotations {
			if strings.HasPrefix(k, "containermom.template/param-") {
				paramName := strings.TrimPrefix(k, "containermom.template/param-")
				templateValues[paramName] = v
			}
		}
	}

	// Add additional context values
	templateValues["namespace"] = namespace
	templateValues["name"] = deployment.Name
	if deployment.Labels != nil {
		if customerID, ok := deployment.Labels["containermom.cloud/customer-id"]; ok {
			templateValues["customer_id"] = customerID
		}
	}

	// Push the template values to the app of apps values.yaml
	err = forgejoSvc.PushTemplateValues(ctx, repoOwner, repoName, template, templateValues, deployment.Name)
	if err != nil {
		log.Error(err, "Failed to push template values to app of apps values.yaml",
			"deploymentName", deployment.Name,
			"templateName", template.Name)
		// Continue with the integration even if pushing values fails
	}

	// Commit the application definition to the app-of-apps repository
	err = forgejoSvc.CommitArgoAppDefinition(ctx, template, deployment, namespace)
	if err != nil {
		log.Error(err, "Failed to commit application definition to Forgejo",
			"deploymentName", deployment.Name,
			"templateName", template.Name)
		return false
	}

	log.Info("Successfully committed application definition to Forgejo repo",
		"deploymentName", deployment.Name,
		"templateName", template.Name)

	return true
}

// convertToK8sEnvVars converts ContainerMom env vars to Kubernetes env vars
func convertToK8sEnvVars(envVars []v1alpha1.EnvVar) []corev1.EnvVar {
	// First, count valid env vars (those with non-empty names)
	validCount := 0
	for _, env := range envVars {
		if env.Name != "" {
			validCount++
		}
	}

	// Create slice with exact size needed
	k8sEnvVars := make([]corev1.EnvVar, 0, validCount)

	// Only include env vars with non-empty names
	for _, env := range envVars {
		if env.Name != "" {
			k8sEnvVars = append(k8sEnvVars, corev1.EnvVar{
				Name:  env.Name,
				Value: env.Value,
			})
		}
	}

	return k8sEnvVars
}

func (c *WorkloadController) createOrUpdate(ctx context.Context, client client.Client, obj client.Object) error {
	// Try to create first
	if err := client.Create(ctx, obj); err != nil {
		if !errors.IsAlreadyExists(err) {
			return err
		}
		// If it exists, update it
		if err := client.Update(ctx, obj); err != nil {
			return err
		}
	}
	return nil
}

func buildHostname(domain v1alpha1.DomainSpec) string {
	if domain.UseWildcard {
		return "*." + domain.DomainName
	}
	if domain.Prefix != "" {
		return domain.Prefix + "." + domain.DomainName
	}
	return domain.DomainName
}
