package main

import (
	"flag"
	"net/http"
	_ "net/http/pprof" //nolint: gosec // internal service
	"os"
	"os/signal"
	"path/filepath"
	"syscall"

	"github.com/go-logr/zapr"
	"github.com/prometheus/client_golang/prometheus/promhttp"
	"github.com/tidbcloud/aws-shared-provider/pkg/gateway"
	"github.com/tidbcloud/aws-shared-provider/pkg/gateway/config"
	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
	"k8s.io/client-go/rest"
	"k8s.io/client-go/tools/clientcmd"
	ctrl "sigs.k8s.io/controller-runtime"
	zapopt "sigs.k8s.io/controller-runtime/pkg/log/zap"
)

var (
	addr                          string
	probeAddr                     string
	metricsAddr                   string
	tlsCA                         string
	tlsCert                       string
	tlsKey                        string
	tlsVersion                    string
	clusterTLSCA                  string
	clusterTLSCert                string
	clusterTLSKey                 string
	sqlTlsCA                      string
	sqlTlsCert                    string
	sqlTlsKey                     string
	infraRedisClientCertDir       string
	requireBackendTLS             bool
	enableCompression             bool
	awsSDKLogMode                 string
	infraRedisAddr                string
	infraClusterTable             string
	otsInstanceName               string
	region                        string
	infraKubeconfig               string
	cloudProviderType             string
	kubeClusterName               string
	gatewayMode                   string
	developMode                   bool
	trafficAllowList              config.TrafficIPAllowListConfig
	tlsOnly                       bool
	tlsOnlySkipUserPrefixes       config.TrafficIPAllowListConfig
	maxIdleSeconds                uint
	e2eEnv                        bool
	enableZeroBackend             bool
	pushTrafficMetricsAddr        string
	pushTrafficMetricsInterval    int
	tidbServerVersion             string
	onlyForZR                     bool
	gracefulShutdownTimeout       int
	gracefulWaitBeforeCloseServer int
	enableInfraMysqlClient        bool
	infraRdsSecretArn             string
	cacheServerAddr               string
	frontendReadTimeout           int
)

func getZapLoggerAndParse() *zap.Logger {
	lvl := zap.NewAtomicLevelAt(zap.FatalLevel)
	opt := zapopt.Options{
		Development:     true,
		StacktraceLevel: &lvl,
		TimeEncoder:     zapcore.ISO8601TimeEncoder,
	}
	opt.BindFlags(flag.CommandLine)
	flag.Parse()
	return zapopt.NewRaw(zapopt.UseFlagOptions(&opt))
}

func init() {
	flag.StringVar(&addr, "addr", ":3306", "listening address")
	flag.StringVar(&probeAddr, "probeAddr", ":8081", "probe listening address")
	flag.StringVar(&metricsAddr, "metricsAddr", ":9090", "metrics listening address")
	flag.StringVar(&tlsCA, "tls-ca", "", "TLS CA file")
	flag.StringVar(&tlsCert, "tls-cert", "", "TLS cert file")
	flag.StringVar(&tlsKey, "tls-key", "", "TLS key file")
	flag.StringVar(&tlsVersion, "tls-version", "", "Minimal TLS version (TLSv1.0/TLSv1.1/TLSv1.2/TLSv1.3)")
	flag.StringVar(&clusterTLSCA, "cluster-tls-ca", "", "Cluster TLS CA file")
	flag.StringVar(&clusterTLSCert, "cluster-tls-cert", "", "Cluster TLS cert file")
	flag.StringVar(&clusterTLSKey, "cluster-tls-key", "", "Cluster TLS key file")
	flag.StringVar(&sqlTlsCA, "sql-tls-ca", "", "SQL TLS CA file")
	flag.StringVar(&sqlTlsCert, "sql-tls-cert", "", "SQL TLS cert file")
	flag.StringVar(&sqlTlsKey, "sql-tls-key", "", "SQL TLS key file")
	flag.StringVar(&infraRedisClientCertDir, "infra-redis-client-cert-dir", "/infra-redis-client-cert",
		"The infra redis client cert dir")
	flag.BoolVar(&requireBackendTLS, "require-backend-tls", false, "Require backend TLS")
	flag.BoolVar(&enableCompression, "compress", false, "Enable compression")
	flag.StringVar(&awsSDKLogMode, "aws-sdk-log-mode", "", "The AWS SDK log mode")
	flag.StringVar(&infraRedisAddr, "infra-redis-addr", "", "The infra redis addr in regional")
	flag.StringVar(&infraClusterTable, "infra-cluster-table", "", "The infra cluster table in dynamodb")
	flag.StringVar(&otsInstanceName, "ots-instance-name", "", "The alicloud tablestore instance name")
	flag.StringVar(&region, "region", "", "The region where serverless cluster in")
	flag.StringVar(&infraKubeconfig, "infra-kubeconfig", "", "kubeconfig for cluster hosted infra api")
	flag.StringVar(&cloudProviderType, "cloud-provider-type", "", "The cloud provider type")
	flag.StringVar(&kubeClusterName, "kube-cluster-name", "", "The name of current kubernetes cluster")
	flag.StringVar(&gatewayMode, "gateway-mode", "public", "The gateway mode, public/inner/privatelink")
	flag.BoolVar(&developMode, "develop", false, "Enable local develop mode")
	flag.Var(&trafficAllowList, "allow-traffic-ip", "DEPRECATED: devtier configuration, not used anymore")
	flag.BoolVar(&tlsOnly, "tls-only", false, "Enable TLS only")
	flag.Var(&tlsOnlySkipUserPrefixes, "tls-only-skip-user-prefix", "Users with these prefixes are exempt from TLS requirement")
	flag.UintVar(&maxIdleSeconds, "max-idle-seconds", 0, "Gateway specific max idle seconds")
	flag.BoolVar(&e2eEnv, "e2e-env", false, "Running in e2e environment")
	flag.BoolVar(&enableZeroBackend, "enable-zero-backend", true, "DEPRECATED: allways regions are true")
	flag.StringVar(&pushTrafficMetricsAddr, "push-traffic-metrics-addr", "", "Push traffic metrics address")
	flag.IntVar(&pushTrafficMetricsInterval, "push-traffic-metrics-interval", 0, "Push traffic metrics interval, in seconds") //nolint:lll // ignore
	flag.StringVar(&tidbServerVersion, "tidb-server-version", "", "TiDB server version")
	flag.BoolVar(&onlyForZR, "only-for-zr", false, "Only for redundant zone")
	flag.IntVar(&gracefulShutdownTimeout, "graceful-shutdown-timeout", 0, "DEPRECATED: migrated to graceful-wait-before-close-server")
	flag.IntVar(&gracefulWaitBeforeCloseServer, "graceful-wait-before-close-server", 0, "Graceful wait before close tiproxy server, in seconds")
	flag.BoolVar(&enableInfraMysqlClient, "enable-infra-mysql-client", false, "Enable infra mysql client")
	flag.StringVar(&infraRdsSecretArn, "infra-rds-secret-arn", "", "The ARN of the secret for infra RDS")
	flag.StringVar(&cacheServerAddr, "cache-server-addr", "", "The address of the cache server")
	flag.IntVar(&frontendReadTimeout, "frontend-read-timeout", 0, "Frontend read timeout in seconds")
}

func main() {
	zapLogger := getZapLoggerAndParse()
	ctrl.SetLogger(zapr.NewLogger(zapLogger))

	if infraRedisAddr == "" {
		ctrl.Log.Info("infra-redis-addr must set")
		return
	}
	if infraClusterTable == "" {
		ctrl.Log.Info("infra-cluster-table must set")
		return
	}
	if region == "" {
		ctrl.Log.Info("region must set")
		return
	}
	if gatewayMode != config.PublicMode && gatewayMode != config.InnerMode && gatewayMode != config.PrivatelinkMode {
		ctrl.Log.Info("invalid gateway-mode", "gateway-mode", gatewayMode)
		return
	}

	var infraConfig, localConfig *rest.Config

	if !developMode {
		var err error
		infraConfig, err = clientcmd.BuildConfigFromFlags("", infraKubeconfig)
		if err != nil {
			ctrl.Log.Error(err, "build infra-kubeconfig")
			return
		}
		localConfig, err = rest.InClusterConfig()
		if err != nil {
			ctrl.Log.Error(err, "build local kubeconfig")
			return
		}
	}

	ctrl.Log.Info("initializing gateway", "addr", addr, "probAddr", probeAddr,
		"using TLS", tlsCA+tlsCert+tlsKey != "",
		"infra-redis-addr", infraRedisAddr,
		"infra-cluster-table", infraClusterTable,
		"kube-cluster-name", kubeClusterName,
		"infra-redis-client-cert-dir", infraRedisClientCertDir,
		"gateway-mode", gatewayMode,
		"only-for-zr", onlyForZR,
		"graceful-shutdown-timeout", gracefulShutdownTimeout,
		"frontend-read-timeout", frontendReadTimeout,
	)

	tlsConfig := config.TLSConfig{CA: tlsCA, Cert: tlsCert, Key: tlsKey, MinVersion: tlsVersion}
	clusterTLSConfig := config.TLSConfig{CA: clusterTLSCA, Cert: clusterTLSCert, Key: clusterTLSKey}
	sqlTLSConfig := config.TLSConfig{CA: sqlTlsCA, Cert: sqlTlsCert, Key: sqlTlsKey}
	infraRedisClientTLSConfig := config.TLSConfig{
		CA:   filepath.Join(infraRedisClientCertDir, "ca.crt"),
		Cert: filepath.Join(infraRedisClientCertDir, "tls.crt"),
		Key:  filepath.Join(infraRedisClientCertDir, "tls.key"),
	}

	gw, err := gateway.New(&config.Config{
		Address:                    addr,
		TLS:                        tlsConfig,
		ClusterTLS:                 clusterTLSConfig,
		SQLTLS:                     sqlTLSConfig,
		InfraRedisClientTLSConfig:  infraRedisClientTLSConfig,
		RequireBackendTLS:          requireBackendTLS,
		EnableCompression:          enableCompression,
		AWSSDKLogMode:              awsSDKLogMode,
		InfraRedisAddr:             infraRedisAddr,
		InfraClusterTable:          infraClusterTable,
		OtsInstanceName:            otsInstanceName,
		Region:                     region,
		InfraConfig:                infraConfig,
		LocalConfig:                localConfig,
		KubeClusterName:            kubeClusterName,
		CloudProviderType:          cloudProviderType,
		GatewayMode:                gatewayMode,
		DevelopMode:                developMode,
		TrafficIPAllowList:         trafficAllowList,
		TLSOnly:                    tlsOnly,
		TLSOnlySkipUserPrefixes:    tlsOnlySkipUserPrefixes,
		MaxIdleSeconds:             maxIdleSeconds,
		E2EEnv:                     e2eEnv,
		PushTrafficMetricsAddr:     pushTrafficMetricsAddr,
		PushTrafficMetricsInterval: pushTrafficMetricsInterval,
		TiDBServerVersion:          tidbServerVersion,
		OnlyForZR:                  onlyForZR,
		EnableInfraMysqlClient:     enableInfraMysqlClient,
		InfraRdsSecretArn:          infraRdsSecretArn,
		CacheServerAddr:            cacheServerAddr,
		FrontendReadTimeout:        frontendReadTimeout,
	}, zapLogger)
	if err != nil {
		ctrl.Log.Error(err, "failed to create gateway")
		return
	}
	err = gw.StartServe()
	if err != nil {
		ctrl.Log.Error(err, "failed to start gateway")
		return
	}

	http.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {})
	http.HandleFunc("/readyz", func(w http.ResponseWriter, r *http.Request) {
		if gw.IsServing() {
			w.WriteHeader(http.StatusOK)
		} else {
			w.WriteHeader(http.StatusServiceUnavailable)
		}
	})
	go http.ListenAndServe(probeAddr, nil) //nolint:errcheck // skip error check

	if metricsAddr != "" {
		mux := http.NewServeMux()
		mux.Handle("/metrics", promhttp.Handler())
		go http.ListenAndServe(metricsAddr, mux) //nolint:errcheck // skip error check
	}

	sigs := make(chan os.Signal, 1)
	signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
	sig := <-sigs
	ctrl.Log.Info("received signal", "signal", sig)
	if gracefulWaitBeforeCloseServer > 0 {
		gw.WaitForConnectionsClose(gracefulWaitBeforeCloseServer)
	}
	gw.Stop()
}
