// Package workload provides controller implementation for workload resources
package workload

import (
	"context"
	"fmt"
	"os"
	"os/exec"
	"strings"
	"time"

	"k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"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"
	"sigs.k8s.io/controller-runtime/pkg/event"
	"sigs.k8s.io/controller-runtime/pkg/predicate"

	momv1 "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/templates"
)

// Controller reconciles ContainerMomDeployment objects across multiple clusters
type Controller 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
	operatorNamespace string // Namespace where the operator is running
}

// NewController creates a new workload controller
func NewController(mgr ctrl.Manager, clusterConfigs map[string]*rest.Config) (*Controller, 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",
	})

	// Determine the operator namespace from environment variables
	operatorNamespace := os.Getenv("OPERATOR_NAMESPACE")
	if operatorNamespace == "" {
		// Fall back to watch namespace
		operatorNamespace = os.Getenv("WATCH_NAMESPACE")
	}
	if operatorNamespace == "" {
		// Default to container-mom-system if neither is set
		operatorNamespace = "container-mom-system"
	}

	ctrl.Log.Info("Operator running in namespace", "namespace", operatorNamespace)

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

	// 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(&momv1.ContainerMomDeployment{}).
		WithOptions(controller.Options{
			MaxConcurrentReconciles: 1,
		}).
		// Add predicates to filter unnecessary reconciliation events
		WithEventFilter(predicate.Or(
			// Only reconcile when the spec changes (generation changes)
			predicate.GenerationChangedPredicate{},
			// Reconcile when annotations change to handle force-rebuild, etc.
			predicate.AnnotationChangedPredicate{},
			// Custom predicate for creation and deletion events
			predicate.Funcs{
				CreateFunc: func(_ event.CreateEvent) bool {
					return true // Always reconcile on create
				},
				DeleteFunc: func(_ event.DeleteEvent) bool {
					return true // Always reconcile on delete
				},
				UpdateFunc: func(_ event.UpdateEvent) bool {
					// Skip reconciliation for status-only updates
					return false
				},
				GenericFunc: func(_ event.GenericEvent) bool {
					return false // Ignore generic events
				},
			},
		))

	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 *Controller) 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))

	// Get the ContainerMomDeployment
	var deployment momv1.ContainerMomDeployment
	if err := c.client.Get(ctx, req.NamespacedName, &deployment); err != nil {
		if errors.IsNotFound(err) {
			// Deployment was deleted, nothing to do
			return ctrl.Result{}, nil
		}
		log.Error(err, "Failed to get deployment", "name", req.Name)
		return ctrl.Result{}, err
	}

	// Check for annotations
	log.Info("CR annotations", "name", req.Name, "annotations", fmt.Sprintf("%v", deployment.Annotations))

	// Check if we need to ensure we have the namespace annotation
	var appNamespace string
	if ns, ok := deployment.Annotations["containermom.mom/namespace"]; ok && ns != "" {
		appNamespace = ns
		log.Info("CR already has namespace annotation", "name", req.Name, "namespace", req.Namespace, "appNamespace", appNamespace)
	} else {
		// Create a namespace for this app
		customerName := deployment.Name
		if owner, ok := deployment.Labels["containermom.mom/owner"]; ok && owner != "" {
			customerName = owner
		}

		appNamespace = generateNamespaceName(customerName, deployment.Name)

		// Create a fresh copy before updating
		freshDeployment := &momv1.ContainerMomDeployment{}
		if err := c.client.Get(ctx, req.NamespacedName, freshDeployment); err != nil {
			log.Error(err, "Failed to get fresh deployment for annotation update", "name", req.Name)
		} else {
			if freshDeployment.Annotations == nil {
				freshDeployment.Annotations = make(map[string]string)
			}
			freshDeployment.Annotations["containermom.mom/namespace"] = appNamespace
			if err := c.client.Update(ctx, freshDeployment); err != nil {
				log.Error(err, "Failed to update deployment with namespace annotation", "name", req.Name)
			} else {
				log.Info("Updated deployment with namespace annotation", "name", req.Name, "appNamespace", appNamespace)
				// We want to get the most updated version of the deployment
				if err := c.client.Get(ctx, req.NamespacedName, &deployment); err != nil {
					log.Error(err, "Failed to get updated deployment", "name", req.Name)
				}
			}
		}
	}

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

			// Update status to show we're starting a forced reconciliation
			updatedStatus := deployment.Status
			updatedStatus.Phase = "ForcedReconciliation"
			if err := c.updateDeploymentStatus(ctx, &deployment, updatedStatus); err != nil {
				log.Error(err, "Failed to update deployment status")
			}
		}
	}

	// CRITICAL: Create or update route regardless of other conditions if domain is specified
	if deployment.Spec.Domain.DomainName != "" && appNamespace != "" {
		log.Info("Domain specified in deployment, ensuring route creation",
			"namespace", appNamespace,
			"name", deployment.Name,
			"domain", deployment.Spec.Domain.DomainName,
			"prefix", deployment.Spec.Domain.Prefix)

		// First ensure service exists
		serviceCmd := exec.Command("oc", "get", "service", deployment.Name, "-n", appNamespace)
		serviceOutput, serviceErr := serviceCmd.CombinedOutput()
		if serviceErr != nil {
			log.Info("Service does not exist yet, will create it",
				"namespace", appNamespace,
				"name", deployment.Name,
				"output", string(serviceOutput))

			// Create service directly using CLI to avoid cache issues
			serviceYAML := fmt.Sprintf(`
apiVersion: v1
kind: Service
metadata:
  name: %s
  namespace: %s
  labels:
    app: %s
    app.kubernetes.io/managed-by: container-mom
spec:
  selector:
    app: %s
  ports:
  - name: http
    port: 80
    targetPort: 8080
    protocol: TCP
  type: ClusterIP
`, deployment.Name, appNamespace, deployment.Name, deployment.Name)

			// Use oc apply command
			applyCmd := exec.Command("oc", "apply", "-f", "-")
			applyCmd.Stdin = strings.NewReader(serviceYAML)
			applyOutput, applyErr := applyCmd.CombinedOutput()

			if applyErr != nil {
				log.Error(applyErr, "Failed to create service with direct command",
					"namespace", appNamespace,
					"name", deployment.Name,
					"output", string(applyOutput))
			} else {
				log.Info("Created service with direct command",
					"namespace", appNamespace,
					"name", deployment.Name)
			}
		} else {
			log.Info("Service exists, proceeding with route creation",
				"namespace", appNamespace,
				"name", deployment.Name)
		}

		// Now create route
		hostname := buildHostname(deployment.Spec.Domain)
		routeYaml := fmt.Sprintf(`
apiVersion: route.openshift.io/v1
kind: Route
metadata:
  name: %s
  namespace: %s
  labels:
    app: %s
    app.kubernetes.io/managed-by: container-mom
spec:
  host: %s
  to:
    kind: Service
    name: %s
    weight: 100
  port:
    targetPort: http
  tls:
    termination: edge
    insecureEdgeTerminationPolicy: Redirect
`, deployment.Name, appNamespace, deployment.Name, hostname, deployment.Name)

		// Use oc apply with route YAML
		routeCmd := exec.Command("oc", "apply", "-f", "-")
		routeCmd.Stdin = strings.NewReader(routeYaml)
		routeOutput, routeErr := routeCmd.CombinedOutput()

		if routeErr != nil {
			log.Error(routeErr, "Failed to create route with direct command",
				"namespace", appNamespace,
				"name", deployment.Name,
				"output", string(routeOutput))
		} else {
			log.Info("Created or updated route with direct command",
				"namespace", appNamespace,
				"name", deployment.Name,
				"hostname", hostname,
				"output", string(routeOutput))
		}

		// Verify route exists
		verifyCmd := exec.Command("oc", "get", "route", deployment.Name, "-n", appNamespace, "-o", "jsonpath={.spec.host}")
		verifyOutput, verifyErr := verifyCmd.CombinedOutput()
		if verifyErr != nil {
			log.Error(verifyErr, "Failed to verify route creation",
				"namespace", appNamespace,
				"name", deployment.Name,
				"output", string(verifyOutput))
		} else {
			actualHostname := strings.TrimSpace(string(verifyOutput))
			log.Info("Route verification",
				"namespace", appNamespace,
				"name", deployment.Name,
				"expectedHostname", hostname,
				"actualHostname", actualHostname,
				"match", actualHostname == hostname)
		}
	}

	// Update status to show deployment is building
	updatedStatus := deployment.Status
	updatedStatus.Phase = "Building"
	if err := c.updateDeploymentStatus(ctx, &deployment, updatedStatus); err != nil {
		log.Error(err, "Failed to update deployment status for build start")
	}

	// 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, exists := c.clusters[clusterRef]
		if !exists {
			// Try to find by partial match
			// This allows specifying clusters by region part (e.g., "euw" for "euw-wkl1")
			var matchKeys []string
			for k := range c.clusters {
				if strings.Contains(k, clusterRef) {
					matchKeys = append(matchKeys, k)
				}
			}

			if len(matchKeys) == 0 {
				log.Error(fmt.Errorf("no matching cluster found"), "failed to find cluster",
					"clusterRef", clusterRef, "availableClusters", len(c.clusters))
				errs = append(errs, fmt.Errorf("no matching cluster found for %s", clusterRef))
				continue
			}

			if len(matchKeys) > 1 {
				log.Error(fmt.Errorf("ambiguous cluster reference"), "multiple matching clusters found",
					"clusterRef", clusterRef, "matches", matchKeys)
				errs = append(errs, fmt.Errorf("ambiguous cluster reference %s, matches: %v", clusterRef, matchKeys))
				continue
			}

			// Use the single matching cluster
			cluster = c.clusters[matchKeys[0]]
			log.Info("Using matched cluster", "clusterRef", clusterRef, "matchedCluster", matchKeys[0])
		}

		// Update status to show deployment is in progress for this cluster
		clusterStatus := momv1.ClusterStatus{
			Name:  clusterRef,
			Phase: "Deploying",
		}
		updatedStatus := deployment.Status
		updatedStatus.Phase = "Deploying"

		// Update or add the cluster status
		found := false
		for i, cs := range updatedStatus.ClusterStatus {
			if cs.Name == clusterRef {
				updatedStatus.ClusterStatus[i] = clusterStatus
				found = true
				break
			}
		}
		if !found {
			updatedStatus.ClusterStatus = append(updatedStatus.ClusterStatus, clusterStatus)
		}

		if err := c.updateDeploymentStatus(ctx, &deployment, updatedStatus); err != nil {
			log.Error(err, "Failed to update deployment status for cluster deployment")
		}

		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))

			// Update status to show deployment failed for this cluster
			clusterStatus.Phase = "Failed"
		} else {
			deployedClusters[clusterRef] = true
			clusterStatus.Phase = "Deployed"
		}

		// Update the cluster status
		updatedStatus = deployment.Status
		for i, cs := range updatedStatus.ClusterStatus {
			if cs.Name == clusterRef {
				updatedStatus.ClusterStatus[i] = clusterStatus
				break
			}
		}

		if err := c.updateDeploymentStatus(ctx, &deployment, updatedStatus); err != nil {
			log.Error(err, "Failed to update deployment status after cluster deployment")
		}
	}

	// Update deployment status with results
	status := momv1.ContainerMomDeploymentStatus{
		Phase:          momv1.DeploymentPhaseSucceeded,
		LastUpdateTime: metav1.Now(),
	}

	// Copy existing cluster status
	freshDeployment := &momv1.ContainerMomDeployment{}
	if err := c.client.Get(ctx, req.NamespacedName, freshDeployment); err == nil {
		status.ClusterStatus = freshDeployment.Status.ClusterStatus
	}

	if len(errs) > 0 {
		status.Phase = momv1.DeploymentPhaseFailed
		errMsgs := make([]string, 0, len(errs))
		for _, err := range errs {
			errMsgs = append(errMsgs, err.Error())
		}

		status.Conditions = append(status.Conditions, momv1.Condition{
			Type:    "Failed",
			Status:  "True",
			Message: strings.Join(errMsgs, "; "),
			Reason:  "ReconciliationErrors",
		})
	} else {
		status.Conditions = append(status.Conditions, momv1.Condition{
			Type:    "Ready",
			Status:  "True",
			Message: "Deployment has been successfully reconciled",
			Reason:  "ReconciliationComplete",
		})
	}

	// Add information about which clusters were deployed to
	clustersStr := ""
	for cluster := range deployedClusters {
		if clustersStr != "" {
			clustersStr += ", "
		}
		clustersStr += cluster
	}
	status.Conditions = append(status.Conditions, momv1.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, momv1.Condition{
			Type:    "Namespace",
			Status:  "True",
			Message: ns,
		})
	}

	// Update the CR status
	if err := c.updateDeploymentStatus(ctx, &deployment, status); err != nil {
		log.Error(err, "failed to update final 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 appNamespace == "" {
		// 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, but only if forced or specifically needed
	if forcedReconciliation || len(errs) > 0 {
		return ctrl.Result{RequeueAfter: 5 * time.Minute}, nil
	}

	// For normal, successful reconciliations, requeue less frequently
	return ctrl.Result{RequeueAfter: 30 * time.Minute}, nil
}

// updateDeploymentStatus is a helper function to update the status of a ContainerMomDeployment
// THIS FUNCTION HAS BEEN MOVED TO workload_status.go
// The implementation there should be used instead

// updateBuildStatus updates the deployment status with build results
// THIS FUNCTION HAS BEEN MOVED TO workload_status.go
// The implementation there should be used instead

// getImageForDeployment returns the image to use, falling back to a derived image if needed
// THIS FUNCTION HAS BEEN MOVED TO workload_image.go
// The implementation there should be used instead

// removeForceRebuildAnnotation removes the force-rebuild annotation from the deployment
// THIS FUNCTION HAS BEEN MOVED TO workload_image.go
// The implementation there should be used instead
