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

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

	appsv1 "k8s.io/api/apps/v1"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"

	v1alpha1 "github.com/pfeifferj/container-mom-go/pkg/apis/containermom/v1alpha1"
	"github.com/pfeifferj/container-mom-go/pkg/utils"
)

// reconcileInCluster handles deployment of resources to a specific cluster
// Moved from workload_controller.go to improve code organization
func (c *Controller) reconcileInCluster(ctx context.Context, client client.Client, deployment *v1alpha1.ContainerMomDeployment) error {
	log := ctrl.LoggerFrom(ctx)

	// First, check if we need to create a namespace
	var appNamespace string
	if ns, ok := deployment.Annotations["containermom.mom/namespace"]; ok && ns != "" {
		appNamespace = ns
	} else {
		// No namespace annotation, need to create one
		customerName := deployment.Name
		if owner, ok := deployment.Labels["containermom.mom/owner"]; ok && owner != "" {
			customerName = owner
		}
		appNamespace = generateNamespaceName(customerName, deployment.Name)
	}

	// Check for pause annotation
	isPaused := false
	if deployment.Annotations != nil {
		if _, ok := deployment.Annotations["containermom.mom/paused"]; ok {
			isPaused = true
			log.Info("Deployment is paused, workloads will be scaled to 0",
				"deployment", deployment.Name,
				"namespace", appNamespace)
		}
	}

	// First verify the namespace exists using direct API call rather than client cache
	// This helps avoid cache synchronization issues
	nsCheckCmd := exec.Command("oc", "get", "namespace", appNamespace)
	nsOutput, nsErr := nsCheckCmd.CombinedOutput()

	namespaceExists := nsErr == nil

	if !namespaceExists {
		log.Info("Namespace doesn't exist or can't be accessed",
			"namespace", appNamespace,
			"error", string(nsOutput))

		// Create the namespace using oc to avoid cache issues
		nsCreateCmd := exec.Command("oc", "create", "namespace", appNamespace)
		createOutput, createErr := nsCreateCmd.CombinedOutput()

		if createErr != nil {
			log.Error(createErr, "Failed to create namespace",
				"namespace", appNamespace,
				"output", string(createOutput))
			return fmt.Errorf("failed to create namespace %s: %w", appNamespace, createErr)
		}

		log.Info("Created namespace using direct oc command",
			"namespace", appNamespace)

		// Add labels to the namespace
		labelCmd := exec.Command("oc", "label", "namespace", appNamespace,
			"app.kubernetes.io/managed-by=container-mom",
			fmt.Sprintf("containermom.mom/owner=%s", deployment.Name),
			"--overwrite")
		labelOutput, labelErr := labelCmd.CombinedOutput()

		if labelErr != nil {
			log.Error(labelErr, "Failed to label namespace",
				"namespace", appNamespace,
				"output", string(labelOutput))
			// Continue despite label error
		}

		// Update the deployment with the namespace annotation if needed
		if _, ok := deployment.Annotations["containermom.mom/namespace"]; !ok {
			freshDeployment := &v1alpha1.ContainerMomDeployment{}
			if err := c.client.Get(ctx, utils.NamespacedName{Name: deployment.Name, Namespace: deployment.Namespace}, freshDeployment); err != nil {
				log.Error(err, "Failed to get fresh deployment for annotation update", "name", deployment.Name)
			} else {
				// Initialize annotations if needed
				if freshDeployment.Annotations == nil {
					freshDeployment.Annotations = make(map[string]string)
				}
				// Set the namespace annotation
				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")
					// Continue despite error - we can recover on next reconciliation
				} else {
					log.Info("Updated deployment with namespace annotation", "namespace", appNamespace)
				}
			}
		}

		// Wait a moment for the namespace to be fully established
		time.Sleep(2 * time.Second)
	}

	// Get the image to use
	imageName := c.getImageForDeployment(ctx, deployment)

	// Check if deployment exists using oc
	deployCmd := exec.Command("oc", "get", "deployment", deployment.Name, "-n", appNamespace)
	deployOutput, deployErr := deployCmd.CombinedOutput()

	deploymentExists := deployErr == nil

	// First, create a secret for the environment variables
	if len(deployment.Spec.Env) > 0 {
		log.Info("Creating secret for environment variables",
			"namespace", appNamespace,
			"name", fmt.Sprintf("%s-env", deployment.Name))

		// Build a map of environment variables
		envData := make(map[string]string)
		for _, env := range deployment.Spec.Env {
			if env.Name != "" {
				envData[env.Name] = env.Value
			}
		}

		// Instead of building YAML directly, create a JSON structure for the secret
		// This avoids issues with YAML formatting and special characters
		secretJSON := fmt.Sprintf(`{
  "apiVersion": "v1",
  "kind": "Secret",
  "metadata": {
    "name": "%s-env",
    "namespace": "%s",
    "labels": {
      "app": "%s",
      "app.kubernetes.io/managed-by": "container-mom"
    }
  },
  "type": "Opaque",
  "stringData": {`, deployment.Name, appNamespace, deployment.Name)

		// Add each environment variable to the secret with proper JSON escaping
		first := true
		for key, value := range envData {
			if !first {
				secretJSON += ","
			}
			// Escape JSON special characters in the value
			escapedValue := escapeJSONString(value)
			secretJSON += fmt.Sprintf(`
    "%s": "%s"`, key, escapedValue)
			first = false
		}

		// Close the JSON structure
		secretJSON += `
  }
}`

		// Apply the secret
		secretCmd := exec.Command("oc", "apply", "-f", "-")
		secretCmd.Stdin = strings.NewReader(secretJSON)
		secretOutput, secretErr := secretCmd.CombinedOutput()

		if secretErr != nil {
			log.Error(secretErr, "Failed to create secret for environment variables",
				"namespace", appNamespace,
				"name", fmt.Sprintf("%s-env", deployment.Name),
				"output", string(secretOutput))
			// We'll continue and just log the error, as we can still create the deployment without env vars
		} else {
			log.Info("Created secret for environment variables",
				"namespace", appNamespace,
				"name", fmt.Sprintf("%s-env", deployment.Name))
		}
	}

	if !deploymentExists {
		log.Info("Deployment doesn't exist, will create",
			"namespace", appNamespace,
			"name", deployment.Name,
			"output", string(deployOutput))

		// Create deployment with oc
		// Set replicas based on pause state
		replicas := 1
		if isPaused {
			replicas = 0
		}

		// Convert CPU and memory requests to appropriate format
		cpuRequest := "100m"
		memoryRequest := "128Mi"
		if deployment.Spec.Resources.CPU != "" {
			cpuRequest = deployment.Spec.Resources.CPU
		}
		if deployment.Spec.Resources.Memory != "" {
			memoryRequest = deployment.Spec.Resources.Memory
		}

		// Base deployment YAML
		deployYAML := fmt.Sprintf(`
apiVersion: apps/v1
kind: Deployment
metadata:
  name: %s
  namespace: %s
  labels:
    app: %s
    app.kubernetes.io/managed-by: container-mom
spec:
  replicas: %d
  selector:
    matchLabels:
      app: %s
  template:
    metadata:
      labels:
        app: %s
    spec:
      containers:
      - name: %s
        image: %s
        ports:
        - containerPort: 8080
        resources:
          requests:
            cpu: %s
            memory: %s
          limits:
            cpu: %s
            memory: %s
`, deployment.Name, appNamespace, deployment.Name, replicas,
			deployment.Name, deployment.Name, deployment.Name,
			imageName, cpuRequest, memoryRequest, cpuRequest, memoryRequest)

		// Add environment variables from secret if we have any
		if len(deployment.Spec.Env) > 0 {
			deployYAML += fmt.Sprintf(`        envFrom:
        - secretRef:
            name: %s-env
`, deployment.Name)
		}

		// Apply the deployment
		applyCmd := exec.Command("oc", "apply", "-f", "-")
		applyCmd.Stdin = strings.NewReader(deployYAML)
		applyOutput, applyErr := applyCmd.CombinedOutput()

		if applyErr != nil {
			log.Error(applyErr, "Failed to create deployment with direct oc command",
				"namespace", appNamespace,
				"name", deployment.Name,
				"output", string(applyOutput))
			return fmt.Errorf("failed to create deployment: %w", applyErr)
		}

		log.Info("Created deployment", "namespace", appNamespace, "name", deployment.Name)
	} else {
		// If the deployment exists, we still need to update the secret
		// and then trigger a deployment rollout to pick up the changes

		if len(deployment.Spec.Env) > 0 {
			log.Info("Deployment exists, updating to ensure any env var changes are applied",
				"namespace", appNamespace,
				"name", deployment.Name)

			// Trigger a rollout restart to pick up the updated secret
			restartCmd := exec.Command("oc", "rollout", "restart", "deployment", deployment.Name, "-n", appNamespace)
			_, restartErr := restartCmd.CombinedOutput()

			if restartErr != nil {
				log.Error(restartErr, "Failed to restart deployment to apply env changes",
					"namespace", appNamespace,
					"name", deployment.Name)
				// Continue despite the error as it's not critical
			} else {
				log.Info("Triggered rollout to apply environment changes",
					"namespace", appNamespace,
					"name", deployment.Name)
			}
		}

		log.Info("Deployment already exists", "namespace", appNamespace, "name", deployment.Name)
	}

	// Create or update service using direct oc
	err := c.createOrUpdateService(ctx, client, deployment, appNamespace)
	if err != nil {
		log.Error(err, "Failed to create or update service",
			"namespace", appNamespace,
			"name", deployment.Name)
		return err
	}

	// Create route if domain is specified
	if deployment.Spec.Domain.DomainName != "" {
		if err := c.createOrUpdateRoute(ctx, deployment, appNamespace); err != nil {
			log.Error(err, "Failed to create route",
				"namespace", appNamespace,
				"name", deployment.Name)
			// Continue despite route error - it's not fatal
		}
	}

	return nil
}

// buildHostname creates a hostname from a DomainSpec
// Moved from workload_controller.go to improve code organization
func buildHostname(domain v1alpha1.DomainSpec) string {
	if domain.UseWildcard {
		return "*." + domain.DomainName
	}
	if domain.Prefix != "" {
		return domain.Prefix + "." + domain.DomainName
	}
	return domain.DomainName
}

// convertToK8sEnvVars converts ContainerMom env vars to Kubernetes env vars
// Moved from workload_controller.go to improve code organization
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
}

// createOrUpdateService creates or updates a Kubernetes Service for the given deployment
func (c *Controller) createOrUpdateService(ctx context.Context, client client.Client, deployment *v1alpha1.ContainerMomDeployment, namespace string) error {
	log := ctrl.LoggerFrom(ctx)

	// Check if service exists using oc to avoid cache issues
	serviceCmd := exec.Command("oc", "get", "service", deployment.Name, "-n", namespace)
	serviceOutput, serviceErr := serviceCmd.CombinedOutput()

	serviceExists := serviceErr == nil

	if !serviceExists {
		log.Info("Service doesn't exist, will create it",
			"namespace", namespace,
			"name", deployment.Name,
			"output", string(serviceOutput))

		// Create service YAML
		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, namespace, deployment.Name, deployment.Name)

		// Apply service using oc
		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", namespace,
				"name", deployment.Name,
				"output", string(applyOutput))
			return fmt.Errorf("failed to create service: %w", applyErr)
		}

		log.Info("Created service with direct command",
			"namespace", namespace,
			"name", deployment.Name)
	} else {
		// Service exists, no update needed as service selector and port don't change
		log.Info("Service already exists",
			"namespace", namespace,
			"name", deployment.Name)
	}

	return nil
}

// createOrUpdateRoute creates or updates an OpenShift route for the deployment
func (c *Controller) createOrUpdateRoute(ctx context.Context, deployment *v1alpha1.ContainerMomDeployment, namespace string) error {
	log := ctrl.LoggerFrom(ctx)

	// Get hostname from domain spec
	hostname := buildHostname(deployment.Spec.Domain)
	log.Info("Creating or updating route with hostname",
		"namespace", namespace,
		"name", deployment.Name,
		"hostname", hostname,
		"domainName", deployment.Spec.Domain.DomainName,
		"prefix", deployment.Spec.Domain.Prefix)

	// Call the centralized route creation function
	return CreateOrUpdateRoute(ctx, deployment, deployment.Name, deployment.Name, hostname, namespace)
}

// updateDeploymentWithCommandLine uses the oc/kubectl command to update a deployment when the cache method fails
func (c *Controller) updateDeploymentWithCommandLine(ctx context.Context, deployment *appsv1.Deployment, imageName string) error {
	log := ctrl.LoggerFrom(ctx)

	// Create patch data to update image
	patchData := fmt.Sprintf(`{"spec":{"template":{"spec":{"containers":[{"name":"%s","image":"%s"}]}}}}`,
		deployment.Name,
		imageName)

	// Use oc patch command
	patchCmd := exec.Command("oc", "patch", "deployment", deployment.Name,
		"-n", deployment.Namespace,
		"--type=strategic",
		"-p", patchData)

	output, err := patchCmd.CombinedOutput()
	if err != nil {
		// If deployment doesn't exist, create it
		if strings.Contains(string(output), "not found") {
			log.Info("Deployment not found, creating with command line",
				"namespace", deployment.Namespace,
				"name", deployment.Name)

			// Convert deployment to YAML
			// For simplicity, we'll use a simplified version that just has the core elements
			deployYAML := fmt.Sprintf(`
apiVersion: apps/v1
kind: Deployment
metadata:
  name: %s
  namespace: %s
  labels:
    app: %s
    app.kubernetes.io/managed-by: container-mom
spec:
  replicas: 1
  selector:
    matchLabels:
      app: %s
  template:
    metadata:
      labels:
        app: %s
    spec:
      containers:
      - name: %s
        image: %s
        ports:
        - containerPort: 8080
          name: http
`, deployment.Name, deployment.Namespace, deployment.Name,
				deployment.Name, deployment.Name, deployment.Name, imageName)

			createCmd := exec.Command("oc", "apply", "-f", "-")
			createCmd.Stdin = strings.NewReader(deployYAML)
			createOutput, createErr := createCmd.CombinedOutput()

			if createErr != nil {
				return fmt.Errorf("failed to create deployment with command line: %w, output: %s", createErr, string(createOutput))
			}

			log.Info("Created deployment with command line",
				"namespace", deployment.Namespace,
				"name", deployment.Name)

			return nil
		}

		return fmt.Errorf("failed to update deployment with command line: %w, output: %s", err, string(output))
	}

	log.Info("Updated deployment with command line",
		"namespace", deployment.Namespace,
		"name", deployment.Name,
		"image", imageName)

	return nil
}

// createServiceWithCommandLine creates a service using the oc/kubectl command line
// nolint:unused // Reserved for future implementation
func createServiceWithCommandLine(_, _ map[string]string, _ string, _ string, _ metav1.Object, _ int32, _ map[string]string) (*corev1.Service, error) {
	return nil, nil
}
