package main

import (
	"context"
	"flag"
	"fmt"
	"net"
	"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/tidbcloud/core/core_ctx/infra/config"
	"github.com/tidbcloud/core/core_ctx/infra/controller"
	"github.com/tidbcloud/idl/pbgen/proto/core"
	pbutil "github.com/tidbcloud/idl/pbgen/util"
	"github.com/tidbcloud/pkgv2/bizerr"
	grpcbizerr "github.com/tidbcloud/pkgv2/grpc/middleware/bizerr"
	grpcsentry "github.com/tidbcloud/pkgv2/grpc/middleware/sentry"
	grpctrace "github.com/tidbcloud/pkgv2/grpc/middleware/trace"
	pkglog "github.com/tidbcloud/pkgv2/log"
	"github.com/tidbcloud/pkgv2/logid"
	"github.com/tidbcloud/pkgv2/tracing"
	"github.com/tidbcloud/pkgv2/version"
	"github.com/tidbcloud/pkgv2/zapext/zapsentry"
	"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
	"google.golang.org/grpc"
)

const (
	defaultConfigPath = "/etc/dbaas/config.yaml"
)

type server struct {
}

func main() {
	var configPath string
	flag.StringVar(&configPath, "config-path", defaultConfigPath, "dbaas configuration file path")
	flag.Parse()

	err := config.InitConfig(configPath)
	if err != nil {
		log.Fatal("Initialize configuration failed", zap.Error(err))
	}

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

	err = s.serve()
	if err != nil {
		log.Fatal("serve return err", zap.Error(err))
	}
}

func (s *server) setup() (func(), error) {
	if err := setupSentry(); err != nil {
		return nil, fmt.Errorf("setupSentry failed: %w", err)
	}
	setupTracing()
	var level zapcore.Level
	if err := level.UnmarshalText([]byte(config.Global().ServerConfig.SentryConf.LogLevel)); err != nil {
		return nil, fmt.Errorf("invalid sentry.LogLevel: %s", config.Global().ServerConfig.SentryConf.LogLevel)
	}
	sentryOpt := zap.WrapCore(func(core zapcore.Core) zapcore.Core {
		return zapcore.NewTee(core, zapsentry.NewCore(level, sentry.CurrentHub().Client()))
	})

	if err := setupLogger(sentryOpt); 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()
	setupCronWorker()

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

func (s *server) serve() error {
	svr, err := setupGrpcServer()
	if err != nil {
		return fmt.Errorf("setupGrpcServer failed: %s", err)
	}
	grpcprometheus.Register(svr)
	core.RegisterCoreServiceServer(svr, &controller.CoreController{})

	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 setupLogger(opts ...zap.Option) error {
	cfg := &log.Config{
		Level: config.Global().ServerConfig.LogLevel,
	}
	gl, props, err := log.InitLogger(cfg, append(opts, zap.AddStacktrace(zapcore.ErrorLevel))...)
	if err != nil {
		return fmt.Errorf("init logger failed: %w", err)
	}
	log.ReplaceGlobals(gl, props)

	return nil
}

func setupSentry() error {
	cfg := config.Global().ServerConfig.SentryConf
	opts := sentry.ClientOptions{
		Dsn:         cfg.DSN,
		Environment: cfg.Environment,
	}
	if err := sentry.Init(opts); err != nil {
		log.Error("setupSentry failed", zap.String("dsn", cfg.DSN), zap.String("env", cfg.Environment))
		return err
	}
	return nil
}

func setupTracing() {
	tracing.InitTracer(log.L(), &tracing.TraceInfo{
		ServiceName: "core",
		Version:     version.GitHash,
		TraceToken:  os.Getenv("LS_ACCESS_TOKEN"),
	})
}

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

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 setupGrpcServer() (*grpc.Server, error) {
	opts := []grpcrecovery.Option{
		grpcrecovery.WithRecoveryHandlerContext(panicHandler),
	}
	return grpc.NewServer(
		//grpc.Creds(credentials.NewServerTLSFromCert(&insecure.Cert)),

		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(),
				logid.UnaryServerInterceptor(),
				grpcsentry.UnaryServerInterceptor(),
				grpcvalidator.UnaryServerInterceptor(),
				grpctrace.UnaryServerInterceptor(),
				grpcprometheus.UnaryServerInterceptor,
				grpcrecovery.UnaryServerInterceptor(opts...),
			),
		),
	), nil
}

func setupDI() {
	controller.InitDependencies()
}

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