package main

import (
	"flag"
	"fmt"
	"net"
	"net/http"
	_ "net/http/pprof" // #nosec
	"net/url"
	"os"
	"os/signal"
	"runtime"
	"strconv"
	"syscall"
	"time"

	"github.com/Azure/go-autorest/autorest/azure"

	"github.com/Azure/secrets-store-csi-driver-provider-azure/pkg/metrics"
	"github.com/Azure/secrets-store-csi-driver-provider-azure/pkg/server"
	"github.com/Azure/secrets-store-csi-driver-provider-azure/pkg/utils"
	"github.com/Azure/secrets-store-csi-driver-provider-azure/pkg/version"

	"google.golang.org/grpc"
	"google.golang.org/grpc/health/grpc_health_v1"
	logsapi "k8s.io/component-base/logs/api/v1"
	json "k8s.io/component-base/logs/json"
	"k8s.io/klog/v2"
	k8spb "sigs.k8s.io/secrets-store-csi-driver/provider/v1alpha1"
)

const (
	readHeaderTimeout = 5 * time.Second
)

var (
	versionInfo   = flag.Bool("version", false, "prints the version information")
	endpoint      = flag.String("endpoint", "unix:///tmp/azure.sock", "CSI gRPC endpoint")
	logFormatJSON = flag.Bool("log-format-json", false, "set log formatter to json")
	enableProfile = flag.Bool("enable-pprof", false, "enable pprof profiling")
	profilePort   = flag.Int("pprof-port", 6060, "port for pprof profiling")

	healthzPort    = flag.Int("healthz-port", 8989, "port for health check")
	healthzPath    = flag.String("healthz-path", "/healthz", "path for health check")
	healthzTimeout = flag.Duration("healthz-timeout", 5*time.Second, "RPC timeout for health check")

	metricsBackend = flag.String("metrics-backend", "Prometheus", "Backend used for metrics")
	prometheusPort = flag.Int("prometheus-port", 8898, "Prometheus port for metrics backend")

	constructPEMChain              = flag.Bool("construct-pem-chain", true, "explicitly reconstruct the pem chain in the order: SERVER, INTERMEDIATE, ROOT")
	writeCertAndKeyInSeparateFiles = flag.Bool("write-cert-and-key-in-separate-files", false,
		"Write cert and key in separate files. The individual files will be named as <secret-name>.crt and <secret-name>.key. These files will be created in addition to the single file.")

	cloudName = flag.String("cloud-name", "AzurePublicCloud", "default cloud environment to use for Azure SDK if not provided in the SecretProviderClass. "+
		"Allowed values: AzurePublicCloud, AzureUSGovernmentCloud, AzureChinaCloud, AzureGermanCloud or AzureStackCloud")
)

func main() {
	klog.InitFlags(nil)
	defer klog.Flush()

	flag.Parse()

	signalChan := make(chan os.Signal, 1)
	signal.Notify(signalChan, syscall.SIGTERM, syscall.SIGINT, os.Interrupt)

	if *logFormatJSON {
		jsonFactory := json.Factory{}
		logger, _ := jsonFactory.Create(logsapi.LoggingConfiguration{Format: "json"})
		klog.SetLogger(logger)
	}

	if *versionInfo {
		if err := version.PrintVersion(); err != nil {
			klog.ErrorS(err, "failed to print version")
			os.Exit(1)
		}
		os.Exit(0)
	}
	klog.InfoS("Starting Azure Key Vault Provider", "version", version.BuildVersion)

	cloudEnv, err := azure.EnvironmentFromName(*cloudName)
	if err != nil {
		klog.ErrorS(err, "failed validating default cloud environment", "cloudName", *cloudName)
		os.Exit(1)
	}

	if *enableProfile {
		klog.InfoS("Starting profiling", "port", *profilePort)
		go func() {
			server := &http.Server{
				Addr:              fmt.Sprintf("%s:%d", "localhost", *profilePort),
				ReadHeaderTimeout: readHeaderTimeout,
			}
			klog.ErrorS(server.ListenAndServe(), "unable to start profiling server")
		}()
	}
	// initialize metrics exporter before creating measurements
	if err = metrics.InitMetricsExporter(*metricsBackend, *prometheusPort); err != nil {
		klog.ErrorS(err, "failed to initialize metrics exporter")
		os.Exit(1)
	}

	if *constructPEMChain {
		klog.Infof("construct pem chain feature enabled")
	}
	if *writeCertAndKeyInSeparateFiles {
		klog.Infof("write cert and key in separate files feature enabled")
	}

	// Initialize and run the gRPC server
	proto, addr, err := utils.ParseEndpoint(*endpoint)
	if err != nil {
		klog.ErrorS(err, "failed to parse endpoint")
		os.Exit(1)
	}

	if proto == "unix" {
		if runtime.GOOS != "windows" {
			addr = "/" + addr
		}
		if err := os.Remove(addr); err != nil && !os.IsNotExist(err) {
			klog.ErrorS(err, "failed to remove socket", "addr", addr)
			os.Exit(1)
		}
	}

	listener, err := net.Listen(proto, addr)
	if err != nil {
		klog.ErrorS(err, "failed to listen", "proto", proto, "addr", addr)
		os.Exit(1)
	}

	opts := []grpc.ServerOption{
		grpc.UnaryInterceptor(utils.LogInterceptor()),
	}
	s := grpc.NewServer(opts...)
	csiDriverProviderServer := server.New(*constructPEMChain, *writeCertAndKeyInSeparateFiles, cloudEnv)
	k8spb.RegisterCSIDriverProviderServer(s, csiDriverProviderServer)
	// Register the health service.
	grpc_health_v1.RegisterHealthServer(s, csiDriverProviderServer)

	klog.InfoS("Listening for connections", "address", listener.Addr())
	go s.Serve(listener)

	healthz := &server.HealthZ{
		HealthCheckURL: &url.URL{
			Host: net.JoinHostPort("", strconv.Itoa(*healthzPort)),
			Path: *healthzPath,
		},
		UnixSocketPath: listener.Addr().String(),
		RPCTimeout:     *healthzTimeout,
	}
	go healthz.Serve()

	<-signalChan
	// gracefully stop the grpc server
	klog.Infof("terminating the server")
	s.GracefulStop()
}
