package controller

import (
	"context"
	"crypto/x509"
	"encoding/pem"
	"fmt"
	"reflect"
	"regexp"
	"slices"
	"strings"
	"time"

	"github.com/go-logr/logr"
	ocpconfigv1 "github.com/openshift/api/config/v1"
	routev1 "github.com/openshift/api/route/v1"
	"github.com/pkg/errors"

	corev1 "k8s.io/api/core/v1"
	v1 "k8s.io/api/core/v1"
	networkingv1 "k8s.io/api/networking/v1"
	storagev1 "k8s.io/api/storage/v1"
	k8serrors "k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/api/meta"
	"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/client-go/tools/record"

	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller"
	"sigs.k8s.io/controller-runtime/pkg/event"
	"sigs.k8s.io/controller-runtime/pkg/handler"
	"sigs.k8s.io/controller-runtime/pkg/manager"
	"sigs.k8s.io/controller-runtime/pkg/predicate"
	"sigs.k8s.io/controller-runtime/pkg/reconcile"
	"sigs.k8s.io/controller-runtime/pkg/source"

	cdiv1 "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1"
	"kubevirt.io/containerized-data-importer/pkg/common"
	cc "kubevirt.io/containerized-data-importer/pkg/controller/common"
	"kubevirt.io/containerized-data-importer/pkg/operator"
	"kubevirt.io/containerized-data-importer/pkg/util"
	"kubevirt.io/containerized-data-importer/pkg/util/cert"
)

// AnnConfigAuthority is the annotation specifying a resource as the CDIConfig authority
const (
	AnnConfigAuthority = "cdi.kubevirt.io/configAuthority"

	errResourceDoesntExist     = "ErrResourceDoesntExist"
	messageResourceDoesntExist = "Resource managed by %q doesn't exist"

	defaultCPULimit   = "750m"
	defaultMemLimit   = "600M"
	defaultCPURequest = "100m"
	defaultMemRequest = "60M"

	rootCertificateConfigMap = "kube-root-ca.crt"
)

// CDIConfigReconciler members
type CDIConfigReconciler struct {
	client client.Client
	// use this for getting any resources not in the install namespace or cluster scope
	uncachedClient         client.Client
	recorder               record.EventRecorder
	scheme                 *runtime.Scheme
	log                    logr.Logger
	uploadProxyServiceName string
	configName             string
	cdiNamespace           string
	installerLabels        map[string]string
}

// Reconcile the reconcile loop for the CDIConfig object.
func (r *CDIConfigReconciler) Reconcile(_ context.Context, req reconcile.Request) (reconcile.Result, error) {
	log := r.log.WithValues("CDIConfig", req.NamespacedName)
	log.Info("reconciling CDIConfig")

	config, err := r.createCDIConfig()
	if err != nil {
		log.Error(err, "Unable to create CDIConfig")
		return reconcile.Result{}, err
	}
	// Keep a copy of the original for comparison later.
	currentConfigCopy := config.DeepCopyObject()

	config.Status.Preallocation = config.Spec.Preallocation != nil && *config.Spec.Preallocation

	// ignore whatever is in config spec and set to operator view
	if err := r.setOperatorParams(config); err != nil {
		return reconcile.Result{}, err
	}

	if err := r.reconcileUploadProxy(config); err != nil {
		return reconcile.Result{}, err
	}

	if err := r.reconcileStorageClass(config); err != nil {
		return reconcile.Result{}, err
	}

	if err := r.reconcileDefaultPodResourceRequirements(config); err != nil {
		return reconcile.Result{}, err
	}

	if err := r.reconcileImagePullSecrets(config); err != nil {
		return reconcile.Result{}, err
	}

	if err := r.reconcileFilesystemOverhead(config); err != nil {
		return reconcile.Result{}, err
	}

	if err := r.reconcileImportProxy(config); err != nil {
		return reconcile.Result{}, err
	}

	if !reflect.DeepEqual(currentConfigCopy, config) {
		// Updates have happened, update CDIConfig.
		log.Info("Updating CDIConfig", "CDIConfig.Name", config.Name, "config", config)
		if err := r.client.Update(context.TODO(), config); err != nil {
			return reconcile.Result{}, err
		}
	}

	return reconcile.Result{}, nil
}

func (r *CDIConfigReconciler) setOperatorParams(config *cdiv1.CDIConfig) error {
	util.SetRecommendedLabels(config, r.installerLabels, "cdi-controller")

	cdiCR, err := cc.GetActiveCDI(context.TODO(), r.client)
	if err != nil {
		return err
	}

	if cdiCR == nil {
		return nil
	}

	if _, ok := cdiCR.Annotations[AnnConfigAuthority]; !ok {
		return nil
	}

	if cdiCR.Spec.Config == nil {
		config.Spec = cdiv1.CDIConfigSpec{}
	} else {
		config.Spec = *cdiCR.Spec.Config
	}

	return nil
}

func (r *CDIConfigReconciler) reconcileUploadProxy(config *cdiv1.CDIConfig) error {
	log := r.log.WithName("CDIconfig").WithName("UploadProxyReconcile")
	config.Status.UploadProxyURL = config.Spec.UploadProxyURLOverride
	// No override, try Ingress
	if config.Status.UploadProxyURL == nil {
		ingress, err := r.reconcileIngress(config)
		if err != nil {
			log.Error(err, "Unable to reconcile Ingress")
			return err
		}

		if ingress != nil {
			if err := r.reconcileUploadProxyIngressCA(config, *ingress); err != nil {
				log.Error(err, "Unable to reconcile Ingress CA")
				return fmt.Errorf("unable to reconcile Ingress CA: %w", err)
			}
		}
	}
	// No override or Ingress, try Route
	if config.Status.UploadProxyURL == nil {
		if err := r.reconcileRoute(config); err != nil {
			log.Error(err, "Unable to reconcile Routes")
			return err
		}

		if err := r.reconcileUploadProxyRouteCA(config); err != nil {
			log.Error(err, "Unable to reconcile Route CA")
			return fmt.Errorf("unable to reconcile Route CA: %w", err)
		}
	}
	return nil
}

func (r *CDIConfigReconciler) reconcileUploadProxyIngressCA(config *cdiv1.CDIConfig, ingress networkingv1.Ingress) error {
	log := r.log.WithName("CDIconfig").WithName("UploadProxyIngressCAReconcile")

	url := config.Status.UploadProxyURL
	if url == nil || *url == "" {
		return nil
	}

	var secretName string
	i := slices.IndexFunc(ingress.Spec.TLS, func(tls networkingv1.IngressTLS) bool { return tls.SecretName != "" })
	if i == -1 {
		log.Info("Secret name not found in Ingress")
		config.Status.UploadProxyCA = nil
		return nil
	}
	secretName = ingress.Spec.TLS[i].SecretName

	var secret corev1.Secret
	err := r.client.Get(context.TODO(), types.NamespacedName{Name: secretName, Namespace: r.cdiNamespace}, &secret)
	if err != nil {
		return fmt.Errorf("unable to get secret %q: %v", secretName, err)
	}

	certBytes, ok := secret.Data["tls.crt"]
	if !ok {
		log.Info(fmt.Sprintf("Secret %q does not contain %q", secretName, "tls.crt"))
		config.Status.UploadProxyCA = nil
		return nil
	}

	certs, err := cert.ParseCertsPEM(certBytes)
	if err != nil {
		return fmt.Errorf("unable to parse tls.crt: %v", err)
	}

	s, err := findCertByHostName(*config.Status.UploadProxyURL, certs)
	if err != nil {
		return err
	} else if s == "" {
		log.Info("No matching valid certificate found for upload proxy URL", "UploadProxyURL", *config.Status.UploadProxyURL)
		config.Status.UploadProxyCA = nil
		return nil
	}

	log.Info("Setting upload proxy CA", "UploadProxyCA", s)
	config.Status.UploadProxyCA = &s
	return nil
}

func (r *CDIConfigReconciler) reconcileUploadProxyRouteCA(config *cdiv1.CDIConfig) error {
	log := r.log.WithName("CDIconfig").WithName("UploadProxyRouteCAReconcile")

	if config.Status.UploadProxyURL == nil || *config.Status.UploadProxyURL == "" {
		log.Info("No upload proxy URL found, setting upload proxy CA to blank")
		config.Status.UploadProxyCA = nil
		return nil
	}

	var cm corev1.ConfigMap
	err := r.client.Get(context.TODO(), types.NamespacedName{Name: rootCertificateConfigMap, Namespace: r.cdiNamespace}, &cm)
	if err != nil {
		log.Info(fmt.Sprintf("Could not get certificates: %v", err))
		config.Status.UploadProxyCA = nil
		return nil
	}

	rawCert, ok := cm.Data["ca.crt"]
	if !ok {
		log.Info(fmt.Sprintf("Config map %q does not contain %q", rootCertificateConfigMap, "ca.crt"))
		config.Status.UploadProxyCA = nil
		return nil
	}

	certs, err := cert.ParseCertsPEM([]byte(rawCert))
	if err != nil {
		return fmt.Errorf("unable to parse ca.crt: %v", err)
	}

	s, err := findCertByHostName(*config.Status.UploadProxyURL, certs)
	if err != nil {
		return err
	} else if s == "" {
		log.Info("No matching valid certificate found for upload proxy URL", "UploadProxyURL", *config.Status.UploadProxyURL)
		config.Status.UploadProxyCA = nil
		return nil
	}

	log.Info("Setting upload proxy CA", "UploadProxyCA", s)
	config.Status.UploadProxyCA = &s
	return nil
}

func findCertByHostName(hostName string, certs []*x509.Certificate) (string, error) {
	now := time.Now()
	var latestValidCert *x509.Certificate
	for _, cert := range certs {
		// Check validity
		if now.After(cert.NotAfter) {
			continue
		}
		if now.Before(cert.NotBefore) {
			continue
		}
		if err := cert.VerifyHostname(hostName); err != nil {
			continue
		}

		// Check if this is the cert with the latest expiration date
		if latestValidCert == nil {
			latestValidCert = cert
			continue
		}
		if latestValidCert.NotAfter.After(cert.NotAfter) {
			continue
		}
		latestValidCert = cert
	}

	if latestValidCert != nil {
		return buildPemFromCert(latestValidCert, certs)
	}

	if len(certs) > 0 {
		return buildPemFromAllCerts(certs)
	}

	return "", nil
}

func buildPemFromCert(matchingCert *x509.Certificate, allCerts []*x509.Certificate) (string, error) {
	pemOut := strings.Builder{}

	if err := pem.Encode(&pemOut, &pem.Block{Type: "CERTIFICATE", Bytes: matchingCert.Raw}); err != nil {
		return "", fmt.Errorf("could not encode certificate: %w", err)
	}

	if matchingCert.Issuer.CommonName != matchingCert.Subject.CommonName && !matchingCert.IsCA {
		//lookup issuer recursively, if not found a blank is returned.
		chain, err := findCertByHostName(matchingCert.Issuer.CommonName, allCerts)
		if err != nil {
			return "", err
		}

		if _, err := pemOut.WriteString(chain); err != nil {
			return "", fmt.Errorf("could not write issuer certificate: %w", err)
		}
	}

	return strings.TrimSpace(pemOut.String()), nil
}

func buildPemFromAllCerts(allCerts []*x509.Certificate) (string, error) {
	now := time.Now()
	pemOut := strings.Builder{}
	for _, cert := range allCerts {
		if now.After(cert.NotAfter) {
			continue
		}

		if now.Before(cert.NotBefore) {
			continue
		}

		if err := pem.Encode(&pemOut, &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}); err != nil {
			return "", fmt.Errorf("could not encode certificate: %w", err)
		}
	}

	return strings.TrimSpace(pemOut.String()), nil
}

func (r *CDIConfigReconciler) reconcileIngress(config *cdiv1.CDIConfig) (*networkingv1.Ingress, error) {
	log := r.log.WithName("CDIconfig").WithName("IngressReconcile")
	ingressList := &networkingv1.IngressList{}
	if err := r.client.List(context.TODO(), ingressList, &client.ListOptions{Namespace: r.cdiNamespace}); cc.IgnoreIsNoMatchError(err) != nil {
		return nil, err
	}
	for _, ingress := range ingressList.Items {
		ingressURL := getURLFromIngress(&ingress, r.uploadProxyServiceName)
		if ingressURL != "" {
			log.Info("Setting upload proxy url", "IngressURL", ingressURL)
			config.Status.UploadProxyURL = &ingressURL
			return &ingress, nil
		}
	}
	log.Info("No ingress found, setting to blank", "IngressURL", "")
	config.Status.UploadProxyURL = nil
	return nil, nil
}

func (r *CDIConfigReconciler) reconcileRoute(config *cdiv1.CDIConfig) error {
	log := r.log.WithName("CDIconfig").WithName("RouteReconcile")
	routeList := &routev1.RouteList{}
	if err := r.client.List(context.TODO(), routeList, &client.ListOptions{Namespace: r.cdiNamespace}); cc.IgnoreIsNoMatchError(err) != nil {
		return err
	}
	for _, route := range routeList.Items {
		routeURL := getURLFromRoute(&route, r.uploadProxyServiceName)
		if routeURL != "" {
			log.Info("Setting upload proxy url", "RouteURL", routeURL)
			config.Status.UploadProxyURL = &routeURL
			return nil
		}
	}
	log.Info("No route found, setting to blank", "RouteURL", "")
	config.Status.UploadProxyURL = nil
	return nil
}

func (r *CDIConfigReconciler) reconcileStorageClass(config *cdiv1.CDIConfig) error {
	log := r.log.WithName("CDIconfig").WithName("StorageClassReconcile")
	storageClassList := &storagev1.StorageClassList{}
	if err := r.client.List(context.TODO(), storageClassList, &client.ListOptions{}); err != nil {
		return err
	}

	// Check config for scratch space class
	if config.Spec.ScratchSpaceStorageClass != nil {
		for _, storageClass := range storageClassList.Items {
			if storageClass.Name == *config.Spec.ScratchSpaceStorageClass {
				log.Info("Setting scratch space to override", "storageClass.Name", storageClass.Name)
				config.Status.ScratchSpaceStorageClass = storageClass.Name
				return nil
			}
		}
	}

	// If config not set, scratch space will default to using the storage class of target resource
	config.Status.ScratchSpaceStorageClass = ""
	return nil
}

func (r *CDIConfigReconciler) reconcileImagePullSecrets(config *cdiv1.CDIConfig) error {
	config.Status.ImagePullSecrets = config.Spec.ImagePullSecrets
	return nil
}

func (r *CDIConfigReconciler) reconcileDefaultPodResourceRequirements(config *cdiv1.CDIConfig) error {
	cpuLimit, _ := resource.ParseQuantity(defaultCPULimit)
	memLimit, _ := resource.ParseQuantity(defaultMemLimit)
	cpuRequest, _ := resource.ParseQuantity(defaultCPURequest)
	memRequest, _ := resource.ParseQuantity(defaultMemRequest)
	config.Status.DefaultPodResourceRequirements = &v1.ResourceRequirements{
		Limits: map[v1.ResourceName]resource.Quantity{
			v1.ResourceCPU:    cpuLimit,
			v1.ResourceMemory: memLimit,
		},
		Requests: map[v1.ResourceName]resource.Quantity{
			v1.ResourceCPU:    cpuRequest,
			v1.ResourceMemory: memRequest,
		},
	}

	if config.Spec.PodResourceRequirements != nil {
		if config.Spec.PodResourceRequirements.Limits != nil {
			if cpu, exist := config.Spec.PodResourceRequirements.Limits[v1.ResourceCPU]; exist {
				config.Status.DefaultPodResourceRequirements.Limits[v1.ResourceCPU] = cpu
			}

			if memory, exist := config.Spec.PodResourceRequirements.Limits[v1.ResourceMemory]; exist {
				config.Status.DefaultPodResourceRequirements.Limits[v1.ResourceMemory] = memory
			}
		}

		if config.Spec.PodResourceRequirements.Requests != nil {
			if cpu, exist := config.Spec.PodResourceRequirements.Requests[v1.ResourceCPU]; exist {
				config.Status.DefaultPodResourceRequirements.Requests[v1.ResourceCPU] = cpu
			}

			if memory, exist := config.Spec.PodResourceRequirements.Requests[v1.ResourceMemory]; exist {
				config.Status.DefaultPodResourceRequirements.Requests[v1.ResourceMemory] = memory
			}
		}
	}

	return nil
}

func (r *CDIConfigReconciler) reconcileFilesystemOverhead(config *cdiv1.CDIConfig) error {
	var globalOverhead cdiv1.Percent = common.DefaultGlobalOverhead
	var perStorageConfig = make(map[string]cdiv1.Percent)

	log := r.log.WithName("CDIconfig").WithName("FilesystemOverhead")

	// Avoid nil maps and segfaults for the initial case, where filesystemOverhead
	// is nil for both the spec and the status.
	if config.Status.FilesystemOverhead == nil {
		log.Info("No filesystem overhead found in status, initializing to defaults")
		config.Status.FilesystemOverhead = &cdiv1.FilesystemOverhead{
			Global:       globalOverhead,
			StorageClass: make(map[string]cdiv1.Percent),
		}
	}

	if config.Spec.FilesystemOverhead != nil {
		if valid, _ := validOverhead(config.Spec.FilesystemOverhead.Global); valid {
			globalOverhead = config.Spec.FilesystemOverhead.Global
		}
		if config.Spec.FilesystemOverhead.StorageClass != nil {
			perStorageConfig = config.Spec.FilesystemOverhead.StorageClass
		}
	}

	// Set status global overhead
	config.Status.FilesystemOverhead.Global = globalOverhead

	// Set status per-storageClass overhead
	storageClassList := &storagev1.StorageClassList{}
	if err := r.client.List(context.TODO(), storageClassList, &client.ListOptions{}); err != nil {
		return err
	}
	config.Status.FilesystemOverhead.StorageClass = make(map[string]cdiv1.Percent)
	for _, storageClass := range storageClassList.Items {
		storageClassName := storageClass.GetName()
		storageClassNameOverhead, found := perStorageConfig[storageClassName]

		if found {
			valid, err := validOverhead(storageClassNameOverhead)
			if !valid {
				return err
			}
			config.Status.FilesystemOverhead.StorageClass[storageClassName] = storageClassNameOverhead
		} else {
			config.Status.FilesystemOverhead.StorageClass[storageClassName] = globalOverhead
		}
	}

	return nil
}

func validOverhead(overhead cdiv1.Percent) (bool, error) {
	return regexp.MatchString(`^(0(?:\.\d{1,3})?|1)$`, string(overhead))
}

// createCDIConfig creates a new instance of the CDIConfig object if it doesn't exist already, and returns the existing one if found.
// It also sets the operator to be the owner of the CDIConfig object.
func (r *CDIConfigReconciler) createCDIConfig() (*cdiv1.CDIConfig, error) {
	config := &cdiv1.CDIConfig{}
	if err := r.uncachedClient.Get(context.TODO(), types.NamespacedName{Name: r.configName}, config); err != nil {
		if k8serrors.IsNotFound(err) {
			config = cc.MakeEmptyCDIConfigSpec(r.configName)
			if err := operator.SetOwnerRuntime(r.uncachedClient, config); err != nil {
				return nil, err
			}
			util.SetRecommendedLabels(config, r.installerLabels, "cdi-controller")
			if err := r.client.Create(context.TODO(), config); err != nil {
				if k8serrors.IsAlreadyExists(err) {
					config := &cdiv1.CDIConfig{}
					if err := r.uncachedClient.Get(context.TODO(), types.NamespacedName{Name: r.configName}, config); err == nil {
						return config, nil
					}
					return nil, err
				}
				return nil, err
			}
		} else {
			return nil, err
		}
	}
	return config, nil
}

func (r *CDIConfigReconciler) reconcileImportProxy(config *cdiv1.CDIConfig) error {
	config.Status.ImportProxy = config.Spec.ImportProxy

	// Avoid nil pointers and segfaults for the initial case, where ImportProxy is nil for both the spec and the status.
	if config.Status.ImportProxy == nil {
		config.Status.ImportProxy = &cdiv1.ImportProxy{
			HTTPProxy:      new(string),
			HTTPSProxy:     new(string),
			NoProxy:        new(string),
			TrustedCAProxy: new(string),
		}

		// Try Openshift cluster wide proxy only if the CDIConfig default config is empty
		clusterWideProxy, err := getClusterWideProxy(r.client)
		if err != nil {
			return err
		}
		config.Status.ImportProxy.HTTPProxy = &clusterWideProxy.Status.HTTPProxy
		config.Status.ImportProxy.HTTPSProxy = &clusterWideProxy.Status.HTTPSProxy
		config.Status.ImportProxy.NoProxy = &clusterWideProxy.Status.NoProxy
		if err := r.reconcileImportProxyCAConfigMap(config, clusterWideProxy); err != nil {
			return err
		}
		config.Status.ImportProxy.TrustedCAProxy = &clusterWideProxy.Spec.TrustedCA.Name
	}
	return nil
}

// Create/Update a configmap with the CA certificates in the controllor context with the cluster-wide proxy CA certificates to be used by the importer pod
func (r *CDIConfigReconciler) reconcileImportProxyCAConfigMap(config *cdiv1.CDIConfig, clusterWideProxy *ocpconfigv1.Proxy) error {
	cmOldName := config.Status.ImportProxy.TrustedCAProxy
	cmName := clusterWideProxy.Spec.TrustedCA.Name
	client := r.uncachedClient

	// Delete old ConfigMap if name changed
	if cmOldName != nil && *cmOldName != "" && *cmOldName != cmName {
		if err := client.Delete(context.TODO(), r.createProxyConfigMap(*cmOldName, "")); err != nil && !k8serrors.IsNotFound(err) {
			return err
		}
	}
	if cmName == "" {
		return nil
	}

	clusterWideProxyConfigMap := &v1.ConfigMap{}
	if err := client.Get(context.TODO(), types.NamespacedName{Name: cmName, Namespace: ClusterWideProxyConfigMapNameSpace}, clusterWideProxyConfigMap); err != nil {
		if k8serrors.IsNotFound(err) {
			msg := fmt.Sprintf(messageResourceDoesntExist, cmName)
			r.recorder.Event(clusterWideProxy, v1.EventTypeWarning, errResourceDoesntExist, msg)
		}
		return err
	}
	// Copy the cluster-wide proxy CA certificates to the importer pod proxy CA certificates configMap
	certBytes, ok := clusterWideProxyConfigMap.Data[ClusterWideProxyConfigMapKey]
	if !ok {
		return fmt.Errorf("no cluster-wide proxy CA certificate")
	}
	configMap := &v1.ConfigMap{}
	if err := client.Get(context.TODO(), types.NamespacedName{Name: cmName, Namespace: r.cdiNamespace}, configMap); err != nil {
		if !k8serrors.IsNotFound(err) {
			return err
		}
		proxyConfigMap := r.createProxyConfigMap(cmName, certBytes)
		util.SetRecommendedLabels(proxyConfigMap, r.installerLabels, "cdi-controller")
		if err := client.Create(context.TODO(), proxyConfigMap); err != nil {
			return err
		}
		return nil
	}
	configMap.Data[common.ImportProxyConfigMapKey] = certBytes
	util.SetRecommendedLabels(configMap, r.installerLabels, "cdi-controller")
	if err := client.Update(context.TODO(), configMap); err != nil {
		return err
	}
	return nil
}

func (r *CDIConfigReconciler) createProxyConfigMap(cmName, cert string) *v1.ConfigMap {
	return &v1.ConfigMap{
		ObjectMeta: metav1.ObjectMeta{
			Name:      cmName,
			Namespace: r.cdiNamespace},
		Data: map[string]string{common.ImportProxyConfigMapKey: cert},
	}
}

// Init initializes a CDIConfig object.
func (r *CDIConfigReconciler) Init() error {
	_, err := r.createCDIConfig()
	return err
}

// NewConfigController creates a new instance of the config controller.
func NewConfigController(mgr manager.Manager, log logr.Logger, uploadProxyServiceName, configName string, installerLabels map[string]string) (controller.Controller, error) {
	uncachedClient, err := client.New(mgr.GetConfig(), client.Options{
		Scheme: mgr.GetScheme(),
		Mapper: mgr.GetRESTMapper(),
	})
	if err != nil {
		return nil, err
	}
	reconciler := &CDIConfigReconciler{
		client:                 mgr.GetClient(),
		uncachedClient:         uncachedClient,
		recorder:               mgr.GetEventRecorderFor("config-controller"),
		scheme:                 mgr.GetScheme(),
		log:                    log.WithName("config-controller"),
		uploadProxyServiceName: uploadProxyServiceName,
		configName:             configName,
		cdiNamespace:           util.GetNamespace(),
		installerLabels:        installerLabels,
	}

	configController, err := controller.New("config-controller", mgr, controller.Options{
		MaxConcurrentReconciles: 3,
		Reconciler:              reconciler,
	})
	if err != nil {
		return nil, err
	}
	if err := addConfigControllerWatches(mgr, configController, reconciler.cdiNamespace, configName, uploadProxyServiceName, log); err != nil {
		return nil, err
	}
	if err := reconciler.Init(); err != nil {
		log.Error(err, "Unable to initialize CDIConfig")
	}
	log.Info("Initialized CDI Config object")
	return configController, nil
}

// addConfigControllerWatches sets up the watches used by the config controller.
func addConfigControllerWatches(mgr manager.Manager, configController controller.Controller, cdiNamespace, configName, uploadProxyServiceName string, log logr.Logger) error {
	// Setup watches
	if err := watchCDIConfig(mgr, configController, configName); err != nil {
		return err
	}
	if err := watchStorageClass(mgr, configController, configName); err != nil {
		return err
	}
	if err := watchIngress(mgr, configController, cdiNamespace, configName, uploadProxyServiceName); err != nil {
		return err
	}
	if err := watchRoutes(mgr, configController, cdiNamespace, configName, uploadProxyServiceName); err != nil {
		return err
	}
	if err := watchClusterProxy(mgr, configController, configName); err != nil {
		return err
	}
	if err := watchUploadProxyCA(mgr, configController, configName); err != nil {
		return err
	}

	return nil
}

func watchCDIConfig(mgr manager.Manager, configController controller.Controller, configName string) error {
	if err := configController.Watch(source.Kind(mgr.GetCache(), &cdiv1.CDIConfig{}, &handler.TypedEnqueueRequestForObject[*cdiv1.CDIConfig]{})); err != nil {
		return err
	}
	return configController.Watch(source.Kind(mgr.GetCache(), &cdiv1.CDI{}, handler.TypedEnqueueRequestsFromMapFunc[*cdiv1.CDI](
		func(_ context.Context, _ *cdiv1.CDI) []reconcile.Request {
			return []reconcile.Request{{
				NamespacedName: types.NamespacedName{Name: configName},
			}}
		},
	)))
}

func watchStorageClass(mgr manager.Manager, configController controller.Controller, configName string) error {
	return configController.Watch(source.Kind(mgr.GetCache(), &storagev1.StorageClass{}, handler.TypedEnqueueRequestsFromMapFunc[*storagev1.StorageClass](
		func(_ context.Context, _ *storagev1.StorageClass) []reconcile.Request {
			return []reconcile.Request{{
				NamespacedName: types.NamespacedName{Name: configName},
			}}
		},
	)))
}

func watchIngress(mgr manager.Manager, configController controller.Controller, cdiNamespace, configName, uploadProxyServiceName string) error {
	err := configController.Watch(source.Kind(mgr.GetCache(), &networkingv1.Ingress{}, handler.TypedEnqueueRequestsFromMapFunc[*networkingv1.Ingress](
		func(_ context.Context, _ *networkingv1.Ingress) []reconcile.Request {
			return []reconcile.Request{{
				NamespacedName: types.NamespacedName{Name: configName},
			}}
		}),
		predicate.TypedFuncs[*networkingv1.Ingress]{
			CreateFunc: func(e event.TypedCreateEvent[*networkingv1.Ingress]) bool {
				return "" != getURLFromIngress(e.Object, uploadProxyServiceName) &&
					e.Object.GetNamespace() == cdiNamespace
			},
			UpdateFunc: func(e event.TypedUpdateEvent[*networkingv1.Ingress]) bool {
				return "" != getURLFromIngress(e.ObjectNew, uploadProxyServiceName) &&
					e.ObjectNew.GetNamespace() == cdiNamespace
			},
			DeleteFunc: func(e event.TypedDeleteEvent[*networkingv1.Ingress]) bool {
				return "" != getURLFromIngress(e.Object, uploadProxyServiceName) &&
					e.Object.GetNamespace() == cdiNamespace
			},
		}))
	return err
}

// we only watch the route obj if they exist, i.e., if it is an OpenShift cluster
func watchRoutes(mgr manager.Manager, configController controller.Controller, cdiNamespace, configName, uploadProxyServiceName string) error {
	err := mgr.GetClient().List(context.TODO(), &routev1.RouteList{}, &client.ListOptions{Namespace: cdiNamespace})
	if !meta.IsNoMatchError(err) {
		if err == nil || cc.IsErrCacheNotStarted(err) {
			err := configController.Watch(source.Kind(mgr.GetCache(), &routev1.Route{}, handler.TypedEnqueueRequestsFromMapFunc[*routev1.Route](
				func(_ context.Context, _ *routev1.Route) []reconcile.Request {
					return []reconcile.Request{{
						NamespacedName: types.NamespacedName{Name: configName},
					}}
				}),
				predicate.TypedFuncs[*routev1.Route]{
					CreateFunc: func(e event.TypedCreateEvent[*routev1.Route]) bool {
						return "" != getURLFromRoute(e.Object, uploadProxyServiceName) &&
							e.Object.GetNamespace() == cdiNamespace
					},
					UpdateFunc: func(e event.TypedUpdateEvent[*routev1.Route]) bool {
						return "" != getURLFromRoute(e.ObjectNew, uploadProxyServiceName) &&
							e.ObjectNew.GetNamespace() == cdiNamespace
					},
					DeleteFunc: func(e event.TypedDeleteEvent[*routev1.Route]) bool {
						return "" != getURLFromRoute(e.Object, uploadProxyServiceName) &&
							e.Object.GetNamespace() == cdiNamespace
					},
				}))
			return err
		}
		return err
	}
	return nil
}

// we only watch the cluster-wide proxy obj if they exist, i.e., if it is an OpenShift cluster
func watchClusterProxy(mgr manager.Manager, configController controller.Controller, configName string) error {
	err := mgr.GetClient().List(context.TODO(), &ocpconfigv1.ProxyList{})
	if !meta.IsNoMatchError(err) {
		if err == nil || cc.IsErrCacheNotStarted(err) {
			return configController.Watch(source.Kind(mgr.GetCache(), &ocpconfigv1.Proxy{}, handler.TypedEnqueueRequestsFromMapFunc[*ocpconfigv1.Proxy](
				func(_ context.Context, _ *ocpconfigv1.Proxy) []reconcile.Request {
					return []reconcile.Request{{
						NamespacedName: types.NamespacedName{Name: configName},
					}}
				},
			)))
		}
		return err
	}
	return nil
}

// watchUploadProxyCA watches the kube-root-ca.crt ConfigMap for changes
// to the CA certificate used by the upload proxy.
//
// A change in the UploadProxyURL may invalidate the CA certificate, but
// watchCDIConfig will handle that.
func watchUploadProxyCA(mgr manager.Manager, configcontroller controller.Controller, configName string) error {
	handler := handler.TypedEnqueueRequestsFromMapFunc[*v1.ConfigMap](func(context.Context, *v1.ConfigMap) []reconcile.Request {
		return []reconcile.Request{{NamespacedName: types.NamespacedName{Name: configName}}}
	})

	predicate := predicate.NewTypedPredicateFuncs[*v1.ConfigMap](func(o *v1.ConfigMap) bool {
		return o.Name == rootCertificateConfigMap
	})

	if err := configcontroller.Watch(source.Kind(mgr.GetCache(), &v1.ConfigMap{}, handler, predicate)); err != nil {
		return fmt.Errorf("could not watch UploadProxyCA ConfigMap: %w", err)
	}
	return nil
}

func getURLFromIngress(ing *networkingv1.Ingress, uploadProxyServiceName string) string {
	if ing.Spec.DefaultBackend != nil && ing.Spec.DefaultBackend.Service != nil {
		if ing.Spec.DefaultBackend.Service.Name != uploadProxyServiceName {
			return ""
		}
		return ing.Spec.Rules[0].Host
	}
	for _, rule := range ing.Spec.Rules {
		if rule.HTTP == nil {
			continue
		}
		for _, path := range rule.HTTP.Paths {
			if path.Backend.Service != nil && path.Backend.Service.Name == uploadProxyServiceName {
				if rule.Host != "" {
					return rule.Host
				}
			}
		}
	}
	return ""
}

func getURLFromRoute(route *routev1.Route, uploadProxyServiceName string) string {
	if route.Spec.To.Name == uploadProxyServiceName {
		if len(route.Status.Ingress) > 0 {
			return route.Status.Ingress[0].Host
		}
	}
	return ""
}

// getClusterWideProxy returns the OpenShift cluster wide proxy object
func getClusterWideProxy(r client.Client) (*ocpconfigv1.Proxy, error) {
	clusterWideProxy := &ocpconfigv1.Proxy{}
	// Ignore both no CRD found (IgnoreIsNoMatch) and the object itself not existing IsNotFound because we want to skip if not
	// in Open Shift.
	if err := r.Get(context.TODO(), types.NamespacedName{Name: ClusterWideProxyName}, clusterWideProxy); cc.IgnoreIsNoMatchError(err) != nil && !k8serrors.IsNotFound(err) {
		return nil, err
	}
	return clusterWideProxy, nil
}

// GetImportProxyConfig attempts to import proxy URLs if configured in the CDIConfig.
func GetImportProxyConfig(config *cdiv1.CDIConfig, field string) (string, error) {
	if config == nil {
		return "", errors.New("failed to get field, the CDIConfig is nil")
	}
	if config.Status.ImportProxy == nil {
		return "", errors.New("failed to get field, the CDIConfig ImportProxy is nil")
	}

	switch field {
	case common.ImportProxyHTTP:
		if config.Status.ImportProxy.HTTPProxy != nil {
			return *config.Status.ImportProxy.HTTPProxy, nil
		}
	case common.ImportProxyHTTPS:
		if config.Status.ImportProxy.HTTPSProxy != nil {
			return *config.Status.ImportProxy.HTTPSProxy, nil
		}
	case common.ImportProxyNoProxy:
		if config.Status.ImportProxy.NoProxy != nil {
			return *config.Status.ImportProxy.NoProxy, nil
		}
	case common.ImportProxyConfigMapName:
		if config.Status.ImportProxy.TrustedCAProxy != nil {
			return *config.Status.ImportProxy.TrustedCAProxy, nil
		}
	default:
		return "", errors.Errorf("CDIConfig ImportProxy does not have the field: %s", field)
	}

	// If everything fails, return blank
	return "", nil
}
