package app

import (
	"flag"
	"fmt"
	"os"

	"github.com/go-logr/logr"
	"github.com/spf13/cobra"
	"github.com/spf13/pflag"
	"go.uber.org/zap"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/fields"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/cache"
	ctrlcluster "sigs.k8s.io/controller-runtime/pkg/cluster"
	"sigs.k8s.io/controller-runtime/pkg/healthz"
	zapopt "sigs.k8s.io/controller-runtime/pkg/log/zap"

	"github.com/tidbcloud/shared-provider/cmd/dispatcher/app/config"
	"github.com/tidbcloud/shared-provider/cmd/dispatcher/app/options"
	"github.com/tidbcloud/shared-provider/pkg/dispatcher"
	"github.com/tidbcloud/shared-provider/pkg/manager"
	"github.com/tidbcloud/shared-provider/pkg/scheme"
	"github.com/tidbcloud/shared-provider/pkg/version"
)

// NewCommand returns app command.
func NewCommand() *cobra.Command {
	setupLog := ctrl.Log.WithName("setup")

	opts := options.New()
	lvl := zap.NewAtomicLevelAt(zap.FatalLevel)
	zapOpts := zapopt.Options{
		Development:     true,
		StacktraceLevel: &lvl,
	}

	cmd := &cobra.Command{
		Use:  "dispatcher",
		Long: "dispatcher watches infra API and dispatch to different kube cluster",
		Run: func(cmd *cobra.Command, args []string) {
			ctrl.SetLogger(zapopt.New(zapopt.UseFlagOptions(&zapOpts)))
			fmt.Printf("%v\n", version.Get())
			printFlags(setupLog, cmd.Flags())

			cfg, err := opts.Config()
			if err != nil {
				setupLog.Error(err, "can't parse options to config")
				os.Exit(-1)
			}

			setupLog.Info("begin to start dispatcher")
			if err := Run(cfg); err != nil {
				setupLog.Error(err, "run dispatcher failed")
				os.Exit(-1)
			}
		},
	}
	opts.AddFlags(cmd.Flags())
	zapOpts.BindFlags(flag.CommandLine)

	cmd.AddCommand(NewVersionCmd())

	return cmd
}

// NewVersionCmd return cmd reports version.
func NewVersionCmd() *cobra.Command {
	cmd := &cobra.Command{
		Use:  "version",
		Long: "dispatcher version",
		Run: func(cmd *cobra.Command, args []string) {
			fmt.Printf("%v\n", version.Get())
		},
	}

	return cmd
}

// Run runs the operator.
func Run(cfg *config.Config) error {
	mgr, err := manager.New(cfg.Kubeconfig, cfg.InfraKubeconfig, manager.Options{
		Manager: ctrl.Options{
			Scheme:                 scheme.Scheme,
			MetricsBindAddress:     cfg.MetricsBindAddress,
			Port:                   cfg.WebhookPort,
			HealthProbeBindAddress: cfg.HealthProbeBindAddress,
			LeaderElection:         cfg.EnableLeaderElection,
			LeaderElectionID:       "7dd07113.pingcap.com",
			// Only watch a specific configmap
			// If more configmaps is needed to watch, please change below code
			NewCache: cache.BuilderWithOptions(cache.Options{
				SelectorsByObject: cache.SelectorsByObject{
					&corev1.ConfigMap{}: {
						Field: fields.SelectorFromSet(
							fields.Set{
								"metadata.name":      cfg.KubeClusterConfigMapName,
								"metadata.namespace": cfg.KubeClusterConfigMapNamespace,
							},
						),
					},
				},
			}),
		},
		InfraCluster: ctrlcluster.Options{
			Scheme: scheme.Scheme,
		},
	})
	if err != nil {
		return fmt.Errorf("unable to start dispatcher: %w", err)
	}

	nd := dispatcher.NewNetworkDispatcher(&dispatcher.NetworkDispatcherConfig{
		Manager:                       mgr,
		KubeClusterConfigMapName:      cfg.KubeClusterConfigMapName,
		KubeClusterConfigMapNamespace: cfg.KubeClusterConfigMapNamespace,
		MaxPerKubeCluster:             cfg.MaxPerKubeCluster,
	})

	if err = dispatcher.New(nd).SetupWithManager(mgr); err != nil {
		return fmt.Errorf("unable to create network dispatcher controller: %w", err)
	}

	cd := dispatcher.NewClusterDispatcher(&dispatcher.ClusterDispatcherConfig{
		Manager:                       mgr,
		RescheduleThreshold:           cfg.RescheduleThreshold,
		KubeClusterConfigMapName:      cfg.KubeClusterConfigMapName,
		KubeClusterConfigMapNamespace: cfg.KubeClusterConfigMapNamespace,
	})

	if err = dispatcher.New(cd).SetupWithManager(mgr); err != nil {
		return fmt.Errorf("unable to create cluster dispatcher controller: %w", err)
	}

	resources := []string{
		dispatcher.TypeRestore,
		dispatcher.TypeBackup,
		dispatcher.TypeImport,
	}

	for _, r := range resources {
		d, err := dispatcher.NewOthersDispatcher(&dispatcher.OthersDispatcherConfig{
			Manager: mgr,
			Type:    r,
		})
		if err != nil {
			return err
		}
		if err := dispatcher.New(d).SetupWithManager(mgr); err != nil {
			return fmt.Errorf("unable to create %s dispatcher controller: %w", r, err)
		}
	}

	if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
		return fmt.Errorf("unable to set up health check: %w", err)
	}
	if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil {
		return fmt.Errorf("unable to set up ready check: %w", err)
	}

	if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
		return fmt.Errorf("problem running manager: %w", err)
	}

	return nil
}

func printFlags(logger logr.Logger, fs *pflag.FlagSet) {
	fs.VisitAll(func(f *pflag.Flag) {
		logger.Info("FLAG", f.Name, f.Value)
	})
}
