package daemon

import (
	"context"
	"flag"
	"net/http"
	"os"

	"github.com/tidbcloud/infra-provider/infra-inspector/pkg/config"
	"github.com/tidbcloud/infra-provider/infra-inspector/pkg/deps"
	"github.com/tidbcloud/infra-provider/infra-inspector/pkg/inspect"
	"github.com/tidbcloud/infra-provider/infra-inspector/pkg/notify"

	"github.com/go-logr/logr"
	"github.com/prometheus/client_golang/prometheus/promhttp"
	"github.com/robfig/cron/v3"
	"github.com/spf13/cobra"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"go.uber.org/zap/zapcore"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/log"
	"sigs.k8s.io/controller-runtime/pkg/log/zap"
)

const (
	examples = `
# inspect resources in a kubernetes cluster
	KUBECONFIG=/path/to/kubeconfig inspector inspect --rego-rule=/path/to/rego-rule cloud.pingcap.com/v1:Cluster

# list dataplanes and inspect resources in each dataplanes
	KUBECONFIG=/path/to/infra_apiserver inspector inspect --rego-rule=/path/to/rego-rule --dataplane v1:Pod
`
)

var Cmd = &cobra.Command{
	Use:     "daemon",
	Short:   "run a daemon and inspect resources in infra, gardener and dataplane periodically",
	Example: examples,
	Run:     run,
	Args:    cobra.MinimumNArgs(0),
}

var (
	setupLog = log.Log.WithName("Daemon")
	logOpts  = zap.Options{
		Development: true,
		TimeEncoder: zapcore.RFC3339TimeEncoder,
	}

	flagConfigFile string
)

func init() {
	Cmd.Flags().StringVar(&flagConfigFile, "config", "", "path to read root config")
	logOpts.BindFlags(flag.CommandLine)
	Cmd.Flags().AddGoFlagSet(flag.CommandLine)

	Cmd.MarkFlagsRequiredTogether("config")
}

func run(cmd *cobra.Command, args []string) {
	ctrl.SetLogger(zap.New(zap.UseFlagOptions(&logOpts)))

	config, err := config.LoadDaemonConfig(flagConfigFile)
	exitIfErr(err, "unable to load root config")
	setupLog.Info("load daemon config", "config", config)

	ctx := ctrl.SetupSignalHandler()

	deps := deps.New()
	err = deps.CreateClientsForInfra(config.InfraKubeConfig)
	exitIfErr(err, "unable to create deps for infra")
	err = deps.CreateClientsForGardener(config.GardenerKubeConfig)
	exitIfErr(err, "unable to create deps for gardener")

	cron := cron.New()
	notifier, err := notify.NewNotifier(config.Notify)
	exitIfErr(err, "unable to create notifier")

	// infra
	if config.Infra != nil {
		inspector, err := inspect.NewGenericInspector("infra", config.Infra.Engine, deps.InfraClient, deps.InfraDynamicClient)
		exitIfErr(err, "unable to create infra inspector")

		err = cronInspectAndNotify(ctx, log.Log.WithName("Infra"), cron, inspector, notifier, *config.Infra)
		exitIfErr(err, "unable to add cron job")

		setupLog.Info("added cron job for inspecting infra")
	}

	// gardener
	if config.Gardener != nil {
		inspector, err := inspect.NewGenericInspector("gardener", config.Gardener.Engine, deps.GardenerClient, deps.GardenerDynamicClient)
		exitIfErr(err, "unable to create gardener inspector")

		err = cronInspectAndNotify(ctx, log.Log.WithName("Gardener"), cron, inspector, notifier, *config.Gardener)
		exitIfErr(err, "unable to add cron job")

		setupLog.Info("added cron job for inspecting gardener")
	}

	// dataplane
	if config.Dataplane != nil {
		inspector, err := inspect.NewDataplaneInspector(config.Environment, deps, config.Dataplane.Engine)
		exitIfErr(err, "unable to create dataplane inspector")

		err = cronInspectAndNotify(ctx, log.Log.WithName("Dataplane"), cron, inspector, notifier, *config.Dataplane)
		exitIfErr(err, "unable to add cron job")

		setupLog.Info("added cron job for inspecting dataplane")
	}

	cron.Start()

	mux := http.DefaultServeMux
	mux.Handle("/metrics", promhttp.Handler())
	startHTTPServer(ctx, &http.Server{Addr: ":8080", Handler: mux})

	<-cron.Stop().Done()
}

func cronInspectAndNotify(
	ctx context.Context,
	logger logr.Logger,
	cron *cron.Cron,
	inspector inspect.Inspector,
	notifier *notify.Notifier,
	config config.CronInspectConfig,
) error {
	_, err := cron.AddFunc(config.Schedule, func() {

		logger := logger.WithValues("trace_id", cloudv1.RandomString(8))
		logger.Info("begin inspecting resources")

		results, err := inspector.Inspect(logr.NewContext(ctx, logger), config.List)
		if err != nil {
			logger.Error(err, "unable to inspect resources")
			return
		}

		for _, result := range results {
			notifier.Notify(ctx, result)

			for _, violation := range result.Violations {
				for _, rule := range violation.ViolatedRules {
					logger.Info("a violation found", "rule", rule, "resource", violation.Resource)
				}
			}
		}
		logger.Info("end inspecting resources")
	})
	return err
}

func exitIfErr(err error, msg string) {
	if err != nil {
		setupLog.Error(err, msg)
		os.Exit(1)
	}
}

func startHTTPServer(ctx context.Context, server *http.Server) {
	closed := make(chan struct{})
	go func() {
		<-ctx.Done()
		if err := server.Shutdown(context.Background()); err != nil {
			setupLog.Error(err, "failed to shutdown http server")
		}
		close(closed)
	}()

	setupLog.Info("starting http server", "addr", server.Addr)
	err := server.ListenAndServe()
	if err != http.ErrServerClosed {
		exitIfErr(err, "failed to start metrics server")
	}

	<-closed
	setupLog.Info("closed http server")
}
