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

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

	"github.com/google/uuid"
	v1alpha1 "github.com/pfeifferj/container-mom-go/pkg/apis/containermom/v1alpha1"
	"github.com/pfeifferj/container-mom-go/pkg/builder"
	"k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/types"
	ctrl "sigs.k8s.io/controller-runtime"
)

// ensureBuildPermissions ensures that the necessary permissions exist in the target namespace
// for build and image operations.
func (c *WorkloadController) ensureBuildPermissions(ctx context.Context, namespace string) {
	log := ctrl.LoggerFrom(ctx)
	log.Info("Ensuring build permissions in namespace", "namespace", namespace)

	// 1. Create 'builder' service account if it doesn't exist
	cmd := exec.Command("oc", "get", "serviceaccount", "builder", "-n", namespace)
	if err := cmd.Run(); err != nil {
		log.Info("Creating builder service account", "namespace", namespace)
		createCmd := exec.Command("oc", "create", "serviceaccount", "builder", "-n", namespace)
		if output, err := createCmd.CombinedOutput(); err != nil {
			log.Error(err, "Failed to create builder service account", "namespace", namespace, "output", string(output))
			// Continue anyway - it might already exist
		} else {
			log.Info("Successfully created builder service account", "namespace", namespace)
		}
	}

	// 2. Add the builder service account to system:image-builder role in this namespace
	log.Info("Adding system:image-builder role to builder service account", "namespace", namespace)
	roleCmd := exec.Command("oc", "adm", "policy", "add-role-to-user", "system:image-builder",
		"--serviceaccount=builder", "-n", namespace)
	if output, err := roleCmd.CombinedOutput(); err != nil {
		log.Error(err, "Failed to add system:image-builder role", "namespace", namespace, "output", string(output))
		// Continue anyway - the role might already be assigned
	} else {
		log.Info("Successfully added system:image-builder role", "namespace", namespace)
	}

	// 3. Add edit access which is needed for managing builds in the namespace
	log.Info("Adding edit role to builder service account", "namespace", namespace)
	editCmd := exec.Command("oc", "adm", "policy", "add-role-to-user", "edit",
		"--serviceaccount=builder", "-n", namespace)
	if output, err := editCmd.CombinedOutput(); err != nil {
		log.Error(err, "Failed to add edit role", "namespace", namespace, "output", string(output))
		// Try with a more basic set of permissions if edit role is too broad
		basicCmd := exec.Command("oc", "adm", "policy", "add-role-to-user", "view",
			"--serviceaccount=builder", "-n", namespace)
		if basicOutput, basicErr := basicCmd.CombinedOutput(); basicErr != nil {
			log.Error(basicErr, "Also failed to add view role", "output", string(basicOutput))
			// Continue anyway - we'll try to work with what we've got
		} else {
			log.Info("Added view role as fallback", "namespace", namespace)
		}
	} else {
		log.Info("Successfully added edit role", "namespace", namespace)
	}

	// 4. Also give container-mom service account edit rights if not working in same namespace
	if c.operatorNamespace != namespace {
		log.Info("Adding edit role to container-mom service account",
			"operatorNamespace", c.operatorNamespace, "targetNamespace", namespace)

		// Give the operator's service account edit rights in the target namespace
		operatorSA := fmt.Sprintf("system:serviceaccount:%s:container-mom", c.operatorNamespace)

		// First try with explicit build-related permissions which are more likely to be granted
		buildPermsCmd := exec.Command("oc", "adm", "policy", "add-role-to-user", "system:image-builder",
			operatorSA, "-n", namespace)

		if output, err := buildPermsCmd.CombinedOutput(); err != nil {
			log.Error(err, "Failed to add system:image-builder role to operator's service account",
				"operatorNamespace", c.operatorNamespace,
				"targetNamespace", namespace,
				"output", string(output))
		} else {
			log.Info("Successfully added system:image-builder role to operator's service account",
				"operatorNamespace", c.operatorNamespace,
				"targetNamespace", namespace)
		}

		// Try with view permissions which are often more permissive
		viewCmd := exec.Command("oc", "adm", "policy", "add-role-to-user", "view",
			operatorSA, "-n", namespace)

		if output, err := viewCmd.CombinedOutput(); err != nil {
			log.Error(err, "Failed to add view role to operator's service account",
				"operatorNamespace", c.operatorNamespace,
				"targetNamespace", namespace,
				"output", string(output))
		} else {
			log.Info("Successfully added view role to operator's service account",
				"operatorNamespace", c.operatorNamespace,
				"targetNamespace", namespace)
		}
	}

	log.Info("Successfully configured build permissions", "namespace", namespace)
}

// buildImageFromSource builds an image from source code
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
	}

	// Ensure the build namespace has the necessary permissions
	// This is critical for the build process to work correctly
	log.Info("Setting up build permissions before creating resources", "namespace", namespace)
	c.ensureBuildPermissions(ctx, namespace)

	// Instead of scaling down deployments, temporarily increase the resource quota for the namespace
	// This allows builds to run while existing deployments continue to operate
	resourceManager := NewResourceManager(c)
	quotaIncreased := false
	err := resourceManager.TemporarilyIncreaseCertManagerQuota(ctx, namespace)
	if err != nil {
		log.Error(err, "Failed to increase resource quota for build, but will continue",
			"namespace", namespace)
		// Continue with the build anyway
	} else {
		quotaIncreased = true
	}

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

		// Restore the original resource quota if we increased it
		if quotaIncreased {
			// Using cert-manager-quota which is the name used in TemporarilyIncreaseCertManagerQuota
			if err := resourceManager.PatchExistingQuota(ctx, namespace, "cert-manager-quota", "400m", "2Gi"); err != nil {
				log.Error(err, "Failed to restore resource quota after build")
			}
		}

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

		// Restore the original resource quota if we increased it
		if quotaIncreased {
			// Using cert-manager-quota which is the name used in TemporarilyIncreaseCertManagerQuota
			if err := resourceManager.PatchExistingQuota(ctx, namespace, "cert-manager-quota", "400m", "2Gi"); err != nil {
				log.Error(err, "Failed to restore resource quota after build")
			}
		}

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

	// Restore the original resource quota if we increased it
	if quotaIncreased {
		// Using cert-manager-quota which is the name used in TemporarilyIncreaseCertManagerQuota
		if err := resourceManager.PatchExistingQuota(ctx, namespace, "cert-manager-quota", "400m", "2Gi"); err != nil {
			log.Error(err, "Failed to restore resource quota after build")
		}
	}

	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
}

// 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
}

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

				// Apply the status update with retries
				maxRetries := 3
				retryDelay := 500 * time.Millisecond
				statusUpdated := false

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

					if err != nil {
						if errors.IsNotFound(err) {
							// Deployment was deleted, don't update status
							log.Info("Deployment was deleted before status update could be applied")
							break
						}
						log.Error(err, "Failed to get fresh deployment for status update, retrying...",
							"deploymentName", deployment.Name,
							"attempt", attempt+1,
							"maxRetries", maxRetries)

						// Wait before retrying
						time.Sleep(retryDelay)
						retryDelay *= 2
						continue
					}

					// Update image in the fresh deployment status
					freshDeployment.Status.Image = imageName

					if updateErr := c.client.Status().Update(ctx, freshDeployment); updateErr != nil {
						if errors.IsConflict(updateErr) {
							log.Info("Conflict when updating deployment status with built image, retrying...",
								"deploymentName", deployment.Name,
								"attempt", attempt+1,
								"maxRetries", maxRetries)

							// Wait before retrying
							time.Sleep(retryDelay)
							retryDelay *= 2
							continue
						}

						log.Error(updateErr, "Failed to update deployment status with built image, non-retriable error",
							"deploymentName", deployment.Name,
							"image", imageName)
						break
					}

					// Success
					statusUpdated = true
					log.Info("Successfully updated deployment status with built image",
						"deploymentName", deployment.Name,
						"image", imageName)
					break
				}

				if !statusUpdated {
					log.Error(nil, "Failed to update deployment status with built image after maximum retries",
						"deploymentName", deployment.Name,
						"image", imageName)
				}

				return imageName
			}
		}
	}

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