package cmd

import (
	"context"
	"fmt"
	"net/http"
	"net/http/pprof"
	"os"
	"time"

	"github.com/aws/aws-sdk-go-v2/config"
	"github.com/pingcap/errors"
	"go.uber.org/zap/zapcore"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/types"
	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
	_ "k8s.io/client-go/plugin/pkg/client/auth" // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.
	"k8s.io/client-go/tools/clientcmd"
	"k8s.io/utils/ptr"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/cache"
	"sigs.k8s.io/controller-runtime/pkg/client"
	runtimeConfig "sigs.k8s.io/controller-runtime/pkg/config"
	"sigs.k8s.io/controller-runtime/pkg/healthz"
	"sigs.k8s.io/controller-runtime/pkg/log/zap"
	metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
	"sigs.k8s.io/controller-runtime/pkg/webhook"

	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-provider/pkg/auth"
	"github.com/tidbcloud/infra-provider/pkg/cloud/aws"
	pkgconfig "github.com/tidbcloud/infra-provider/pkg/config"
	sdk "github.com/tidbcloud/infra-provider/pkg/gitops-sdk"
	"github.com/tidbcloud/infra-provider/pkg/metrics"
	"github.com/tidbcloud/infra-provider/pkg/o11y/sentry"
	"github.com/tidbcloud/infra-provider/pkg/scheme"
	//+kubebuilder:scaffold:imports
)

var setupLog = ctrl.Log.WithName("setup")

type Controller string

const (
	ClusterController       Controller = "cluster"
	WorkflowController      Controller = "workflow"
	ConfigChangeController  Controller = "configchange"
	NetworkController       Controller = "network"
	NetworkPeerController   Controller = "networkpeer"
	BRController            Controller = "br"
	BackupController        Controller = "backup"        // BackupController is the new backup controller
	CompactBackupController Controller = "compactbackup" // CompactBackupController is the compact backup controller
	RestoreController       Controller = "restore"       // RestoreController is the new restore controller
	CDCController           Controller = "cdc"
	ImportController        Controller = "import"
	DMController            Controller = "dm"
	DWController            Controller = "dw"
	DumplingController      Controller = "dumpling"
	ReplayController        Controller = "replay"

	// K8sVersionController is the UpgradeOperation controller only for cloud native arch.
	K8sVersionController Controller = "k8sversion"
	// UOController is the UpgradeOperation controller only for 3-layer arch.
	UOController Controller = "upgradeoperation"
)

// availableControllers is used to generate commandline argument help doc
var availableControllers = []string{
	string(ClusterController),
	string(WorkflowController),
	string(ConfigChangeController),
	string(NetworkController),
	string(NetworkPeerController),
	string(K8sVersionController),
	string(BRController),
	string(BackupController),
	string(CompactBackupController),
	string(RestoreController),
	string(CDCController),
	string(ImportController),
	string(DMController),
	string(DWController),
	string(DumplingController),
	string(UOController),
	string(ReplayController),
}

func controllerEnabled(controller Controller) bool {
	if enabledControllers == nil {
		switch controller {
		case ClusterController, ConfigChangeController, WorkflowController:
			return enableCMControllers
		case NetworkController, NetworkPeerController:
			return enableNetworkControllers
		case K8sVersionController:
			return enableK8sVersionController
		case UOController:
			return enableUOController
		case ImportController, DMController:
			return enableDPControllers
		case BRController, CDCController, DWController, DumplingController:
			// br, cdc and dw, dumpling controller are enabled by cobra subcommand
			return false
		case BackupController, RestoreController:
			// new backup and restore controller should only run if defined in enabledControllers
			return false
		default:
			panic("unknown controller")
		}
	}
	for _, c := range enabledControllers {
		if string(controller) == c {
			return true
		}
	}
	return false
}

func initProxyAuth(config pkgconfig.RootConfig) (func(r *http.Request) error, error) {
	// ignore auth if the dev mode is enabled
	if config.Network.Proxy.EnableDevMode {
		setupLog.Info("dev mode is enabled for cluster-proxy, disable auth0 M2M")
		return nil, nil
	}

	proxyAuth0ClientId, ok1 := os.LookupEnv("PROXY_AUTH0_CLIENT_ID")
	proxyAuth0ClientSecret, ok2 := os.LookupEnv("PROXY_AUTH0_CLIENT_SECRET")
	if ok1 && ok2 {
		// verify auth0 credentials and warmup token cache
		_, err := auth.GetClientToken(config.Network.Proxy.Auth0Domain, proxyAuth0ClientId, proxyAuth0ClientSecret, config.Network.Proxy.Auth0Audience)
		if err != nil {
			return nil, err
		}
		pdAuth := auth.NewAuth0Auth(config.Network.Proxy.Auth0Domain, proxyAuth0ClientId, proxyAuth0ClientSecret, config.Network.Proxy.Auth0Audience)
		return pdAuth, nil
	}

	return nil, nil
}

func initAWSClientSet() (aws.Client, error) {
	cfg, err := config.LoadDefaultConfig(context.TODO())
	if err != nil {
		return nil, err
	}
	return aws.NewClientSet(cfg), nil
}

func validateAuditConfig(overrideCfg pkgconfig.OverrideConfig, config pkgconfig.AuditConfig) error {
	img := sdk.OverrideImageWithRepoPrefix(overrideCfg.OverrideRegistry, overrideCfg.OverrideRepoPrefix, config.SidecarImage)
	if img == "" {
		return errors.New("audit config `audit.sidecarImage` not set")
	}

	cmd := config.SidecarCommand
	if len(cmd) == 0 {
		return errors.New("audit config `audit.sidecarCommand` not set")
	}

	return nil
}

// override RootConfig with command line flags
func overrideRootConfigWithFlags(rc *pkgconfig.RootConfig) {
	if region != "" {
		rc.Network.Region = region
		if rc.Predicate.MatchLabels != nil {
			rc.Predicate.MatchLabels[cloudv1.RegionLabel] = region
		}
	}
	if zones != nil {
		rc.Network.Zones = zones
	}
	if assumeRoleName != "" {
		rc.FullManage.AssumeRoleName = assumeRoleName
	}
	if readonlyAssumeRoleName != "" {
		rc.FullManage.ReadonlyAssumeRoleName = readonlyAssumeRoleName
	}
	if awsClusterServerRoleARN != "" {
		rc.FullManage.AWSClusterServerRoleARN = awsClusterServerRoleARN
	}
	if gcpClusterServerSAName != "" {
		rc.FullManage.GCPClusterServerSAName = gcpClusterServerSAName
	}
	if overrideRegistry != "" {
		rc.Override.OverrideRegistry = overrideRegistry
	}
	if overrideHelperImage != "" {
		rc.Override.OverrideHelperImage = overrideHelperImage
	}
}

// mustBuildRootConfig parse predicates and region provider from root config
func mustBuildRootConfig() pkgconfig.RootConfig {
	// set logger first to avoid log lost
	opts := zap.Options{
		Development: true,
		TimeEncoder: zapcore.RFC3339TimeEncoder,
	}
	ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))

	rootConfig, err := pkgconfig.LoadRootConfig(rootConfig)
	if err != nil {
		setupLog.Error(err, "unable to load root config")
		os.Exit(1)
	}

	overrideRootConfigWithFlags(&rootConfig)

	// Currently, admin nodes' instance type is fixed to t3.medium,
	// so set `BOTTLEROCKET_x86_64` as default AMI type.
	if rootConfig.Network.AdminNodesAMIType == "" {
		rootConfig.Network.AdminNodesAMIType = "BOTTLEROCKET_x86_64"
	}

	if rootConfig.Sentry.DSN != "" {
		sentryZapOpt, err := sentry.AddSentrytoLogger(rootConfig.Sentry.DSN, rootConfig.Sentry.Environment)
		if err != nil {
			setupLog.Error(err, "sentry init error")
			os.Exit(1)
		}
		opts.ZapOpts = append(opts.ZapOpts, sentryZapOpt)
		// reset logger with sentry integration
		ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
	}

	if rootConfig.Hibernation.CheckIdleInterval.Duration == 0 {
		rootConfig.Hibernation.CheckIdleInterval.Duration = 5 * time.Minute
	}
	if rootConfig.Hibernation.IdleTimeThreshold.Duration == 0 {
		rootConfig.Hibernation.IdleTimeThreshold.Duration = 24 * time.Hour
	}

	// br default
	if rootConfig.BR.CRRFDaemon == nil {
		rootConfig.BR.CRRFDaemon = &pkgconfig.HelmConfig{
			ChartPath: "/etc/helm/crrf-daemon",
		}
	}

	setupLog.Info("hibernation-config",
		"enabled", rootConfig.Hibernation.Enabled,
		"checkIdleInterval", rootConfig.Hibernation.CheckIdleInterval,
		"idleTimeThreshold", rootConfig.Hibernation.IdleTimeThreshold,
		"featureGate", rootConfig.FeatureGates[pkgconfig.NetworkHibernation],
	)

	predicates := rootConfig.Predicate
	var ok bool
	var region string

	if predicates.MatchLabels == nil {
		setupLog.Info("no predicate labels are configured")
		os.Exit(1)
	}
	if region, ok = predicates.MatchLabels[cloudv1.RegionLabel]; !ok {
		setupLog.Info("no region predicate are configured")
		os.Exit(1)
	}
	if _, ok = predicates.MatchLabels[cloudv1.ProviderTypeLabel]; !ok {
		setupLog.Info("no provider type predicate are configured")
		os.Exit(1)
	}
	if rootConfig.Network.Region == "" {
		setupLog.Error(err, "region information is required")
		os.Exit(1)
	}
	provider := predicates.MatchLabels[cloudv1.ProviderTypeLabel]

	if err := validateGKEMaintenanceExclusionConfig(&rootConfig); err != nil {
		setupLog.Info(err.Error())
		os.Exit(1)
	}

	setupLog.Info("check root config passed", "provider", provider, "region", region)
	return rootConfig
}

func validateGKEMaintenanceExclusionConfig(rootCfg *pkgconfig.RootConfig) error {
	// GKE Maintenance Exclusion
	startTime := rootCfg.Network.GKEMaintenanceExclusionTimeWindow.StartTime
	endTime := rootCfg.Network.GKEMaintenanceExclusionTimeWindow.EndTime
	if startTime == nil && endTime == nil {
		return nil
	} else if startTime == nil && endTime != nil {
		return fmt.Errorf("no startTime in gkeMaintenanceExclusionTimeWindow")
	} else if startTime != nil && endTime == nil {
		return fmt.Errorf("no endTime in gkeMaintenanceExclusionTimeWindow")
	}

	// 30 day
	maxDuration, err := time.ParseDuration("720h")
	if err != nil {
		return err
	}
	if !endTime.After(*startTime) {
		return fmt.Errorf("endTime in gkeMaintenanceExclusionTimeWindow must after startTime")
	}
	if endTime.Sub(*startTime) > maxDuration {
		return fmt.Errorf("GKE Maintenance Exclusion length must not exceed 30 days")
	}

	return nil
}

// mustBuildManager builds a new manager and returns it.
func mustBuildManager(controllerName string, region string, cs map[client.Object]cache.ByObject, cacheWithoutObjects []client.Object, dryRun bool) ctrl.Manager {
	infraAPIConfig, err := clientcmd.BuildConfigFromFlags("", infraAPIKubeconfig)
	if err != nil {
		setupLog.Error(err, "unable to load infra api kubeconfig")
		os.Exit(1)
	}
	// add default QPS and Burst if not set for keep same behavior as before
	if infraAPIConfig.QPS == 0.0 {
		infraAPIConfig.QPS = 20.0
		infraAPIConfig.Burst = 30.0
	}
	metricsOpt := metricsserver.Options{
		BindAddress: metricsAddr,
	}
	if pprofActive {
		metricsOpt.ExtraHandlers = map[string]http.Handler{
			"/debug/pprof/":        http.HandlerFunc(pprof.Index),
			"/debug/pprof/cmdline": http.HandlerFunc(pprof.Cmdline),
			"/debug/pprof/profile": http.HandlerFunc(pprof.Profile),
			"/debug/pprof/symbol":  http.HandlerFunc(pprof.Symbol),
			"/debug/pprof/trace":   http.HandlerFunc(pprof.Trace),
		}
	}
	leaderElectID := fmt.Sprintf("%s.%s.tidbcloud.com", controllerName, region)
	utilruntime.PanicHandlers = append(utilruntime.PanicHandlers, func(ctx context.Context, _ any) {
		metrics.ControllerPanic.WithLabelValues(controllerName, region).Inc()
	})
	mgr, err := ctrl.NewManager(infraAPIConfig, ctrl.Options{
		Scheme:  scheme.Scheme,
		Metrics: metricsOpt,
		WebhookServer: webhook.NewServer(webhook.Options{
			Port: 9443,
		}),
		HealthProbeBindAddress: probeAddr,
		LeaderElection:         enableLeaderElection,
		LeaderElectionID:       leaderElectID,
		// Only load objects to local cache if it matches the label selector.
		Cache: cache.Options{
			ByObject: cs,
		},
		Controller: runtimeConfig.Controller{
			RecoverPanic: ptr.To(true),
		},
		Client: client.Options{
			DryRun: ptr.To(dryRun),
			Cache: &client.CacheOptions{
				DisableFor: cacheWithoutObjects,
			},
		},
	})
	if err != nil {
		setupLog.Error(err, "unable to new manager")
		os.Exit(1)
	}
	return mgr
}

func buildHealthChecker(mgr ctrl.Manager) healthz.Checker {
	// Cached client would still return ok if failed to access infra apiserver
	// So here we use the un-cached client
	restCfg := mgr.GetConfig()
	kcli, err := client.New(restCfg, client.Options{})
	if err != nil {
		setupLog.Error(err, "unable to create infra api client")
		os.Exit(1)
	}
	checker := func(_ *http.Request) error {
		ns := &corev1.Namespace{}
		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		defer cancel()
		if err := kcli.Get(ctx, types.NamespacedName{Name: "default"}, ns); err != nil {
			return errors.Errorf("failed to access infra apiserver %v", err)
		}
		return nil
	}
	return checker
}

func buildReadyzChecker(mgr ctrl.Manager) healthz.Checker {
	// We already check infra-api accessibility in healthz checker
	// So here we use the cached client to do application logic check
	// The list operation should be lightweight with cached client
	// Note: the cached client can't work until the manager starts successfully
	kcli := mgr.GetClient()
	checker := func(_ *http.Request) error {
		networkList := &cloudv1.NetworkList{}
		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		defer cancel()
		if err := kcli.List(ctx, networkList, &client.ListOptions{}); err != nil {
			return errors.Errorf("failed to list network %v", err)
		}
		return nil
	}
	return checker
}

func mustRunManager(ctx context.Context, mgr ctrl.Manager) {
	//+kubebuilder:scaffold:builder
	if err := mgr.AddHealthzCheck("healthz", buildHealthChecker(mgr)); err != nil {
		setupLog.Error(err, "unable to set up health check")
		os.Exit(1)
	}
	if err := mgr.AddReadyzCheck("readyz", buildReadyzChecker(mgr)); err != nil {
		setupLog.Error(err, "unable to set up ready check")
		os.Exit(1)
	}

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