package main

import (
	"context"
	"flag"
	"fmt"
	"net/http"
	"strings"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/pingcap/log"
	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/promhttp"
	"github.com/robfig/cron/v3"
	"github.com/tidbcloud/ecosystem-service/api/middleware"
	"github.com/tidbcloud/ecosystem-service/api/v1/vercel"
	"github.com/tidbcloud/pkgv2/ctxinfo"
	"github.com/tidbcloud/pkgv2/logid"
	"github.com/tidbcloud/tiinf/errs"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"github.com/tidbcloud/tiinf/monitor/titracing"
	"github.com/tidbcloud/tiinf/sdk/auditlog"
	"github.com/tidbcloud/tiinf/sdk/cors"
	"github.com/tidbcloud/tiinf/transport/http/tigin"

	"go.uber.org/zap"
)

const (
	defaultConfigPath    = "/etc/ecosystem-service/config.yaml"
	internalErrorMessage = "We are currently facing internal issues. Our team is actively working to resolve this as quickly as possible. Please try again later."
)

var (
	swagHandlerVercel  gin.HandlerFunc
	swagHandlerGit     gin.HandlerFunc
	swagHandlerConnect gin.HandlerFunc

	durationHistogram = prometheus.NewHistogramVec(
		prometheus.HistogramOpts{
			Namespace: "ecosystem_service",
			Name:      "http_request_duration_seconds",
			Help:      "The HTTP request latencies in seconds.",
			Buckets:   []float64{0.001, 0.01, 0.05, 0.1, 0.5, 1, 5, 15, 60},
		}, []string{"code", "method", "handler", "host", "url"},
	)
	counter = prometheus.NewCounterVec(
		prometheus.CounterOpts{
			Namespace: "ecosystem_service",
			Name:      "http_request_total",
			Help:      "The HTTP request counter.",
		},
		[]string{"code", "method", "handler", "host", "url"},
	)
)

func init() {
	prometheus.MustRegister(durationHistogram)
	prometheus.MustRegister(counter)
}

func main() {
	configPath := flag.String("config-path", defaultConfigPath, "ecosystem configuration file path")
	flag.Parse()

	fmt.Println(*configPath)
	var server *Server
	server, err := initializeServer(*configPath)
	if err != nil {
		log.Fatal("Failed to init server", zap.Error(err))
	}

	if _, err := titracing.InitJaeger("ecosystem-service"); err != nil {
		log.Fatal("initialize jaeger failed", zap.Error(err))
	}

	err = server.recorder.Migrator.RunMigrate()
	if err != nil {
		log.Fatal("Failed to migrate", zap.Error(err))
	}

	// Every 30 minutes, check if there are any deleted clusters that associated connections need to be deleted
	c := cron.New(cron.WithChain(cron.SkipIfStillRunning(cron.DefaultLogger), cron.Recover(cron.DefaultLogger)))
	_, err = c.AddFunc("*/30 * * * *", server.autoCleanJob.AutoCleanConnections)
	if err != nil {
		log.Fatal("Add auto clean cron job failed", zap.Error(err))
	}

	// Every minute, collect the metrics of the vercel connections.
	_, err = c.AddFunc("*/1 * * * *", server.vercelMetricsJob.CollectMetrics)
	if err != nil {
		log.Fatal("Add vercel metrics cron job failed", zap.Error(err))
	}

	// Every 10 minutes, check if there are any deleted clusters that associated git connections need to be deleted
	_, err = c.AddFunc("*/10 * * * *", server.gitCronJob.AutoClean)
	if err != nil {
		log.Fatal("Add git auto clean cron job failed", zap.Error(err))
	}

	err = server.eventBus.Eventbus.Run()
	if err != nil {
		log.Fatal("Failed to run eventbus", zap.Error(err))
	}

	if server.conf.BigQueryConfig.Enable {
		// Every day, upload the vercel connections to bigquery.
		_, err = c.AddFunc("0 0 * * *", server.uploadJob.UploadToBigquery)
		if err != nil {
			log.Fatal("Add bigquery upload cron job failed", zap.Error(err))
		}
	}

	if server.conf.ServerConfig.EnableAuditLog {
		if err := auditlog.InitAuditLogSDK(context.TODO(), time.Second); err != nil {
			log.Fatal("Initialize audit log sdk failed", zap.Error(err))
		}
	} else {
		log.Info("audit log is disabled")
	}

	c.Start()
	defer c.Stop()

	route := tigin.Default()
	route.Use(gin.CustomRecovery(recordPanicFunc))
	route.Use(cors.GinNew(middleware.GetCorsOptions(server.conf.ServerConfig.Project)))
	initAuth(route, server)
	route.Use(ctxinfo.ExtractCtxInfoMW, logid.InjectLogid4GRPCGatewayMW)

	initGinMetrics(route)
	route.GET("/metrics", prometheusHandler())

	// vercel api
	v1 := route.Group("/ecosystem/api/v1")
	{
		// User API
		v1.POST("/orgs/:org_id/projects/:project_id/clusters/:cluster_id/vercel/connections", server.vercelController.AddClusterConnection)
		v1.DELETE("/orgs/:org_id/projects/:project_id/clusters/:cluster_id/vercel/connections", server.vercelController.DeleteClusterConnection)
		v1.GET("/orgs/:org_id/projects/:project_id/clusters/:cluster_id/vercel/connections", server.vercelController.ListClusterConnectionsByCluster)
		v1.GET("/vercel/integration/configurations/:configuration_id/connections", server.vercelController.ListVercelConnectionsByIntegrationConfiguration)
		v1.GET("/vercel/integration/configurations/:configuration_id/projects", server.vercelController.ListVercelProjectsByIntegrationConfiguration)
		v1.GET("/orgs/:org_id/vercel/connections", server.vercelController.ListVercelConnectionsByOrg)
		v1.POST("/vercel/access_token", server.vercelController.ExchangeIntegrationAccessToken)
		v1.POST("/orgs/:org_id/projects/:project_id/serverless/clusters", server.vercelController.CreateServerlessCluster)
		v1.DELETE("/orgs/:org_id/projects/:project_id/dataapps/:data_app_id/vercel/connections", server.vercelController.DeleteDataAppConnection)
		v1.POST("/orgs/:org_id/projects/:project_id/dataapps/:data_app_id/vercel/connections", server.vercelController.AddDataAppVercelConnection)

		// External API
		v1.POST("/vercel/webhooks", server.vercelController.Webhook)
	}

	// connect api
	{
		v1.GET("/connect/info", server.connectController.GetConnectInfo)
	}

	// connect internal api
	// use open api key authentication
	internal := route.Group("/ecosystem/api/internal")
	{
		internal.GET("/connect/info", server.connectController.GetConnectInfo)
		internal.POST("/pingchat/chat", server.pingChatController.Chat)
	}

	// gitops api
	{
		v1.POST("/integration/github/webhooks", server.gitController.GitHubWebHook)
		v1.POST("/orgs/:org_id/projects/:project_id/clusters/:cluster_id/git/connections", server.gitController.AddGitConnections)
		v1.DELETE("/orgs/:org_id/projects/:project_id/clusters/:cluster_id/git/connections", server.gitController.DeleteGitConnections)
		v1.GET("/orgs/:org_id/projects/:project_id/git/connections", server.gitController.ListGitConnections)
		v1.GET("/orgs/:org_id/projects/:project_id/clusters/:cluster_id/git/connections", server.gitController.GetGitConnections)
		v1.GET("/integration/github/accounts", server.gitController.GetGitHubInstallAccounts)
		v1.GET("/integration/github/exchange-token", server.gitController.ExchangeGitHubToken)
		v1.GET("/integration/github/search-repo-v2", server.gitController.GetGitHubRepoV2)

	}

	// swagger
	if swagHandlerVercel != nil {
		route.GET("/swagger/vercel/*any", swagHandlerVercel)
	}
	if swagHandlerGit != nil {
		route.GET("/swagger/git/*any", swagHandlerGit)
	}
	if swagHandlerConnect != nil {
		route.GET("/swagger/connect/*any", swagHandlerConnect)
	}
	log.Info("Starting ecosystem-service")

	if err := route.Run(":8080"); err != nil {
		log.Fatal("run server error", zap.Error(err))
	}
}

func initGinMetrics(route *gin.Engine) {
	route.Use(middleware.GinMetricsHandler(urlLabelMappingFn, durationHistogram, counter))
}

func urlLabelMappingFn(c *gin.Context) string {
	url := c.Request.URL.Path
	for _, p := range c.Params {
		switch p.Key {
		case "cluster_id":
			url = strings.Replace(url, p.Value, ":cluster_id", 1)
		case "org_id":
			url = strings.Replace(url, p.Value, ":org_id", 1)
		case "configuration_id":
			url = strings.Replace(url, p.Value, ":configuration_id", 1)
		case "project_id":
			url = strings.Replace(url, p.Value, ":project_id", 1)
		case "data_app_id":
			url = strings.Replace(url, p.Value, ":data_app_id", 1)
		}
	}
	return url
}

func initAuth(route *gin.Engine, server *Server) {
	log.Info("Init Auth ...")
	route.Use(
		middleware.AuthenticationHandler(),
		middleware.AuthorizationHandler(server.mgmtSvc),
	)
}

func prometheusHandler() gin.HandlerFunc {
	h := promhttp.Handler()

	return func(c *gin.Context) {
		h.ServeHTTP(c.Writer, c.Request)
	}
}

func recordPanicFunc(c *gin.Context, err interface{}) {
	url := urlLabelMappingFn(c)
	counter.WithLabelValues("500", c.Request.Method, c.HandlerName(), c.Request.Host, url).Inc()
	tilog.Error(c.Request.Context(), "panic triggered", zap.String("panic", fmt.Sprintf("%v", err)), zap.String("url", url))
	if strings.HasPrefix(url, "/ecosystem/api/internal") || strings.EqualFold(url, "/ecosystem/api/v1/vercel/webhooks") ||
		strings.EqualFold(url, "/ecosystem/api/v1/integration/github/webhooks") {
		c.JSON(http.StatusInternalServerError, gin.H{"error": internalErrorMessage})
	} else {
		c.JSON(http.StatusOK, vercel.NewBaseResp(errs.ErrCodeInternalServerError, internalErrorMessage))
	}
}
