package main

import (
	"context"
	"flag"
	"net/http"
	"net/http/pprof"
	"os"
	"path/filepath"

	awsconfig "github.com/aws/aws-sdk-go-v2/config"
	pcaissuerv1beta1 "github.com/cert-manager/aws-privateca-issuer/pkg/api/v1beta1"
	ec2v1alpha1 "github.com/crossplane/provider-aws/apis/ec2/v1alpha1"
	awskmsv1alpha1 "github.com/crossplane/provider-aws/apis/kms/v1alpha1"
	awsdnsv1alpha1 "github.com/crossplane/provider-aws/apis/route53/v1alpha1"
	s3v1beta1 "github.com/crossplane/provider-aws/apis/s3/v1beta1"
	awsvpcpeeringv1alpha1 "github.com/crossplane/provider-aws/apis/vpcpeering/v1alpha1"
	gcpdnsv1alpha1 "github.com/crossplane/provider-gcp/apis/dns/v1alpha1"
	iamv1alpha1 "github.com/crossplane/provider-gcp/apis/iam/v1alpha1"
	storagev1alpha3 "github.com/crossplane/provider-gcp/apis/storage/v1alpha3"
	gcpvpcpeeringv1beta1 "github.com/crossplane/provider-gcp/apis/vpcpeering/v1beta1"
	certmanagerv1alpha2 "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha2"
	operatorv1alpha1 "github.com/pingcap/tidb-operator/pkg/apis/pingcap/v1alpha1"
	configsdk "github.com/tidbcloud/config-service/sdk"
	"go.opentelemetry.io/contrib/instrumentation/github.com/aws/aws-sdk-go-v2/otelaws"
	clientgoscheme "k8s.io/client-go/kubernetes/scheme"
	"k8s.io/component-base/config/v1alpha1"

	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-api/controllers"
	"github.com/tidbcloud/infra-api/controllers/backup"
	"github.com/tidbcloud/infra-api/controllers/cluster"
	"github.com/tidbcloud/infra-api/controllers/imports"
	"github.com/tidbcloud/infra-api/controllers/networkpeer"
	"github.com/tidbcloud/infra-api/controllers/providertype"
	"github.com/tidbcloud/infra-api/controllers/ticdc"
	"github.com/tidbcloud/infra-api/controllers/utils"
	"github.com/tidbcloud/infra-api/pkg/audit"
	"github.com/tidbcloud/infra-api/pkg/config"
	"github.com/tidbcloud/infra-api/pkg/consts"
	"github.com/tidbcloud/infra-api/pkg/gardener"
	"github.com/tidbcloud/infra-api/pkg/gardener/core/apis/v1beta1"
	"github.com/tidbcloud/infra-api/pkg/gardener/hibernation"
	"github.com/tidbcloud/infra-api/pkg/gardener/node_controller"
	"github.com/tidbcloud/infra-api/pkg/gitops"
	"github.com/tidbcloud/infra-api/pkg/metrics"
	"github.com/tidbcloud/infra-api/pkg/o11y"
	asts "github.com/tidbcloud/infra-api/pkg/pingcap/apis/v1"
	"github.com/tidbcloud/infra-api/pkg/provider"
	"github.com/tidbcloud/infra-api/pkg/providers/aws"
	providergcp "github.com/tidbcloud/infra-api/pkg/providers/gcp"
	"github.com/tidbcloud/infra-api/pkg/tracer"
	"github.com/tidbcloud/infra-api/pkg/util"

	// 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/apimachinery/pkg/runtime"
	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
	_ "k8s.io/client-go/plugin/pkg/client/auth"
	ctrl "sigs.k8s.io/controller-runtime"
	cfg "sigs.k8s.io/controller-runtime/pkg/config/v1alpha1"
	"sigs.k8s.io/controller-runtime/pkg/healthz"
	"sigs.k8s.io/controller-runtime/pkg/log/zap"
	"sigs.k8s.io/controller-runtime/pkg/predicate"
	//+kubebuilder:scaffold:imports
)

var (
	scheme               = runtime.NewScheme()
	setupLog             = ctrl.Log.WithName("setup")
	metricsAddr          string
	enableLeaderElection bool
	probeAddr            string
	pprofActive          bool
	namespace            string
	enableGitopsV2       bool
	enableO11yAgent      bool
)

func init() {
	utilruntime.Must(v1beta1.AddToScheme(scheme))
	utilruntime.Must(clientgoscheme.AddToScheme(scheme))
	utilruntime.Must(cloudv1.AddToScheme(scheme))
	utilruntime.Must(awskmsv1alpha1.AddToScheme(scheme))
	utilruntime.Must(gcpvpcpeeringv1beta1.SchemeBuilder.AddToScheme(scheme))
	utilruntime.Must(awsvpcpeeringv1alpha1.SchemeBuilder.AddToScheme(scheme))
	utilruntime.Must(ec2v1alpha1.SchemeBuilder.AddToScheme(scheme))
	utilruntime.Must(certmanagerv1alpha2.AddToScheme(scheme))
	utilruntime.Must(pcaissuerv1beta1.AddToScheme(scheme))
	utilruntime.Must(s3v1beta1.SchemeBuilder.AddToScheme(scheme))
	utilruntime.Must(storagev1alpha3.SchemeBuilder.AddToScheme(scheme))
	utilruntime.Must(iamv1alpha1.SchemeBuilder.AddToScheme(scheme))
	utilruntime.Must(operatorv1alpha1.AddToScheme(scheme))
	utilruntime.Must(awsdnsv1alpha1.SchemeBuilder.AddToScheme(scheme))
	utilruntime.Must(gcpdnsv1alpha1.SchemeBuilder.AddToScheme(scheme))
	utilruntime.Must(asts.SchemeBuilder.AddToScheme(scheme))
}

func main() {
	flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.")
	flag.BoolVar(&pprofActive, "pprof", true, "Enable pprof endpoint")
	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.")
	var configFile string
	flag.StringVar(&configFile, "config", "",
		"The controller will load its initial configuration from this file. "+
			"Omit this flag to use the default configuration values. "+
			"Command-line flags override configuration from this file.")
	flag.StringVar(&namespace, "namespace", "", "Namespace if specified restricts the manager's cache to watch objects in the desired namespace.")
	flag.BoolVar(&enableGitopsV2, "enable-gitops-v2", false, "Used to enable gitops v2 feature")
	flag.BoolVar(&enableO11yAgent, "enable-o11y-agent", false, "Used to enable o11y agent controller")

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

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

	tracer.InitTracer(setupLog)

	var err error
	ctrlConfig := config.InfraAPIConfig{
		InfraAPIConfig: cloudv1.InfraAPIConfig{},
		ControllerManagerConfigurationSpec: cfg.ControllerManagerConfigurationSpec{
			LeaderElection: &v1alpha1.LeaderElectionConfiguration{},
		},
	}
	options := ctrl.Options{
		Scheme:                 scheme,
		MetricsBindAddress:     metricsAddr,
		Port:                   9443,
		HealthProbeBindAddress: probeAddr,
		LeaderElection:         enableLeaderElection,
		LeaderElectionID:       "81a1860c.pingcap.com",
		Namespace:              namespace,
	}

	if configFile != "" {
		options, err = options.AndFrom(ctrl.ConfigFile().AtPath(configFile).OfKind(&ctrlConfig))
		if err != nil {
			setupLog.Error(err, "unable to load the config file")
			os.Exit(1)
		}

		setupLog.Info("load config from file", "gardener", ctrlConfig.Gardener)
	}

	ctrlConfig.Default()
	setupLog.Info("mixed-deploy-pd-with-tikv",
		"enabled",
		ctrlConfig.MixedDeployPDWithTiKV.Enabled,
		"allow list",
		ctrlConfig.MixedDeployPDWithTiKV.AffectNamespaceAllowList,
		"block list",
		ctrlConfig.MixedDeployPDWithTiKV.AffectNamespaceBlockList,
		"affect clusters created after",
		ctrlConfig.MixedDeployPDWithTiKV.AffectClusterCreatedAfter,
		"affect topology",
		ctrlConfig.MixedDeployPDWithTiKV.AffectTopology.String())

	err = ctrlConfig.Validate()
	if err != nil {
		setupLog.Error(err, "validate config failed")
		os.Exit(1)
	}

	if os.Getenv("GARDENER_CONFIG") != "" {
		ctrlConfig.Gardener.KubeConfigPath = os.Getenv("GARDENER_CONFIG")
	}

	mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), options)
	if err != nil {
		setupLog.Error(err, "unable to start manager")
		os.Exit(1)
	}

	// This assumes we can only run with one provider(one account for aws type)
	awsCfg, err := awsconfig.LoadDefaultConfig(context.Background())
	if err != nil {
		setupLog.Error(err, "unable to load aws configuration")
		os.Exit(1)
	}

	// Instrument all aws clients
	otelaws.AppendMiddlewares(&awsCfg.APIOptions)

	awsClient := aws.NewClient(awsCfg)
	gcpapi, err := providergcp.NewDefaultGCPAPI()
	if err != nil {
		setupLog.Error(err, "unable to setup gcpapi")
		os.Exit(1)
	}

	// configService is an interface of gitops which provides the configuration of TiDB clusters
	configService := configsdk.New(ctrlConfig.ConfigServiceUrl)

	nodeControllerRegister := &node_controller.ClusterNodeControllersRegister{
		ConfigProvider: gitops.NewHardcode(),
		InfraClient:    mgr.GetClient(),
		CRDPaths:       []string{filepath.Join(ctrlConfig.Server.ManifestDirectoryPrefix, "/manifests/tidb-operator-crd.yaml")},
		AWSClient:      awsClient,
		GCPClient:      gcpapi,
	}
	gardenerEnv, err := gardener.NewGardEnvironment(&ctrlConfig.Gardener, &ctrlConfig.InfraAPIConfig,
		[]gardener.ShootReconcilerRegister{nodeControllerRegister})
	if err != nil {
		setupLog.Error(err, "unable to init gardener env")
		os.Exit(1)
	}
	defer gardenerEnv.Close()

	predicate, err := predicate.LabelSelectorPredicate(ctrlConfig.Predicate)
	if err != nil {
		setupLog.Error(err, "unable to create predicate")
		os.Exit(1)
	}
	AWSGCPProviderPredicate := utils.GetPredicateForProviderTypes(consts.ProviderTypeAWS, consts.ProviderTypeGCP)

	if err = providertype.NewReconciler(mgr, consts.ProviderTypeAWS).SetupWithManager(mgr, predicate); err != nil {
		setupLog.Error(err, "unable to create controller", "controller", "ProviderType")
		os.Exit(1)
	}
	if err = providertype.NewReconciler(mgr, consts.ProviderTypeGCP).SetupWithManager(mgr, predicate); err != nil {
		setupLog.Error(err, "unable to create controller", "controller", "ProviderType")
		os.Exit(1)
	}

	// Setup o11y agent controllers
	var o11yApi o11y.Api
	if enableO11yAgent {
		// Init o11y api
		o11yApi, err = o11y.InitApi(
			ctrlConfig.ObservabilityInfraConfig.Auth0GetTokenURL,
			ctrlConfig.ObservabilityInfraConfig.Auth0ClientID,
			ctrlConfig.ObservabilityInfraConfig.Auth0ClientSecret,
			ctrlConfig.ObservabilityInfraConfig.Auth0Audience,
			ctrlConfig.ObservabilityInfraConfig.OpenAPIAddress,
			ctrlConfig.ObservabilityInfraConfig.CreateO11yInfraPath,
			ctrlConfig.ObservabilityInfraConfig.DeleteO11yInfraPath,
			ctrlConfig.ObservabilityInfraConfig.GetO11yInfraPath,
		)
		if err != nil {
			setupLog.Error(err, "unable to setup o11y client")
			os.Exit(1)
		}
	}

	if err = cluster.NewReconciler(
		mgr,
		gardenerEnv,
		consts.ProviderTypeAWS,
		awsClient,
		gcpapi,
		configService,
		&ctrlConfig.InfraAPIConfig,
		enableO11yAgent,
		ctrlConfig.ManifestDirectoryPrefix,
		ctrlConfig.ObservabilityInfraConfig,
		o11yApi,
	).SetUpGitopsV2(enableGitopsV2).SetupWithManager(mgr, predicate, ctrlConfig.ClusterMaxConcurrentReconciles); err != nil {
		setupLog.Error(err, "unable to create controller", "controller", "Cluster")
		os.Exit(1)
	}
	if err = cluster.NewReconciler(
		mgr,
		gardenerEnv,
		consts.ProviderTypeGCP,
		awsClient,
		gcpapi,
		configService,
		&ctrlConfig.InfraAPIConfig,
		enableO11yAgent,
		ctrlConfig.ManifestDirectoryPrefix,
		ctrlConfig.ObservabilityInfraConfig,
		o11yApi,
	).SetUpGitopsV2(enableGitopsV2).SetupWithManager(mgr, predicate, ctrlConfig.ClusterMaxConcurrentReconciles); err != nil {
		setupLog.Error(err, "unable to create controller", "controller", "Cluster")
		os.Exit(1)
	}

	if err = (&controllers.NetworkReconciler{
		Client:      mgr.GetClient(),
		Log:         ctrl.Log.WithName("controllers").WithName("Network"),
		Scheme:      mgr.GetScheme(),
		GardenerEnv: gardenerEnv,
		Recorder:    mgr.GetEventRecorderFor("network-controller"),
		Config:      &ctrlConfig.InfraAPIConfig,
		AWSClient:   awsClient,
		GCPAPI:      gcpapi,
	}).SetupWithManager(mgr,
		enableO11yAgent,
		ctrlConfig.ManifestDirectoryPrefix,
		ctrlConfig.ObservabilityInfraConfig,
		o11yApi,
		ctrlConfig.NetworkMaxConcurrentReconciles,
		predicate,
		AWSGCPProviderPredicate,
	); err != nil {
		setupLog.Error(err, "unable to create controller", "controller", "Network")
		os.Exit(1)
	}

	if err = hibernation.NewHibernationController(
		gardenerEnv.Manager().GetClient(),
		mgr.GetClient(),
		gardenerEnv,
		mgr.GetEventRecorderFor("hibernation-controller"),
		ctrlConfig.Predicate,
		ctrlConfig.Hibernation).SetupWithManager(gardenerEnv.Manager()); err != nil {
		setupLog.Error(err, "unable to create controller", "controller", "Hibernation")
		os.Exit(1)
	}

	if err = networkpeer.NewNetworkPeerReconciler(
		mgr,
		ctrl.Log.WithName("controllers").WithName("NetworkPeer"),
		&ctrlConfig.InfraAPIConfig,
		awsClient,
		gcpapi).
		SetupWithManager(mgr, predicate); err != nil {
		setupLog.Error(err, "unable to create controller", "controller", "NetworkPeer")
		os.Exit(1)
	}

	if err = imports.NewImportReconciler(mgr, ctrl.Log.WithName("controllers").WithName("Import"), imports.NewGardenerShootEnvBuilder(mgr.GetClient(), gardenerEnv), configService).
		SetUpGitopsV2().SetupWithManager(mgr, predicate, AWSGCPProviderPredicate); err != nil {
		setupLog.Error(err, "unable to create controller", "controller", "Import")
		os.Exit(1)
	}
	if err = backup.NewBackupReconciler(mgr, ctrl.Log.WithName("controllers").WithName("Backup"), gardenerEnv, nil, configService, provider.NewUnimplemented()).
		SetupWithManager(mgr, predicate, AWSGCPProviderPredicate); err != nil {
		setupLog.Error(err, "unable to create controller", "controller", "Backup")
		os.Exit(1)
	}
	if err = backup.NewRestoreReconciler(mgr, ctrl.Log.WithName("controllers").WithName("Restore"), gardenerEnv, nil, configService, awsClient, gcpapi, provider.NewUnimplemented()).
		SetupWithManager(mgr, predicate, AWSGCPProviderPredicate); err != nil {
		setupLog.Error(err, "unable to create controller", "controller", "Restore")
		os.Exit(1)
	}

	if err = ticdc.NewTiCDCReconciler(
		mgr,
		ctrl.Log.WithName("controllers").WithName("TiCDC"),
		gardenerEnv,
		nil,
		configService,
		mgr.GetEventRecorderFor("cdc-controller"), provider.NewGardenerCDCProvider()).
		SetupWithManager(mgr, predicate, AWSGCPProviderPredicate); err != nil {
		setupLog.Error(err, "unable to create controller", "controller", "TiCDC")
		os.Exit(1)
	}

	if os.Getenv("ENABLE_WEBHOOKS") != "false" {
		if err = cloudv1.RegisterWebhooks(mgr, util.NewCloudPeerCreator(awsClient, gcpapi)); err != nil {
			setupLog.Error(err, "unable to register webhooks")
			os.Exit(1)
		}
	}

	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)
	}

	// handlers set in pprof.init()
	handlers := map[string]http.HandlerFunc{}
	if pprofActive {
		handlers = map[string]http.HandlerFunc{
			"/debug/pprof/":        pprof.Index,
			"/debug/pprof/cmdline": pprof.Cmdline,
			"/debug/pprof/profile": pprof.Profile,
			"/debug/pprof/symbol":  pprof.Symbol,
			"/debug/pprof/trace":   pprof.Trace,
		}
	}

	for path, handler := range handlers {
		err := mgr.AddMetricsExtraHandler(path, handler)
		if err != nil {
			setupLog.Error(err, "unable to set up pprof handler", "path", path)
			os.Exit(1)
		}
	}

	setupAudit(ctrlConfig.Audit)

	ctx := ctrl.SetupSignalHandler()

	go func() {
		<-mgr.Elected()
		setupLog.Info("win a leader election or no election, start to run gardener env")
		// Run metrics collector after elected as leader
		// otherwise the collector will reports errors, see https://jira.tidbcloud.com/browse/DM-5055
		metrics.RegisterMetrics(mgr.GetClient(), gardenerEnv.Manager().GetClient())
		err := gardenerEnv.Start(ctx)
		if err != nil {
			setupLog.Error(err, "problem running gardener env")
			os.Exit(1)
		}
	}()

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

func setupAudit(cfg cloudv1.AuditConfig) {
	audit.RegisterGCPCredentialGetter(consts.ProviderTypeGCP, imports.GCPCredentials)

	if cfg.SidecarImage != "" {
		audit.SidecarTemplate.Image = cfg.SidecarImage
	}

	if cfg.SidecarCommand != nil {
		audit.SidecarTemplate.Command = cfg.SidecarCommand
	}
}
