/*
SPDX-FileCopyrightText: Red Hat

SPDX-License-Identifier: Apache-2.0
*/

package operator

import (
	"context"
	"crypto/tls"
	"flag"
	"log/slog"
	"strings"

	openshiftv1 "github.com/openshift/api/config/v1"
	openshiftoperatorv1 "github.com/openshift/api/operator/v1"
	tlspkg "github.com/openshift/controller-runtime-common/pkg/tls"
	"sigs.k8s.io/controller-runtime/pkg/metrics/filters"

	"github.com/openshift-kni/oran-o2ims/internal/constants"
	ctlrutils "github.com/openshift-kni/oran-o2ims/internal/controllers/utils"

	// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
	// to ensure that exec-entrypoint and run can make use of them.
	_ "k8s.io/client-go/plugin/pkg/client/auth"
	"k8s.io/klog/v2"

	networkingv1 "k8s.io/api/networking/v1"
	"k8s.io/apimachinery/pkg/runtime"
	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
	clientgoscheme "k8s.io/client-go/kubernetes/scheme"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/cache"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/healthz"
	metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
	"sigs.k8s.io/controller-runtime/pkg/webhook"

	"github.com/go-logr/logr"

	metal3v1alpha1 "github.com/metal3-io/baremetal-operator/apis/metal3.io/v1alpha1"
	ibguv1alpha1 "github.com/openshift-kni/cluster-group-upgrades-operator/pkg/api/imagebasedgroupupgrades/v1alpha1"
	hwmgmtv1alpha1 "github.com/openshift-kni/oran-o2ims/api/hardwaremanagement/v1alpha1"
	inventoryv1alpha1 "github.com/openshift-kni/oran-o2ims/api/inventory/v1alpha1"
	provisioningv1alpha1 "github.com/openshift-kni/oran-o2ims/api/provisioning/v1alpha1"
	svcutils "github.com/openshift-kni/oran-o2ims/internal/service/common/utils"
	assistedservicev1beta1 "github.com/openshift/assisted-service/api/v1beta1"
	hivev1 "github.com/openshift/hive/apis/hive/v1"
	"github.com/spf13/cobra"
	observabilityv1beta1 "github.com/stolostron/multicluster-observability-operator/operators/multiclusterobservability/api/v1beta1"
	siteconfig "github.com/stolostron/siteconfig/api/v1alpha1"
	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
	addonv1alpha1 "open-cluster-management.io/api/addon/v1alpha1"
	clusterv1 "open-cluster-management.io/api/cluster/v1"
	workv1 "open-cluster-management.io/api/work/v1"
	policiesv1 "open-cluster-management.io/governance-policy-propagator/api/v1"
	msav1beta1 "open-cluster-management.io/managed-serviceaccount/apis/authentication/v1beta1"

	"github.com/openshift-kni/oran-o2ims/internal"
	"github.com/openshift-kni/oran-o2ims/internal/controllers"
	"github.com/openshift-kni/oran-o2ims/internal/exit"
)

// ControllerManager creates and returns the `start controller-manager` command.
func ControllerManager() *cobra.Command {
	c := NewControllerManager()
	result := &cobra.Command{
		Use:   "controller-manager",
		Short: "Starts the controller manager",
		Args:  cobra.NoArgs,
		RunE:  c.run,
	}
	flags := result.Flags()
	flags.StringVar(
		&c.metricsAddr,
		"metrics-bind-address",
		constants.MetricsPort,
		"The address the metric endpoint binds to.",
	)
	flags.StringVar(
		&c.metricsCertDir,
		"metrics-tls-cert-dir",
		"",
		"The directory containing the tls.crt and tls.key.",
	)
	flags.StringVar(
		&c.probeAddr,
		"health-probe-bind-address",
		constants.HealthProbePort,
		"The address the probe endpoint binds to.",
	)
	flag.BoolVar(
		&c.enableHTTP2,
		"enable-http2", false,
		"If set, HTTP/2 will be enabled for the metrics and webhook servers",
	)
	flags.BoolVar(
		&c.enableLeaderElection,
		"leader-elect",
		false,
		"Enable leader election for controller manager. "+
			"Enabling this will ensure there is only one active controller manager.",
	)
	flags.BoolVar(&c.enableWebhooks,
		"enable-webhooks",
		true,
		"Enable the o2ims validating webhooks")
	flags.StringVar(
		&c.image,
		imageFlagName,
		// Intentionally setting the default value to "" if the environment variable is not set to ensure we never
		// run an image that we didn't intend on running.
		ctlrutils.GetEnvOrDefault(constants.ServerImageName, ""),
		"Reference of the container image containing the servers.",
	)
	return result
}

// ControllerManagerCommand contains the data and logic needed to run the `start controller-manager`
// command.
type ControllerManagerCommand struct {
	metricsAddr          string
	metricsCertDir       string
	enableHTTP2          bool
	enableLeaderElection bool
	enableWebhooks       bool
	probeAddr            string
	image                string
	svcutils.CommonServerConfig
}

// NewControllerManager creates a new runner that knows how to execute the `start
// controller-manager` command.
func NewControllerManager() *ControllerManagerCommand {
	return &ControllerManagerCommand{}
}

var (
	scheme = runtime.NewScheme()
)

func init() {
	utilruntime.Must(clientgoscheme.AddToScheme(scheme))
	utilruntime.Must(provisioningv1alpha1.AddToScheme(scheme))
	utilruntime.Must(inventoryv1alpha1.AddToScheme(scheme))
	utilruntime.Must(siteconfig.AddToScheme(scheme))
	utilruntime.Must(hwmgmtv1alpha1.AddToScheme(scheme))
	utilruntime.Must(hwmgmtv1alpha1.AddToScheme(scheme))
	utilruntime.Must(clusterv1.AddToScheme(scheme))
	utilruntime.Must(policiesv1.AddToScheme(scheme))
	utilruntime.Must(openshiftv1.AddToScheme(scheme))
	utilruntime.Must(openshiftoperatorv1.AddToScheme(scheme))
	utilruntime.Must(ibguv1alpha1.AddToScheme(scheme))
	utilruntime.Must(assistedservicev1beta1.AddToScheme(scheme))
	utilruntime.Must(apiextensionsv1.AddToScheme(scheme))
	utilruntime.Must(metal3v1alpha1.AddToScheme(scheme))
	utilruntime.Must(observabilityv1beta1.AddToScheme(scheme))
	utilruntime.Must(hivev1.AddToScheme(scheme))
	utilruntime.Must(msav1beta1.AddToScheme(scheme))
	utilruntime.Must(workv1.Install(scheme))
	utilruntime.Must(addonv1alpha1.Install(scheme))
}

// run executes the `start controller-manager` command.
func (c *ControllerManagerCommand) run(cmd *cobra.Command, argv []string) error {
	// Get the context:
	ctx := cmd.Context()

	// Get the dependencies from the context:
	logger := internal.LoggerFromContext(ctx)
	slog.SetDefault(logger)

	// Configure klog to use our structured logger for vendor modules:
	klog.SetSlogLogger(logger)

	// Configure the controller runtime library to use our logger:
	adapter := logr.FromSlogHandler(logger.Handler())
	ctrl.SetLogger(adapter)
	klog.SetLogger(adapter)

	// Check the flags:
	if c.image == "" {
		logger.ErrorContext(
			ctx,
			"Image flag is mandatory",
			slog.String("flag", imageFlagName),
		)
		return exit.Error(1)
	}

	// Set up common server configuration early so it's available for all servers
	if err := svcutils.SetCommonServerFlags(cmd, &c.CommonServerConfig); err != nil {
		return err //nolint:wrapcheck
	}

	// Create a signal-aware, cancellable context. ctrl.SetupSignalHandler() ensures
	// SIGINT/SIGTERM cancel the context; WithCancel allows the TLS watcher to
	// programmatically trigger graceful shutdown on profile changes.
	ctx, cancel := context.WithCancel(ctrl.SetupSignalHandler())
	defer cancel()

	// Fetch the cluster TLS security profile to configure MinVersion/CipherSuites.
	// This uses a direct client since the manager cache isn't started yet.
	directClient, err := client.New(ctrl.GetConfigOrDie(), client.Options{Scheme: scheme})
	if err != nil {
		logger.ErrorContext(ctx, "Unable to create direct client for TLS profile fetch",
			slog.Any("error", err))
		return exit.Error(1)
	}
	tlsProfile, err := ctlrutils.FetchAPIServerTLSProfile(ctx, directClient)
	if err != nil {
		logger.ErrorContext(ctx, "Unable to fetch cluster TLS profile, using Intermediate default",
			slog.Any("error", err))
		tlsProfile = *openshiftv1.TLSProfiles[openshiftv1.TLSProfileIntermediateType]
	}
	logger.InfoContext(ctx, "Cluster TLS profile loaded",
		slog.String("minVersion", string(tlsProfile.MinTLSVersion)))

	// Set the TLS options.
	// If the enable-http2 flag is false (the default), http/2 will be disabled due to its vulnerabilities.
	// More specifically, disabling http/2 will prevent from being vulnerable to the HTTP/2 Stream
	// Cancelation and Rapid Reset CVEs. For more information see:
	// - https://github.com/advisories/GHSA-qppj-fm5r-hxr3
	// - https://github.com/advisories/GHSA-4374-p667-p6c8
	tlsOpts := []func(*tls.Config){}
	if !c.enableHTTP2 {
		tlsOpts = append(tlsOpts, func(c *tls.Config) {
			logger.InfoContext(ctx, "disabling http/2")
			c.NextProtos = []string{"http/1.1"}
		})
	}
	// Profile configurator is appended last so http/2 disabling (NextProtos) is not overridden.
	tlsOpts = append(tlsOpts, ctlrutils.NewTLSConfiguratorFromProfile(tlsProfile))

	operatorNamespace := ctlrutils.GetEnvOrDefault(constants.DefaultNamespaceEnvName, constants.DefaultNamespace)

	mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
		Scheme: scheme,
		Cache: cache.Options{
			ByObject: map[client.Object]cache.ByObject{
				&networkingv1.NetworkPolicy{}: {
					Namespaces: map[string]cache.Config{
						operatorNamespace: {},
					},
				},
			},
		},
		Metrics: metricsserver.Options{
			SecureServing:  c.metricsCertDir != "",
			CertDir:        c.metricsCertDir,
			BindAddress:    c.metricsAddr,
			TLSOpts:        tlsOpts,
			FilterProvider: filters.WithAuthenticationAndAuthorization,
		},
		HealthProbeBindAddress: c.probeAddr,
		LeaderElection:         c.enableLeaderElection,
		LeaderElectionID:       "a73bc4d2.openshift.io",
		WebhookServer: webhook.NewServer(
			webhook.Options{
				TLSOpts: tlsOpts,
			},
		),

		// LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily
		// when the Manager ends. This requires the binary to immediately end when the
		// Manager is stopped, otherwise, this setting is unsafe. Setting this significantly
		// speeds up voluntary leader transitions as the new leader don't have to wait
		// LeaseDuration time first.
		//
		// In the default scaffold provided, the program ends immediately after
		// the manager stops, so would be fine to enable this option. However,
		// if you are doing or is intended to do any operation such as perform cleanups
		// after the manager stops then its usage might be unsafe.
		// LeaderElectionReleaseOnCancel: true,
	})
	if err != nil {
		logger.ErrorContext(
			ctx,
			"Unable to start manager",
			slog.Any("error", err),
		)
		return exit.Error(1)
	}

	// Create the default inventory CR
	err = ctlrutils.CreateDefaultInventoryCR(ctx, mgr.GetClient())
	if err != nil {
		logger.ErrorContext(
			ctx,
			"Failed to create default inventory CR",
			slog.Any("error", err),
		)
		return exit.Error(1)
	}

	// Wire the TLS security profile watcher — triggers operator restart on profile change.
	profileWatcher := &tlspkg.SecurityProfileWatcher{
		Client:                mgr.GetClient(),
		InitialTLSProfileSpec: tlsProfile,
		OnProfileChange: func(watchCtx context.Context, oldSpec, newSpec openshiftv1.TLSProfileSpec) {
			logger.InfoContext(watchCtx, "Cluster TLS profile changed, initiating graceful shutdown",
				slog.String("oldMinVersion", string(oldSpec.MinTLSVersion)),
				slog.String("newMinVersion", string(newSpec.MinTLSVersion)))
			cancel()
		},
	}
	if err := profileWatcher.SetupWithManager(mgr); err != nil {
		logger.ErrorContext(ctx, "Unable to set up TLS security profile watcher",
			slog.Any("error", err))
		return exit.Error(1)
	}

	// Start the O-Cloud Manager controller.
	if err = (&controllers.Reconciler{
		Client:         mgr.GetClient(),
		Logger:         logger.With(slog.String("controller", "O-Cloud Manager")),
		Image:          c.image,
		TLSProfileHash: ctlrutils.TLSProfileHash(tlsProfile),
		TLSMinVersion:  string(tlsProfile.MinTLSVersion),
		TLSCiphers:     strings.Join(tlsProfile.Ciphers, ","),
	}).SetupWithManager(mgr); err != nil {
		logger.ErrorContext(
			ctx,
			"Unable to create controller",
			slog.String("controller", "O-Cloud Manager"),
			slog.Any("error", err),
		)
		return exit.Error(1)
	}

	// Register field indexes for hierarchy CRs (Location, OCloudSite, ResourcePool) for
	// efficient lookups
	if err := ctlrutils.SetupHierarchyIndexers(ctx, mgr); err != nil {
		logger.ErrorContext(ctx, "Unable to set up hierarchy indexers", slog.Any("error", err))
		return exit.Error(1)
	}

	// Start the Cluster Template controller.
	if err = (&controllers.ClusterTemplateReconciler{
		Client: mgr.GetClient(),
		Logger: logger.With(slog.String("controller", "ClusterTemplate")),
	}).SetupWithManager(mgr); err != nil {
		logger.ErrorContext(
			ctx,
			"Unable to create controller",
			slog.String("controller", "ClusterTemplate"),
			slog.Any("error", err),
		)
		return exit.Error(1)
	}

	// Start the Location controller.
	if err = (&controllers.LocationReconciler{
		Client: mgr.GetClient(),
		Logger: logger.With(slog.String("controller", "Location")),
	}).SetupWithManager(mgr); err != nil {
		logger.ErrorContext(
			ctx,
			"Unable to create controller",
			slog.String("controller", "Location"),
			slog.Any("error", err),
		)
		return exit.Error(1)
	}

	// Start the OCloudSite controller.
	if err = (&controllers.OCloudSiteReconciler{
		Client: mgr.GetClient(),
		Logger: logger.With(slog.String("controller", "OCloudSite")),
	}).SetupWithManager(mgr); err != nil {
		logger.ErrorContext(
			ctx,
			"Unable to create controller",
			slog.String("controller", "OCloudSite"),
			slog.Any("error", err),
		)
		return exit.Error(1)
	}

	// Start the ResourcePool controller.
	if err = (&controllers.ResourcePoolReconciler{
		Client: mgr.GetClient(),
		Logger: logger.With(slog.String("controller", "ResourcePool")),
	}).SetupWithManager(mgr); err != nil {
		logger.ErrorContext(
			ctx,
			"Unable to create controller",
			slog.String("controller", "ResourcePool"),
			slog.Any("error", err),
		)
		return exit.Error(1)
	}

	serverErrors := make(chan error, 1)

	// Start the FirmwareCatalog controller.
	if err = (&controllers.FirmwareCatalogReconciler{
		Client: mgr.GetClient(),
		Logger: logger.With(slog.String("controller", "FirmwareCatalog")),
	}).SetupWithManager(mgr); err != nil {
		logger.ErrorContext(
			ctx,
			"Unable to create controller",
			slog.String("controller", "FirmwareCatalog"),
			slog.Any("error", err),
		)
		return exit.Error(1)
	}

	// Start the Provisioning Request controller.
	if err = (&controllers.ProvisioningRequestReconciler{
		Client: mgr.GetClient(),
		Logger: logger.With(slog.String("controller", "ProvisioningRequest")),
	}).SetupWithManager(mgr); err != nil {
		logger.ErrorContext(
			ctx,
			"Unable to create controller",
			slog.String("controller", "ProvisioningRequest"),
			slog.Any("error", err),
		)
		return exit.Error(1)
	}

	if c.enableWebhooks {
		if err = (&provisioningv1alpha1.ProvisioningRequest{}).SetupWebhookWithManager(mgr); err != nil {
			logger.ErrorContext(
				ctx,
				"Unable to create webhook",
				slog.String("webhook", "ProvisioningRequest"),
				slog.Any("error", err),
			)
			return exit.Error(1)
		}
		if err = (&hwmgmtv1alpha1.FirmwareCatalog{}).SetupWebhookWithManager(mgr); err != nil {
			logger.ErrorContext(
				ctx,
				"Unable to create webhook",
				slog.String("webhook", "FirmwareCatalog"),
				slog.Any("error", err),
			)
			return exit.Error(1)
		}
	}

	if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
		logger.ErrorContext(
			ctx,
			"Unable to set up health check",
			slog.Any("error", err),
		)
		return exit.Error(1)
	}
	if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil {
		logger.ErrorContext(
			ctx,
			"Unable to set up ready check",
			slog.Any("error", err),
		)
		return exit.Error(1)
	}

	go func() {
		logger.InfoContext(
			ctx,
			"Starting manager",
			slog.String("image", c.image),
		)
		if err := mgr.Start(ctx); err != nil {
			logger.ErrorContext(ctx, "Problem running manager", slog.Any("error", err))
			serverErrors <- err
			return
		}
		cancel()
	}()

	select {
	case err = <-serverErrors:
		// Server failed to start
		logger.ErrorContext(ctx, "Problem running internal server", slog.Any("error", err))
		return exit.Error(1)
	case <-ctx.Done():
		return exit.Error(0)
	}
}

// Names of command line flags:
const (
	imageFlagName = "image"
)
