package main

import (
	"context"
	"crypto/tls"
	"flag"
	"fmt"
	"net/http"
	"time"

	"github.com/gin-contrib/gzip"
	"github.com/gin-gonic/gin"
	"github.com/prometheus/client_golang/prometheus/promhttp"
	"github.com/robfig/cron/v3"
	"github.com/tidbcloud/aws-shared-provider/pkg/httpgateway"
	"github.com/tidbcloud/pkgv2/log"
	"github.com/tidbcloud/pkgv2/logid"
	"go.uber.org/zap"
	zap1 "sigs.k8s.io/controller-runtime/pkg/log/zap"
)

var (
	addr          string
	tlsCert       string
	tlsKey        string
	componentName string
	logLevel      string
	metricsAddr   string
	secret        string
)

const TlsCertReloadInterval = 12 * time.Hour

func main() {
	flag.StringVar(&addr, "addr", ":443", "listening address")
	flag.StringVar(&tlsCert, "tls-cert", "", "TLS cert file")
	flag.StringVar(&tlsKey, "tls-key", "", "TLS key file")
	flag.StringVar(&componentName, "component-name", "tidb-http-gateway", "component name")
	flag.StringVar(&logLevel, "log-level", "INFO", "log level")
	flag.StringVar(&metricsAddr, "metricsAddr", ":9090", "metrics listening address")
	flag.StringVar(&secret, "secret", "", "secret for cipher")
	flag.Parse()

	opt := zap1.Options{}
	opt.BindFlags(flag.CommandLine)
	logger := log.WithContextL(context.Background())

	logger.Info("initializing http gateway",
		zap.String("addr", addr),
		zap.String("tlsCert", tlsCert),
		zap.String("tlsKey", tlsKey),
		zap.String("componentName", componentName),
		zap.String("logLevel", logLevel))

	gw, err := httpgateway.New(componentName, logLevel, secret)
	if err != nil {
		logger.Error("failed to create http gateway", zap.Error(err))
		return
	}

	// run every 5 minute
	c := cron.New(cron.WithChain(cron.SkipIfStillRunning(cron.DefaultLogger), cron.Recover(cron.DefaultLogger)))
	_, err = c.AddFunc("0/5 * * * *", gw.GCSession)
	if err != nil {
		logger.Error("failed to add GCSession job", zap.Error(err))
		return
	}

	c.Start()
	defer c.Stop()

	route := gin.New()
	route.Use(
		// Exclude compression for proxy path to avoid double compression
		gzip.Gzip(gzip.DefaultCompression, gzip.WithExcludedPaths([]string{"/v1beta/sql/proxy"})),
		gin.LoggerWithConfig(gin.LoggerConfig{
			Formatter: logFormatterWithLogId,
			SkipPaths: []string{"/healthz"},
		}),
		gin.Recovery(),
	)

	route.GET("/healthz", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{})
	})
	beta := route.Group("/v1beta")
	{
		beta.POST("/sql", gw.ExecuteSQL)
		beta.POST("/sql/proxy", gw.ExecuteSQL)
	}

	logger.Info("starting metrics server")
	go runMetricsServer(metricsAddr, gw)

	cert, err := tls.LoadX509KeyPair(tlsCert, tlsKey)
	if err != nil {
		logger.Error("failed to load cert", zap.Error(err))
		return
	}
	logger.Info("starting reload cert goroutine")
	go reloadTlsCert(tlsCert, tlsKey, &cert)

	// start https server
	server := &http.Server{Addr: addr, Handler: route.Handler()}
	//nolint:gosec // ignore
	server.TLSConfig = &tls.Config{
		GetCertificate: func(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
			return &cert, nil
		},
	}
	// set certFile and keyFile to empty string to use GetCertificate callback
	if err = server.ListenAndServeTLS("", ""); err != nil {
		logger.Error("run https gateway error", zap.Error(err))
		return
	}
}

func reloadTlsCert(certFile, keyFile string, cert *tls.Certificate) {
	logger := log.WithContextL(context.Background())
	ticker := time.NewTicker(TlsCertReloadInterval)
	for range ticker.C {
		logger.Info("reload cert", zap.String("certFile", certFile), zap.String("keyFile", keyFile))
		reloadCert, err := tls.LoadX509KeyPair(certFile, keyFile)
		if err != nil {
			logger.Warn("failed to reload cert", zap.Error(err))
		}
		*cert = reloadCert
		logger.Info("reload cert success")
	}
}

func runMetricsServer(metricsAddr string, gateway *httpgateway.HttpGateway) {
	// register metrics
	httpgateway.RegisterMetrics(gateway)

	mux := http.NewServeMux()
	mux.Handle("/metrics", promhttp.Handler())
	err := http.ListenAndServe(metricsAddr, mux)
	if err != nil {
		panic(fmt.Sprintf("start metrics server failed %+v", err))
	}
}

var logFormatterWithLogId = func(param gin.LogFormatterParams) string {
	var statusColor, methodColor, resetColor string
	if param.IsOutputColor() {
		statusColor = param.StatusCodeColor()
		methodColor = param.MethodColor()
		resetColor = param.ResetColor()
	}

	if param.Latency > time.Minute {
		param.Latency = param.Latency.Truncate(time.Second)
	}
	return fmt.Sprintf("[GIN] [%s] %v |%s %3d %s| %13v | %15s |%s %-7s %s %#v\n%s",
		logid.GetLogid(param.Request.Context()),
		param.TimeStamp.Format("2006/01/02 - 15:04:05"),
		statusColor, param.StatusCode, resetColor,
		param.Latency,
		param.ClientIP,
		methodColor, param.Method, resetColor,
		param.Path,
		param.ErrorMessage,
	)
}
