package main

import (
	"context"
	"flag"
	"fmt"
	"os"
	"path"
	"time"

	features "github.com/openshift/api/features"
	mcfginformersv1 "github.com/openshift/client-go/machineconfiguration/informers/externalversions/machineconfiguration/v1"
	"github.com/openshift/machine-config-operator/cmd/common"
	"github.com/openshift/machine-config-operator/internal/clients"
	bootimagecontroller "github.com/openshift/machine-config-operator/pkg/controller/bootimage"
	certrotationcontroller "github.com/openshift/machine-config-operator/pkg/controller/certrotation"
	ctrlcommon "github.com/openshift/machine-config-operator/pkg/controller/common"
	containerruntimeconfig "github.com/openshift/machine-config-operator/pkg/controller/container-runtime-config"
	"github.com/openshift/machine-config-operator/pkg/controller/drain"
	"github.com/openshift/machine-config-operator/pkg/controller/internalreleaseimage"
	kubeletconfig "github.com/openshift/machine-config-operator/pkg/controller/kubelet-config"
	"github.com/openshift/machine-config-operator/pkg/controller/node"
	osistreamctrl "github.com/openshift/machine-config-operator/pkg/controller/osimagestream"
	"github.com/openshift/machine-config-operator/pkg/controller/pinnedimageset"
	"github.com/openshift/machine-config-operator/pkg/controller/render"
	"github.com/openshift/machine-config-operator/pkg/controller/template"
	"github.com/openshift/machine-config-operator/pkg/imageutils"
	"github.com/openshift/machine-config-operator/pkg/osimagestream"
	"github.com/openshift/machine-config-operator/pkg/version"
	"github.com/spf13/cobra"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	coreinformersv1 "k8s.io/client-go/informers/core/v1"
	"k8s.io/client-go/tools/leaderelection"
	"k8s.io/klog/v2"
)

var (
	startCmd = &cobra.Command{
		Use:   "start",
		Short: "Starts Machine Config Controller",
		Long:  "",
		Run:   runStartCmd,
	}

	startOpts struct {
		kubeconfig               string
		templates                string
		promMetricsListenAddress string
		resourceLockNamespace    string
		tlsCipherSuites          []string
		tlsMinVersion            string
		streamsCache             string
	}
)

func init() {
	rootCmd.AddCommand(startCmd)
	startCmd.PersistentFlags().StringVar(&startOpts.kubeconfig, "kubeconfig", "", "Kubeconfig file to access a remote cluster (testing only)")
	startCmd.PersistentFlags().StringVar(&startOpts.resourceLockNamespace, "resourcelock-namespace", metav1.NamespaceSystem, "Path to the template files used for creating MachineConfig objects")
	startCmd.PersistentFlags().StringVar(&startOpts.promMetricsListenAddress, "metrics-listen-address", "127.0.0.1:8797", "Listen address for prometheus metrics listener")
	startCmd.PersistentFlags().StringSliceVar(&startOpts.tlsCipherSuites, "tls-cipher-suites", nil, "Comma-separated list of cipher suites for the metrics server")
	startCmd.PersistentFlags().StringVar(&startOpts.tlsMinVersion, "tls-min-version", "VersionTLS12", "Minimum TLS version supported for the metrics server")
	startCmd.PersistentFlags().StringVar(&startOpts.streamsCache, "streams-cache", "/var/cache/mcc", "Directory to use as cache for streams discovery")
}

func runStartCmd(_ *cobra.Command, _ []string) {
	flag.Set("logtostderr", "true")
	flag.Parse()

	// This is 'main' context that we thread through the controller context and
	// the leader elections. Cancelling this is "stop everything, we are shutting down".
	runContext, runCancel := context.WithCancel(context.Background())

	// To help debugging, immediately log version
	klog.Infof("Version: %+v (%s)", version.Raw, version.Hash)

	cb, err := clients.NewBuilder(startOpts.kubeconfig)
	if err != nil {
		ctrlcommon.WriteTerminationError(fmt.Errorf("creating clients: %w", err))
	}

	run := func(ctx context.Context) {
		go common.SignalHandler(runCancel)

		// Start the metrics handler

		ctrlctx := ctrlcommon.CreateControllerContext(ctx, cb)

		// Early start the config informer because feature gate depends on it
		ctrlctx.ConfigInformerFactory.Start(ctx.Done())
		if fgErr := ctrlctx.FeatureGatesHandler.Connect(ctx); fgErr != nil {
			klog.Error(fmt.Errorf("failed to connect to feature gates %w", fgErr))
			runCancel()
			<-ctx.Done()
			return
		}

		var inspectorFactory osimagestream.ImagesInspectorFactory
		var inspectionCache *imageutils.FileInspectionCache
		if startOpts.streamsCache != "" {
			inspectionCache = imageutils.NewFileInspectionCache(path.Join(startOpts.streamsCache, "image-inspection.json"), 48*time.Hour)
		}

		// OSImageStream must be the first controller to run: the blocking
		// EnsureOSImageStream call guarantees the CR exists before any other
		// controller starts, since render, node, and template depend on it
		// for OS image URLs.
		var cacheWarmer *pinnedimageset.CacheWarmer
		if osimagestream.IsFeatureEnabled(ctrlctx.FeatureGatesHandler) {
			if inspectionCache != nil {
				inspectorFactory = osimagestream.NewCachedImagesInspectorFactory(
					&osimagestream.DefaultImagesInspectorFactory{},
					inspectionCache,
				)
			} else {
				inspectorFactory = &osimagestream.DefaultImagesInspectorFactory{}
			}

			osImageStreamCtrl := osistreamctrl.New(
				ctrlctx.InformerFactory.Machineconfiguration().V1().OSImageStreams(),
				ctrlctx.InformerFactory.Machineconfiguration().V1().ControllerConfigs(),
				ctrlctx.ConfigInformerFactory.Config().V1().ClusterVersions(),
				ctrlctx.OpenShiftConfigKubeNamespacedInformerFactory.Core().V1().Secrets(),
				ctrlctx.KubeNamespacedInformerFactory.Core().V1().ConfigMaps(),
				ctrlctx.OperatorInformerFactory.Operator().V1alpha1().ImageContentSourcePolicies(),
				ctrlctx.ConfigInformerFactory.Config().V1().ImageDigestMirrorSets(),
				ctrlctx.ConfigInformerFactory.Config().V1().ImageTagMirrorSets(),
				ctrlctx.ConfigInformerFactory.Config().V1().Images(),
				ctrlctx.ClientBuilder.KubeClientOrDie("osimagestream-controller"),
				ctrlctx.ClientBuilder.MachineConfigClientOrDie("osimagestream-controller"),
				ctrlctx.FeatureGatesHandler,
				inspectorFactory,
			)

			ctrlctx.InformerFactory.Start(ctx.Done())
			ctrlctx.ConfigInformerFactory.Start(ctx.Done())
			ctrlctx.OpenShiftConfigKubeNamespacedInformerFactory.Start(ctx.Done())
			ctrlctx.KubeNamespacedInformerFactory.Start(ctx.Done())
			ctrlctx.OperatorInformerFactory.Start(ctx.Done())

			go osImageStreamCtrl.Run(ctx, 1)

			if err := osImageStreamCtrl.EnsureOSImageStream(ctx); err != nil {
				klog.Fatalf("Failed to ensure OSImageStream: %v", err)
			}

			if inspectionCache != nil {
				inspectionCache.RegisterEvicter(osImageStreamCtrl)

				cacheWarmer = pinnedimageset.NewCacheWarmer(
					ctrlctx.InformerFactory.Machineconfiguration().V1().PinnedImageSets().Lister(),
					inspectorFactory,
					ctrlcommon.NewSysContextFactory(
						ctrlctx.InformerFactory.Machineconfiguration().V1().ControllerConfigs().Lister(),
						ctrlctx.OpenShiftConfigKubeNamespacedInformerFactory.Core().V1().Secrets().Lister(),
						ctrlctx.ConfigInformerFactory.Config().V1().Images().Lister(),
						ctrlctx.OperatorInformerFactory.Operator().V1alpha1().ImageContentSourcePolicies().Lister(),
						ctrlctx.ConfigInformerFactory.Config().V1().ImageDigestMirrorSets().Lister(),
						ctrlctx.ConfigInformerFactory.Config().V1().ImageTagMirrorSets().Lister(),
					),
				)
				inspectionCache.RegisterEvicter(cacheWarmer)
			}
		}

		go ctrlcommon.StartMetricsListener(startOpts.promMetricsListenAddress, ctx.Done(), ctrlcommon.RegisterMCCMetrics, startOpts.tlsMinVersion, startOpts.tlsCipherSuites)

		controllers := createControllers(ctrlctx, inspectionCache, inspectorFactory)
		draincontroller := drain.New(
			drain.DefaultConfig(),
			ctrlctx.KubeInformerFactory.Core().V1().Nodes(),
			ctrlctx.InformerFactory.Machineconfiguration().V1().MachineConfigPools(),
			ctrlctx.ClientBuilder.KubeClientOrDie("node-update-controller"),
			ctrlctx.ClientBuilder.MachineConfigClientOrDie("node-update-controller"),
			ctrlctx.FeatureGatesHandler,
		)

		certrotationcontroller, err := certrotationcontroller.New(
			ctrlctx.ClientBuilder.KubeClientOrDie("cert-rotation-controller"),
			ctrlctx.ClientBuilder.ConfigClientOrDie("cert-rotation-controller"),
			ctrlctx.ClientBuilder.MachineClientOrDie("cert-rotation-controller"),
			ctrlctx.ClientBuilder.AROClientOrDie("cert-rotation-controller"),
			ctrlctx.KubeMAOSharedInformer.Core().V1().Secrets(),
			ctrlctx.KubeNamespacedInformerFactory.Core().V1().Secrets(),
			ctrlctx.KubeNamespacedInformerFactory.Core().V1().ConfigMaps(),
			ctrlctx.ConfigInformerFactory.Config().V1().Infrastructures(),
			ctrlctx.ConfigInformerFactory,
			ctrlctx.FeatureGatesHandler,
			ctrlctx.ClientBuilder.MachineConfigClientOrDie("cert-rotation-controller"),
		)
		if err != nil {
			klog.Fatalf("unable to start cert rotation controller: %v", err)
		}

		pinnedImageSet := pinnedimageset.New(
			ctrlctx.InformerFactory.Machineconfiguration().V1().PinnedImageSets(),
			ctrlctx.InformerFactory.Machineconfiguration().V1().MachineConfigPools(),
			ctrlctx.ClientBuilder.KubeClientOrDie("pinned-image-set-controller"),
			ctrlctx.ClientBuilder.MachineConfigClientOrDie("pinned-image-set-controller"),
			cacheWarmer,
		)
		go pinnedImageSet.Run(ctx, 2)

		// Start the shared factory informers that you need to use in your controller
		ctrlctx.InformerFactory.Start(ctrlctx.Stop)
		ctrlctx.KubeInformerFactory.Start(ctrlctx.Stop)
		ctrlctx.OpenShiftConfigKubeNamespacedInformerFactory.Start(ctrlctx.Stop)
		ctrlctx.OperatorInformerFactory.Start(ctrlctx.Stop)
		ctrlctx.ConfigInformerFactory.Start(ctrlctx.Stop)
		ctrlctx.KubeNamespacedInformerFactory.Start(ctrlctx.Stop)
		ctrlctx.KubeMAOSharedInformer.Start(ctrlctx.Stop)
		ctrlctx.OCLInformerFactory.Start(ctrlctx.Stop)
		ctrlctx.MCOPodInformerFactory.Start(ctrlctx.Stop)

		close(ctrlctx.InformersStarted)

		if ctrlctx.FeatureGatesHandler.Enabled(features.FeatureGateNoRegistryClusterInstall) {
			iriController := internalreleaseimage.New(
				ctrlctx.InformerFactory.Machineconfiguration().V1().InternalReleaseImages(),
				ctrlctx.InformerFactory.Machineconfiguration().V1().ControllerConfigs(),
				ctrlctx.InformerFactory.Machineconfiguration().V1().MachineConfigs(),
				ctrlctx.ConfigInformerFactory.Config().V1().ClusterVersions(),
				ctrlctx.KubeInformerFactory.Core().V1().Secrets(),
				ctrlctx.InformerFactory.Machineconfiguration().V1().MachineConfigNodes(),
				ctrlctx.KubeInformerFactory.Core().V1().Nodes(),
				ctrlctx.ConfigInformerFactory.Config().V1().Infrastructures(),
				ctrlctx.ClientBuilder.KubeClientOrDie("internalreleaseimage-controller"),
				ctrlctx.ClientBuilder.MachineConfigClientOrDie("internalreleaseimage-controller"))

			go iriController.Run(ctx, 2)
			// start the informers again to enable feature gated types.
			// see comments in SharedInformerFactory interface.
			ctrlctx.InformerFactory.Start(ctx.Done())
			ctrlctx.KubeInformerFactory.Start(ctx.Done())
		}

		if ctrlcommon.IsBootImageControllerRequired(ctrlctx) {
			bootImageController := bootimagecontroller.New(
				ctrlctx.ClientBuilder.KubeClientOrDie("machine-set-boot-image-controller"),
				ctrlctx.ClientBuilder.MachineClientOrDie("machine-set-boot-image-controller"),
				ctrlctx.KubeNamespacedInformerFactory.Core().V1().ConfigMaps(),
				ctrlctx.MachineInformerFactory.Machine().V1beta1().MachineSets(),
				ctrlctx.MachineInformerFactory.Machine().V1().ControlPlaneMachineSets(),
				ctrlctx.ConfigInformerFactory.Config().V1().Infrastructures(),
				ctrlctx.ClientBuilder.OperatorClientOrDie(componentName),
				ctrlctx.OperatorInformerFactory.Operator().V1().MachineConfigurations(),
				ctrlctx.ConfigInformerFactory.Config().V1().ClusterVersions(),
				ctrlctx.FeatureGatesHandler,
			)
			go bootImageController.Run(ctx)
			// start the informers again to enable feature gated types.
			// see comments in SharedInformerFactory interface.
			ctrlctx.KubeNamespacedInformerFactory.Start(ctx.Done())
			ctrlctx.MachineInformerFactory.Start(ctx.Done())
			ctrlctx.ConfigInformerFactory.Start(ctx.Done())
			ctrlctx.OperatorInformerFactory.Start(ctx.Done())
		}

		for _, c := range controllers {
			go c.Run(ctx, 2)
		}
		go draincontroller.Run(ctx, 5)
		go certrotationcontroller.Run(ctx, 1)

		if inspectionCache != nil {
			inspectionCache.StartEviction(ctx, 24*time.Hour, 10*time.Minute)
		}

		// wait here in this function until the context gets cancelled (which tells us when we are being shut down)
		<-ctx.Done()
	}

	leaderElectionCfg := common.GetDefaultLeaderElectionConfig()

	leaderelection.RunOrDie(runContext, leaderelection.LeaderElectionConfig{
		Lock:            common.CreateResourceLock(cb, startOpts.resourceLockNamespace, componentName),
		ReleaseOnCancel: true,
		LeaseDuration:   leaderElectionCfg.LeaseDuration.Duration,
		RenewDeadline:   leaderElectionCfg.RenewDeadline.Duration,
		RetryPeriod:     leaderElectionCfg.RetryPeriod.Duration,
		Callbacks: leaderelection.LeaderCallbacks{
			OnStartedLeading: run,
			OnStoppedLeading: func() {
				klog.Infof("Stopped leading. Terminating.")
				os.Exit(0)
			},
		},
	})
	panic("unreachable")
}

func createControllers(ctx *ctrlcommon.ControllerContext, inspectionCache *imageutils.FileInspectionCache, inspectorFactory osimagestream.ImagesInspectorFactory) []ctrlcommon.Controller {
	// Only watch IRI informers when the feature gate is enabled. The
	// InternalReleaseImages CRD is not installed on clusters where the gate is
	// off, so the informer list call would fail and WaitForCacheSync in the
	// template controller would block forever.
	var iriSecretsInformer coreinformersv1.SecretInformer
	var iriInformer mcfginformersv1.InternalReleaseImageInformer
	if ctx.FeatureGatesHandler.Enabled(features.FeatureGateNoRegistryClusterInstall) {
		iriSecretsInformer = ctx.KubeInformerFactory.Core().V1().Secrets()
		iriInformer = ctx.InformerFactory.Machineconfiguration().V1().InternalReleaseImages()
	}

	renderCtrl := render.New(
		ctx.InformerFactory.Machineconfiguration().V1().MachineConfigPools(),
		ctx.InformerFactory.Machineconfiguration().V1().MachineConfigs(),
		ctx.InformerFactory.Machineconfiguration().V1().ControllerConfigs(),
		ctx.InformerFactory.Machineconfiguration().V1().ContainerRuntimeConfigs(),
		ctx.InformerFactory.Machineconfiguration().V1().KubeletConfigs(),
		ctx.OperatorInformerFactory.Operator().V1().MachineConfigurations(),
		ctx.InformerFactory.Machineconfiguration().V1().OSImageStreams(),
		ctx.OpenShiftConfigKubeNamespacedInformerFactory.Core().V1().Secrets(),
		ctx.OperatorInformerFactory.Operator().V1alpha1().ImageContentSourcePolicies(),
		ctx.ConfigInformerFactory.Config().V1().ImageDigestMirrorSets(),
		ctx.ConfigInformerFactory.Config().V1().ImageTagMirrorSets(),
		ctx.ConfigInformerFactory.Config().V1().Images(),
		ctx.ClientBuilder.KubeClientOrDie("render-controller"),
		ctx.ClientBuilder.MachineConfigClientOrDie("render-controller"),
		ctx.FeatureGatesHandler,
		inspectorFactory,
	)
	if inspectionCache != nil {
		inspectionCache.RegisterEvicter(renderCtrl)
	}

	var controllers []ctrlcommon.Controller
	controllers = append(controllers,
		renderCtrl,
		// Our primary MCs come from here
		template.New(
			rootOpts.templates,
			ctx.InformerFactory.Machineconfiguration().V1().ControllerConfigs(),
			ctx.OpenShiftConfigKubeNamespacedInformerFactory.Core().V1().Secrets(),
			iriSecretsInformer,
			iriInformer,
			ctx.ConfigInformerFactory.Config().V1().APIServers(),
			ctx.ClientBuilder.KubeClientOrDie("template-controller"),
			ctx.ClientBuilder.MachineConfigClientOrDie("template-controller"),
			ctx.FeatureGatesHandler,
		),
		// Add all "sub-renderers here"
		kubeletconfig.New(
			rootOpts.templates,
			ctx.InformerFactory.Machineconfiguration().V1().MachineConfigPools(),
			ctx.InformerFactory.Machineconfiguration().V1().ControllerConfigs(),
			ctx.InformerFactory.Machineconfiguration().V1().KubeletConfigs(),
			ctx.ConfigInformerFactory.Config().V1().FeatureGates(),
			ctx.ConfigInformerFactory.Config().V1().Nodes(),
			ctx.ConfigInformerFactory.Config().V1().APIServers(),
			ctx.ClientBuilder.KubeClientOrDie("kubelet-config-controller"),
			ctx.ClientBuilder.MachineConfigClientOrDie("kubelet-config-controller"),
			ctx.ClientBuilder.ConfigClientOrDie("kubelet-config-controller"),
			ctx.FeatureGatesHandler,
		),
		containerruntimeconfig.New(
			rootOpts.templates,
			ctx.InformerFactory.Machineconfiguration().V1().MachineConfigPools(),
			ctx.InformerFactory.Machineconfiguration().V1().ControllerConfigs(),
			ctx.InformerFactory.Machineconfiguration().V1().ContainerRuntimeConfigs(),
			ctx.ConfigInformerFactory.Config().V1().Images(),
			ctx.ConfigInformerFactory.Config().V1().ImageDigestMirrorSets(),
			ctx.ConfigInformerFactory.Config().V1().ImageTagMirrorSets(),
			ctx.ConfigInformerFactory,
			ctx.OperatorInformerFactory.Operator().V1alpha1().ImageContentSourcePolicies(),
			ctx.ConfigInformerFactory.Config().V1().ClusterVersions(),
			ctx.ClientBuilder.KubeClientOrDie("container-runtime-config-controller"),
			ctx.ClientBuilder.MachineConfigClientOrDie("container-runtime-config-controller"),
			ctx.ClientBuilder.ConfigClientOrDie("container-runtime-config-controller"),
			ctx.FeatureGatesHandler,
		),
		// The node controller consumes data written by the render controller
		node.New(
			ctx.InformerFactory.Machineconfiguration().V1().ControllerConfigs(),
			ctx.InformerFactory.Machineconfiguration().V1().MachineConfigs(),
			ctx.InformerFactory.Machineconfiguration().V1().MachineConfigPools(),
			ctx.KubeInformerFactory.Core().V1().Nodes(),
			ctx.MCOPodInformerFactory.Core().V1().Pods(),
			ctx.OCLInformerFactory.Machineconfiguration().V1().MachineOSConfigs(),
			ctx.OCLInformerFactory.Machineconfiguration().V1().MachineOSBuilds(),
			ctx.InformerFactory.Machineconfiguration().V1().MachineConfigNodes(),
			ctx.ConfigInformerFactory.Config().V1().Schedulers(),
			ctx.OperatorInformerFactory.Operator().V1().MachineConfigurations(),
			ctx.InformerFactory.Machineconfiguration().V1().OSImageStreams(),
			ctx.ConfigInformerFactory.Config().V1().Infrastructures(),
			ctx.ClientBuilder.KubeClientOrDie("node-update-controller"),
			ctx.ClientBuilder.MachineConfigClientOrDie("node-update-controller"),
			ctx.FeatureGatesHandler,
		),
	)

	return controllers
}
