package main

import (
	"context"
	"fmt"
	"net"
	"net/http"
	"net/http/pprof"
	"os"
	"runtime/debug"

	"github.com/getsentry/sentry-go"
	grpcmiddleware "github.com/grpc-ecosystem/go-grpc-middleware"
	grpcrecovery "github.com/grpc-ecosystem/go-grpc-middleware/recovery"
	grpcvalidator "github.com/grpc-ecosystem/go-grpc-middleware/validator"
	grpcprometheus "github.com/grpc-ecosystem/go-grpc-prometheus"
	"github.com/pingcap/errors"
	"github.com/pingcap/log"
	"github.com/prometheus/client_golang/prometheus/promhttp"
	"github.com/spf13/cobra"
	"github.com/tidbcloud/chaos-svc/idl/pbgen/proto/chaos"
	"github.com/tidbcloud/chaos-svc/migrate"
	"github.com/tidbcloud/chaos-svc/pkg/infra/config"
	"github.com/tidbcloud/chaos-svc/pkg/infra/controller"
	pbutil "github.com/tidbcloud/core-svc/idl/pbgen/util"
	"github.com/tidbcloud/pkgv2/bizerr"
	grpcbizerr "github.com/tidbcloud/pkgv2/grpc/middleware/bizerr"
	grpcsentry "github.com/tidbcloud/pkgv2/grpc/middleware/sentry"
	pkglog "github.com/tidbcloud/pkgv2/log"
	"github.com/tidbcloud/pkgv2/logid"
	"go.uber.org/zap"
	"google.golang.org/grpc"
	"google.golang.org/grpc/metadata"
	"gorm.io/gorm"
)

const (
	defaultConfigPath = "/etc/chaos-svc/config.yaml"

	allRole         = "all"
	envChaosSvcRole = "CHAOS_SVC_ROLE"
	apiServiceRole  = "API_SERVICE"
	workerRole      = "WORKER"
)

var (
	configPath string
)

func main() {
	cobra.OnInitialize(func() {
		log.Info("OnInitialize", zap.String("configPath", configPath))
		err := config.InitConfig(configPath)
		if err != nil {
			log.Fatal("Initialize configuration failed", zap.Error(err))
		}

		flushFn, err := setup()
		if err != nil {
			log.Fatal("setup failed", zap.Error(err))
		}
		defer flushFn()
		log.Info("setup finished")
	})

	rootCmd := newRootCmd()
	if err := rootCmd.Execute(); err != nil {
		log.Fatal("serve return err", zap.Error(err))
	}
}

func newRootCmd() *cobra.Command {
	cmd := &cobra.Command{
		Use:               "chaos-svc",
		CompletionOptions: cobra.CompletionOptions{DisableDefaultCmd: true},
		Run: func(cmd *cobra.Command, args []string) {
			cobra.CheckErr(serve())
		},
	}
	cmd.PersistentFlags().StringVar(&configPath, "config-path", defaultConfigPath, "dbaas configuration file path")
	return cmd
}

func setup() (func(), error) {
	if err := setupLogger(); err != nil {
		return nil, fmt.Errorf("initialize logger failed: %w", err)
	}
	if err := setupPrometheus(); err != nil {
		return nil, fmt.Errorf("setup prometheus failed: %w", err)
	}

	setupDI()
	if enableWorker() {
		// TODO: worker do the DB migration
		if err := controller.DiCall(func(db *gorm.DB) error {
			return migrate.RunDBMigrate(db)
		}); err != nil {
			panic(fmt.Sprintf("do DB migration failed: %s", err))
		}
		log.Info("migrate finished")
		setupCronWorker()
	}

	// flush all buffering log
	flushFn := func() {
		if err := log.L().Sync(); err != nil {
			fmt.Println(err.Error())
		}
	}
	return flushFn, nil
}

func serve() error {
	svr, err := setupGrpcServer()
	if err != nil {
		return fmt.Errorf("setupGrpcServer failed: %s", err)
	}
	grpcprometheus.Register(svr)

	setupMetricsAndPprof()

	if enableApiService() {
		chaos.RegisterChaosServiceServer(svr, &controller.ChaosController{})
	}

	listener, err := net.Listen("tcp", config.Global().ServerConfig.GrpcAddr)
	if err != nil {
		return fmt.Errorf("create listener failed: %s", err)
	}
	log.S().Infof("listen on: %s", config.Global().ServerConfig.GrpcAddr)
	return svr.Serve(listener)
}

func panicHandler(ctx context.Context, p interface{}) error {
	logger := pkglog.WithContextL(ctx)
	err, ok := p.(error)
	if ok && errors.HasStack(err) {
		logger.Sugar().Warnf("panic, err: %s, stack: %s", err, errors.GetStackTracer(err).StackTrace())
	} else {
		logger.Sugar().Warnf("panic, err: %s, stack: %s", p, debug.Stack())
	}

	// TODO(chenfei): opt, Should replaced by grpc_sentry.UnaryServerInterceptor
	// report panic to sentry
	sentry.CurrentHub().Recover(err)
	return bizerr.New(bizerr.ErrCodePanic, "panic with: %s", p)
}

func LogidUnaryServerInterceptor() grpc.UnaryServerInterceptor {
	return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
		// try to get requestid from ctx for compatible with grpc-gateway
		md, ok := metadata.FromIncomingContext(ctx)
		if ok {
			if vals := md.Get(logid.LogidHTTPHeaderKey); len(vals) == 1 {
				ctx = logid.WithLogid(ctx, vals[0])
			}
		}

		ctx = injectCtxInfoIntoCtx(ctx, req)
		return handler(ctx, req)
	}
}

func injectCtxInfoIntoCtx(ctx context.Context, req interface{}) context.Context {
	if req == nil {
		return ctx
	}

	requestid := pbutil.GetTagsFromReq(req)[logid.GRPCHeaderKey]
	if requestid != "" {
		ctx = logid.WithLogid(ctx, requestid)
	}
	return ctx
}

func setupGrpcServer() (*grpc.Server, error) {
	opts := []grpcrecovery.Option{
		grpcrecovery.WithRecoveryHandlerContext(panicHandler),
	}
	return grpc.NewServer(
		grpc.UnaryInterceptor(
			grpcmiddleware.ChainUnaryServer(
				grpcbizerr.UnaryServerInterceptor(func(resp interface{}, err bizerr.BizError) (interface{}, error) {
					pbutil.SetCodeMsg(resp, int64(err.ErrCode()), err.ErrMsg())
					return resp, nil
				}),
				// otelgrpc.UnaryServerInterceptor(),
				LogidUnaryServerInterceptor(),
				grpcsentry.UnaryServerInterceptor(),
				grpcvalidator.UnaryServerInterceptor(),
				// grpctrace.UnaryServerInterceptor(),
				grpcprometheus.UnaryServerInterceptor,
				grpcrecovery.UnaryServerInterceptor(opts...),
			),
		),
	), nil
}

func setupMetricsAndPprof() {
	// Start metrics server.
	mux := http.NewServeMux()
	mux.Handle("/metrics", promhttp.Handler())
	setupPprofHandler(mux)
	go func() {
		err := http.ListenAndServe(config.Global().ServerConfig.HttpAddr, mux)
		if err != nil {
			panic(fmt.Sprintf("metrics server died %+v", err))
		}
	}()
}

func setupPprofHandler(mux *http.ServeMux) {
	mux.HandleFunc("/debug/pprof/", pprof.Index)
	mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
	mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
	mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
	mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
}

func setupLogger() error {
	cfg := &log.Config{
		Level: config.Global().ServerConfig.LogLevel,
	}
	gl, props, err := log.InitLogger(cfg)
	if err != nil {
		return fmt.Errorf("init logger failed: %w", err)
	}
	log.ReplaceGlobals(gl, props)

	return nil
}

func setupDI() {
	controller.InitDependencies()
}

func setupPrometheus() error {
	grpcprometheus.EnableHandlingTimeHistogram()
	return nil
}

func setupCronWorker() {
	if err := controller.DiCall(func(w *controller.GlobalWorker) {
		w.Start()
	}); err != nil {
		panic(fmt.Sprintf("start cron worker failed: %s", err))
	}
}

func getRoleFromEnv() string {
	role := os.Getenv(envChaosSvcRole)
	if role == "" {
		role = allRole
	}
	return role
}

func enableApiService() bool {
	role := getRoleFromEnv()
	return role == allRole || role == apiServiceRole
}

func enableWorker() bool {
	role := getRoleFromEnv()
	return role == allRole || role == workerRole
}
