// Statik generated files are deprecated. We just keep the generated statik.go files for local test.
//go:generate statik -src=../../ui/portal -ns=portal -dest=../../pkg
//go:generate statik -src=../../ui/dashboard/build -ns=dashboard -dest=../../pkg/dashboard

package main

import (
	"context"
	"crypto/rsa"
	"crypto/x509"
	"encoding/pem"
	"flag"
	"fmt"
	"net"
	"net/http"
	"os"
	"time"

	ccFiles "github.com/baurine/cc-files"
	"github.com/gin-contrib/gzip"
	"github.com/gin-contrib/pprof"
	"github.com/gin-gonic/gin"
	gocache "github.com/patrickmn/go-cache"
	"github.com/pingcap/errors"
	"github.com/pingcap/log"
	swaggerfiles "github.com/swaggo/files"
	ginSwagger "github.com/swaggo/gin-swagger"
	"go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"
	"go.opentelemetry.io/otel"
	sdktrace "go.opentelemetry.io/otel/sdk/trace"
	"go.opentelemetry.io/otel/trace"
	"go.uber.org/zap"
	"google.golang.org/grpc"

	"github.com/pingcap-inc/clinic/docs"
	"github.com/pingcap-inc/clinic/pkg/account_sdk"
	"github.com/pingcap-inc/clinic/pkg/api"
	"github.com/pingcap-inc/clinic/pkg/api/ops"
	auth2 "github.com/pingcap-inc/clinic/pkg/auth"
	"github.com/pingcap-inc/clinic/pkg/auth/oauth"
	session2 "github.com/pingcap-inc/clinic/pkg/auth/session"
	"github.com/pingcap-inc/clinic/pkg/cache"
	"github.com/pingcap-inc/clinic/pkg/cloudevent"
	"github.com/pingcap-inc/clinic/pkg/compute_engine"
	"github.com/pingcap-inc/clinic/pkg/config"
	"github.com/pingcap-inc/clinic/pkg/datasource"
	"github.com/pingcap-inc/clinic/pkg/datasource/clustersearch"
	"github.com/pingcap-inc/clinic/pkg/db"
	"github.com/pingcap-inc/clinic/pkg/es"
	mwmetrics "github.com/pingcap-inc/clinic/pkg/middlewares/metrics"
	"github.com/pingcap-inc/clinic/pkg/o11y"
	"github.com/pingcap-inc/clinic/pkg/proxy"
	"github.com/pingcap-inc/clinic/pkg/ratelimit"
	"github.com/pingcap-inc/clinic/pkg/rbac"
	"github.com/pingcap-inc/clinic/pkg/resource_limit"
	"github.com/pingcap-inc/clinic/pkg/serverlesscli"
	"github.com/pingcap-inc/clinic/pkg/storage/aws"
	"github.com/pingcap-inc/clinic/pkg/storage/iface"
	"github.com/pingcap-inc/clinic/pkg/stream"
	"github.com/pingcap-inc/clinic/pkg/task_manager"
	clinictracing "github.com/pingcap-inc/clinic/pkg/tracing"
	"github.com/pingcap-inc/clinic/pkg/upload_file/decrypt"
	manager2 "github.com/pingcap-inc/clinic/pkg/upload_file/manager"
)

var cfgPath string

// @title			Clinic Example API
// @version		1.0
// @description	This is a Clinic server.
// @BasePath		/clinic/api/v1
func main() {
	flag.StringVar(&cfgPath, "c", "./", "server -c=/path/to/config.yaml")
	flag.Parse()

	cfg, err := config.InitConfig(cfgPath)
	if err != nil {
		log.Fatal("init config failed", zap.Error(err))
	}
	// load the dashboard config and panic on error
	if err != cfg.LoadDashboard() {
		log.Fatal("load dashboard config failed", zap.Error(err))
	}

	initTelemetry()

	var accountCli *account_sdk.Client
	log.Info("tidb-cloud-account", zap.String("endpoint", cfg.DBaasConfig.AccountSvcAddr))
	if cfg.DBaasConfig.AccountSvcAddr != "" {
		cli, err := account_sdk.New(cfg.DBaasConfig.AccountSvcAddr, account_sdk.TimeoutOption(2*time.Second))
		if err != nil {
			log.Fatal("init account client failed", zap.Error(err))
		}

		accountCli = cli
	}

	err = db.Initialize(cfg.DBConfig, false)
	if err != nil {
		log.Error("init db failed", zap.Error(err))
		panic(err)
	}
	clustersearch.StartLargeOrgTextSearchCache(context.Background())

	err = datasource.Initialize(cfg.DBaasConfig, accountCli)
	if err != nil {
		log.Error("init dbaascore failed", zap.Error(err))
		panic(err)
	}

	rbacAuth, err := rbac.InitAuth(cfg.DBConfig, cfg.ServerConfig.RbacModelPath)
	if err != nil {
		log.Fatal("init rbac failed", zap.Error(err))
	}
	oauth.NewOAuthService(cfg.ServerConfig.OAuthConfigs, accountCli, rbacAuth)

	// TODO: read port form config
	lis, err := net.Listen("tcp", ":10000")
	if err != nil {
		log.Fatal("failed to listen: %v", zap.Error(err))
	}

	s := grpc.NewServer()
	rbac.RegisterCasbinServer(s, rbacAuth)

	go func() {
		if err := s.Serve(lis); err != nil {
			log.Fatal("failed to serve: %v", zap.Error(err))
		}
	}()

	if err := cache.KibanaCache.Init(); err != nil {
		log.Fatal("init kibana cache failed", zap.Error(err))
	}

	allowCIDRs := make([]*net.IPNet, 0)
	stream.OfSlice(cfg.AllowCIDRS).Map(func(cidr string) *net.IPNet {
		_, ipNet, err := net.ParseCIDR(cidr)
		if err != nil {
			log.Error("parse cidr failed", zap.String("cidr", cidr), zap.Error(err))
			return nil
		}

		return ipNet
	}).Filter(func(ipNet *net.IPNet) bool {
		return ipNet != nil
	}).CollectEmptySlice(&allowCIDRs)
	// Deal with backlog at boot stage
	// processBacklog(cfg)

	session := session2.NewSessionManager(cfg.ServerConfig.SessionConfig, allowCIDRs, cfg.DisableIPWhitelist)

	serverAuth := &auth2.Auth{
		Manager:       session,
		AllowCidrs:    allowCIDRs,
		AllowEmptyIP:  cfg.DisableIPWhitelist,
		UseAPIGateway: cfg.UseAPIGateWay,
	}
	// set request logging to avoid noisy logging
	engine := gin.New()
	engine.Use(
		otelgin.Middleware("clinic-api"),
		traceIDHeaderMiddleware(),
		gin.LoggerWithConfig(gin.LoggerConfig{
			SkipPaths: []string{
				"/clinic/api/v1/data/metrics",
				"/clinic/api/v1/data/log/filebeat-*/_search",
				"/clinic/api/v1/data/log/_search/scroll",
				"/metrics",
				"/api/ping",
				"/grafana/api/live/ws",
			},
			Formatter: func(param gin.LogFormatterParams) string {
				sc := trace.SpanContextFromContext(param.Request.Context())
				tid := ""
				if sc.HasTraceID() {
					tid = sc.TraceID().String()
				}
				return fmt.Sprintf("[GIN] %s | %3d | %s | %s |%-7s %s | trace_id=%s\n",
					param.TimeStamp.Format("2006/01/02 - 15:04:05"),
					param.StatusCode,
					param.Latency,
					param.ClientIP,
					param.Method,
					param.Path,
					tid,
				)
			},
		}),
		gin.Recovery(),
		mwmetrics.NewAPIMetricsGinMiddleware("clinic-api"),
	)

	uig := engine.Group("")
	uig.Use(gzip.Gzip(gzip.DefaultCompression))
	uig.StaticFS("/portal", ccFiles.Assets())
	uig.GET("/", func(c *gin.Context) {
		c.Redirect(http.StatusMovedPermanently, "/portal")
		c.Abort()
	})

	pprof.Register(engine)

	users := make(map[string]string)
	for _, u := range cfg.ServerConfig.Accounts.Users {
		users[u.Name] = u.Password
	}

	admins := make(map[string]string)
	for _, a := range cfg.ServerConfig.Accounts.Admins {
		admins[a.Name] = a.Password
		users[a.Name] = a.Password
	}
	compute_engine.GlobalCMClient = compute_engine.NewCMClient(cfg)

	esClient := es.NewESClient(cfg.SinkConfig.LogConfig)

	quotalimit := resource_limit.NewQuotaLimiter()
	cronJob := resource_limit.NewCron(*cfg)
	if !cfg.DisableInNewDeployment {
		cronJob.Start()
	}
	engine.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerfiles.Handler, ginSwagger.InstanceName(docs.SwaggerInfodefault_server_api.InstanceName())))

	if cfg.LinkTiDBCloud {
		serverlessC, err := serverlesscli.NewClient(cfg.DBaasConfig.ServerlessEndpoint)
		if err != nil {
			panic(err)
		}
		if !cfg.DisableInNewDeployment {
			cloudevent.StartEventHandler(cfg, serverlessC)
		}
	}

	priv, err := loadPrivateKey(cfg.FileStorage.RSAPrivateKey)
	if err != nil {
		log.Error("load private key failed", zap.Error(err))
		panic(err)
	}

	storage, err := newStorage(cfg.FileStorage)
	if err != nil {
		log.Error("init storage failed", zap.Error(err))
		panic(err)
	}
	backdoorAPI, err := ops.NewBackdoorAPI(rbacAuth)
	if err != nil {
		log.Error("new backdoor api failed", zap.Error(err))
		panic(err)
	}

	taskManagerCli := task_manager.NewTaskManagerClient(cfg.TaskManagerConfig)
	decryptor := decrypt.NewDecrypt(priv, storage, nil, nil) // split importer
	rateLimiter := ratelimit.NewRateLimiterMiddleware(cfg.ServerConfig.APILimitConfigs)

	// construct proxy handler ahead
	metricProxyHandler := proxy.NewTiDBClusterMetricHandler(rbacAuth, &cfg.SinkConfig.MetricConfig.SelectAddr, cfg.CentralO11yConfig.Env)
	o11yProxyHandler := proxy.NewO11yProxyHandler(rbacAuth, cfg.CentralO11yConfig.Env)
	cloudNodeMetricandler := proxy.NewCloudNodeMetricHandler(rbacAuth, o11yProxyHandler, cfg.CentralO11yConfig.Env)
	projectMetricHandler := proxy.NewProjectMetricHandler(rbacAuth, cfg.CentralO11yConfig.Env)
	if cfg.EnableO11yCanary {
		if err := cfg.CentralO11yConfig.Validate(); err != nil {
			log.Fatal("central o11y config invalid", zap.Error(err))
		}
		o11ySharedOpts := []o11y.Option{
			o11y.TimeoutOption(40 * time.Second),
			o11y.MaxIdleConnOption(10),
		}
		o11yCli, err := o11y.New(
			cfg.CentralO11yConfig.GSEndpoint,
			cfg.CentralO11yConfig.DSEndpointTPL,
			cfg.CentralO11yConfig.ClientId,
			cfg.CentralO11yConfig.ClientSecret,
			cfg.CentralO11yConfig.Env,
			append(o11ySharedOpts, o11y.ByocDSEndpointOption(cfg.CentralO11yConfig.ByocDSEndpoint))...,
		)
		if err != nil {
			log.Fatal("init central o11y client failed", zap.Error(err))
		}
		// Second O11y client is optional, used to connect to the o11y dev server
		if cfg.SecondO11yConfig != nil && cfg.SecondO11yConfig.Validate() == nil {
			log.Info("init second o11y client", zap.String("endpoint", cfg.SecondO11yConfig.GSEndpoint))
			o11yCli2, err := o11y.New(
				cfg.SecondO11yConfig.GSEndpoint,
				cfg.SecondO11yConfig.DSEndpointTPL,
				cfg.SecondO11yConfig.ClientId,
				cfg.SecondO11yConfig.ClientSecret,
				cfg.SecondO11yConfig.Env,
				o11y.TimeoutOption(40*time.Second),
				o11y.MaxIdleConnOption(10),
				o11y.ByocDSEndpointOption(cfg.SecondO11yConfig.ByocDSEndpoint),
			)
			if err != nil {
				log.Error("init second o11y client failed", zap.Error(err))
			}
			o11yCli.SetSecondO11yClient(o11yCli2)
		}

		cloudNodeMetricandler.SetO11y(o11yCli)
		projectMetricHandler.SetO11y(o11yCli)
		metricProxyHandler.SetO11y(o11yCli)
		o11yProxyHandler.SetO11y(o11yCli)
		o11yProxyHandler.SetClusterInfoCache(gocache.New(gocache.NoExpiration, gocache.NoExpiration))
	}

	if err := api.NewAPIService(&api.APIServiceParams{
		Gin:                engine,
		Ctx:                context.Background(),
		Session:            session,
		Cfg:                cfg,
		Rbac:               rbacAuth,
		Auth:               serverAuth,
		EsClient:           esClient,
		QuotaLimiter:       quotalimit,
		RateLimiter:        rateLimiter,
		Decrypt:            decryptor,
		Manager:            manager2.NewUploadFileManager(storage),
		TaskManagerCli:     taskManagerCli,
		MetricProxyHandler: metricProxyHandler,
		O11yProxyHandler:   o11yProxyHandler,
	}).Start(); err != nil {
		log.Fatal("can not start server", zap.Error(err))
	}

	api.InitExternalAPI(engine, serverAuth, quotalimit, rateLimiter, cfg)
	api.InitSystemAPI(engine)
	api.InitHealthCheckApi(engine)
	api.InitMetricsApi(engine)
	ops.InitBackdoorApi(engine, backdoorAPI)

	api.InitProxy(
		engine,
		session,
		rbacAuth,
		cfg.SinkConfig,
		metricProxyHandler,
		o11yProxyHandler,
		cloudNodeMetricandler,
		projectMetricHandler,
	)

	// Some client constructors require http.DefaultTransport to remain an
	// *http.Transport while applying connection-pool options. Instrument it
	// only after all API clients have been constructed.
	clinictracing.InstrumentDefaultHTTPTransport()

	// api.RegisterForbiddenMiddleware(engine)

	if cfg.ServerConfig.CertFile == "" || cfg.ServerConfig.KeyFile == "" {
		log.Panic("start server failed", zap.Error(engine.Run(cfg.ServerConfig.Addr)))
	}

	log.Panic("start server with tls failed", zap.Error(engine.RunTLS(cfg.ServerConfig.Addr, cfg.ServerConfig.CertFile, cfg.ServerConfig.KeyFile)))
}

func newStorage(cfg config.FileStorage) (iface.IStorage, error) {
	switch cfg.StorageType() {
	case config.AWSStorageType:
		return aws.NewAWSStorage(cfg)
	default:
		return nil, errors.Errorf("unsupported storage type %s", cfg.StorageType())
	}
}

func loadPrivateKey(kpath string) (*rsa.PrivateKey, error) {
	data, err := os.ReadFile(kpath)
	if err != nil {
		return nil, err
	}
	block, _ := pem.Decode(data)
	if block == nil {
		return nil, fmt.Errorf("decode pem failed")
	}
	priv, err := x509.ParsePKCS1PrivateKey(block.Bytes)
	if err != nil {
		return nil, fmt.Errorf("failed to parse certificate: %s", err.Error())
	}
	return priv, nil
}

func initTelemetry() {
	tp := sdktrace.NewTracerProvider(
		sdktrace.WithSampler(sdktrace.ParentBased(sdktrace.TraceIDRatioBased(0.1))),
	)
	otel.SetTracerProvider(tp)
	clinictracing.ConfigurePropagation()
}

func traceIDHeaderMiddleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		sc := trace.SpanContextFromContext(c.Request.Context())
		if sc.HasTraceID() {
			c.Writer.Header().Set("X-Trace-ID", sc.TraceID().String())
		}
		c.Next()
		if !sc.HasTraceID() {
			sc = trace.SpanContextFromContext(c.Request.Context())
			if sc.HasTraceID() {
				c.Writer.Header().Set("X-Trace-ID", sc.TraceID().String())
			}
		}
	}
}
