/*
Copyright 2022.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package main

import (
	"context"
	"flag"
	"fmt"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"

	"github.com/bradleyfalzon/ghinstallation/v2"
	"github.com/gin-gonic/gin"
	"github.com/google/go-github/v56/github"
	"github.com/tidbcloud/config-manager/pkg/clinic_event"
	"github.com/tidbcloud/config-manager/pkg/config"
	"github.com/tidbcloud/config-manager/pkg/controllers/clusterconfig"
	"github.com/tidbcloud/config-manager/pkg/gitops-v2/secondaryconfirm"
	"github.com/tidbcloud/config-manager/pkg/webhooks"
	"github.com/tidbcloud/infra-provider/pkg/scheme"
	"github.com/tidbcloud/tiinf/transport/http/tigin"
	"go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"
	"sigs.k8s.io/controller-runtime/pkg/log"
	metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
	"sigs.k8s.io/controller-runtime/pkg/webhook"

	// 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"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/healthz"
	"sigs.k8s.io/controller-runtime/pkg/log/zap"
)

var (
	setupLog    = ctrl.Log.WithName("setup")
	serviceName = "config-manager"
)

var (
	metricsAddr          string
	enableLeaderElection bool
	probeAddr            string
	gcPeriod             time.Duration
	configRevisionTTL    time.Duration
	activityHub          bool
	activityHubConfig    string
	// github app related
	ghAppPrivateKeyFile string
	ghAppApplicationID  int64
	ghAppInstallID      int64

	clusterNSPrefix string

	jumpEndpoint string
	apiAddr      string

	configFilePath string
)

func main() {
	flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.")
	flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.")
	flag.BoolVar(&enableLeaderElection, "leader-elect", false,
		"Enable leader election for controller manager. "+
			"Enabling this will ensure there is only one active controller manager.")
	flag.DurationVar(&gcPeriod, "gc-period", 1*time.Hour, "The duration of time between garbage-collection runs.")
	flag.DurationVar(&configRevisionTTL, "config-revision-ttl", 12*time.Hour, "The duration of time before a config revision should be garbage collected.")
	flag.BoolVar(&activityHub, "activity", false, "if true, will send the gitops event")
	flag.StringVar(&activityHubConfig, "activity-config", "", "the activity config path")
	flag.StringVar(&ghAppPrivateKeyFile, "ghapp-private-key-file", "", "The private key file for github app.")
	flag.Int64Var(&ghAppApplicationID, "ghapp-app-id", 0, "The app ID for github app.")
	flag.Int64Var(&ghAppInstallID, "ghapp-install-id", 0, "The install ID for github app.")
	flag.StringVar(&jumpEndpoint, "jump-endpoint", "", "The external endpoint where user can apply the config change.")
	flag.StringVar(&clusterNSPrefix, "cluster-ns-prefix", "", "The namespace prefix for accessing cluster cr.")
	flag.StringVar(&apiAddr, "api-bind-address", ":8082", "The address the api server binds to.")
	flag.StringVar(&configFilePath, "config-path", "/etc/config-manager.yaml", "The config file path of config-manager server.")

	opts := zap.Options{
		Development: true,
	}
	opts.BindFlags(flag.CommandLine)
	flag.Parse()
	config.InitConfig(configFilePath)

	ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))

	deploymentRegion := config.Global().Server.DeploymentRegion
	if deploymentRegion == "" {
		deploymentRegion = "global" // to avoid generating invalid leader election record
	}
	leaderElectID := fmt.Sprintf("%s.%s.tidbcloud.com", serviceName, deploymentRegion)
	setupLog.Info("config-manager running", "region", deploymentRegion, "electID", leaderElectID)
	mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
		Scheme: scheme.Scheme,
		Metrics: metricsserver.Options{
			BindAddress: metricsAddr,
		},
		WebhookServer: webhook.NewServer(webhook.Options{
			Port: 9443,
		}),
		HealthProbeBindAddress: probeAddr,
		LeaderElection:         enableLeaderElection,
		LeaderElectionID:       leaderElectID, // use different id from infra-provider.
	})
	if err != nil {
		setupLog.Error(err, "unable to start manager")
		os.Exit(1)
	}

	// enable clusterconfig controller and GC only when config-manager is deployed in global
	if config.Global().Server.IsGloballyDeployed() {
		itr, err := ghinstallation.NewKeyFromFile(http.DefaultTransport, ghAppApplicationID, ghAppInstallID, ghAppPrivateKeyFile)
		if err != nil {
			setupLog.Error(err, "unable to load gh private key")
			os.Exit(1)
		}
		ghClient := github.NewClient(&http.Client{Transport: itr})
		if err = clusterconfig.NewClusterConfigReconciler(
			mgr,
			ctrl.Log.WithName("controllers").WithName("ClusterConfig"),
			clusterconfig.WithGithubClient(&clusterconfig.GithubClientWrapper{Client: ghClient}),
			clusterconfig.WithJumpEndpoint(jumpEndpoint),
		).SetupWithManager(mgr); err != nil {
			setupLog.Error(err, "unable to create controller", "controller", "ClusterConfig")
			os.Exit(1)
		}
		if err = clusterconfig.NewGarbageCollector(mgr, ctrl.Log.WithName("controllers").WithName("ClusterConfigGarbageCollector"), gcPeriod, configRevisionTTL, clusterNSPrefix).SetupWithManager(mgr); err != nil {
			setupLog.Error(err, "unable to create controller", "controller", "ClusterConfigGarbageCollector")
			os.Exit(1)
		}
	}

	// clinic event
	if activity := config.Global().Server.Activity; activity.Enabled {
		setupLog.Info("clinic activity is enabled")
		token := os.Getenv(config.EnvKeyClinicActivityToken)
		if token == "" {
			panic("clinic activity token is required")
		}
		if err := clinic_event.InitEventClient(
			activity.ActivityHubEndpoint,
			activity.ActivityHubSource,
			activity.ActivityHubSourceKey,
			activity.ClinicEndpoint,
			token,
		); err != nil {
			setupLog.Error(err, "unable to init clinic event")
			os.Exit(1)
		}
	}
	// http api server
	{
		// TODO: rename for the api
		api, err := secondaryconfirm.NewAPI(
			mgr.GetClient(),
			clusterNSPrefix)
		if err != nil {
			setupLog.Error(err, "unable to new api")
			os.Exit(1)
		}
		engine := gin.New()
		engine.Use(
			gin.Logger(),
			gin.Recovery(),
			tigin.RecordMetrics,
			otelgin.Middleware("config-manager"),
			tigin.TraceRequest,
			tigin.SetResponseTraceID,
		)
		engine.NoRoute(func(c *gin.Context) {
			logger := log.FromContext(c.Request.Context())
			logger.Info("no route", "url", c.Request.URL.String())
		})
		secondaryconfirm.InitRouter(engine, api)
		go StarAPIServer(engine, apiAddr)
	}

	//+kubebuilder:scaffold:builder

	// setup cluster webhook
	// enable webhook only when the server is running in global
	if config.Global().Server.IsGloballyDeployed() {
		mgr.GetWebhookServer().Register("/mutate-cloud-pingcap-com-v1-cluster-revision", &webhook.Admission{Handler: webhooks.NewClusterRevisionMutator(mgr)})
	}

	if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
		setupLog.Error(err, "unable to set up health check")
		os.Exit(1)
	}
	if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil {
		setupLog.Error(err, "unable to set up ready check")
		os.Exit(1)
	}

	setupLog.Info("starting manager")
	if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
		setupLog.Error(err, "problem running manager")
		os.Exit(1)
	}
}

func StarAPIServer(engine *gin.Engine, addr string) {
	srv := &http.Server{
		Addr:    addr,
		Handler: engine,
	}

	go func() {
		// service connections
		if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
			setupLog.Error(err, "api server listen and serve failed")
			os.Exit(1)
		}
	}()

	ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
	<-ctx.Done()
	defer stop()
	setupLog.Info("api server shutting down")
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	if err := srv.Shutdown(ctx); err != nil {
		setupLog.Error(err, "api server shutdown failed")
		os.Exit(1)
	}
	setupLog.Info("api server existed")
}
