package main

import (
	"context"
	"flag"
	"fmt"
	"log"
	"os"

	"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-node/config"
	"git.frostfs.info/TrueCloudLab/frostfs-node/internal/logs"
	"git.frostfs.info/TrueCloudLab/frostfs-node/misc"
	"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/control"
	"go.uber.org/zap"
)

const (
	// SuccessReturnCode returns when application closed without panic.
	SuccessReturnCode = 0
)

// prints err to standard logger and calls os.Exit(1).
func fatalOnErr(err error) {
	if err != nil {
		log.Fatal(err)
	}
}

// prints err with details to standard logger and calls os.Exit(1).
func fatalOnErrDetails(details string, err error) {
	if err != nil {
		log.Fatal(fmt.Errorf("%s: %w", details, err))
	}
}

func main() {
	configFile := flag.String("config", "", "path to config")
	configDir := flag.String("config-dir", "", "path to config directory")
	versionFlag := flag.Bool("version", false, "frostfs node version")
	dryRunFlag := flag.Bool("check", false, "validate configuration and exit")
	flag.Parse()

	if *versionFlag {
		fmt.Print(misc.BuildInfo("FrostFS Storage node"))

		os.Exit(SuccessReturnCode)
	}

	appCfg := config.New(*configFile, *configDir, config.EnvPrefix)

	err := validateConfig(appCfg)
	fatalOnErr(err)

	if *dryRunFlag {
		return
	}

	c := initCfg(appCfg)

	var ctx context.Context
	ctx, c.ctxCancel = context.WithCancel(context.Background())

	initApp(ctx, c)

	c.setHealthStatus(control.HealthStatus_STARTING)

	bootUp(ctx, c)

	c.setHealthStatus(control.HealthStatus_READY)

	wait(c)
}

func initAndLog(c *cfg, name string, initializer func(*cfg)) {
	c.log.Info(fmt.Sprintf("initializing %s service...", name))
	initializer(c)
	c.log.Info(fmt.Sprintf("%s service has been successfully initialized", name))
}

func initApp(ctx context.Context, c *cfg) {
	c.wg.Add(1)
	go func() {
		c.signalWatcher(ctx)
		c.wg.Done()
	}()

	metrics, _ := metricsComponent(c)
	initAndLog(c, "profiler", initProfilerService)
	initAndLog(c, metrics.name, metrics.init)

	initAndLog(c, "tracing", func(c *cfg) { initTracing(ctx, c) })

	initLocalStorage(c)

	initAndLog(c, "storage engine", func(c *cfg) {
		fatalOnErr(c.cfgObject.cfgLocalStorage.localStorage.Open())
		fatalOnErr(c.cfgObject.cfgLocalStorage.localStorage.Init(ctx))
	})

	initAndLog(c, "gRPC", initGRPC)
	initAndLog(c, "netmap", func(c *cfg) { initNetmapService(ctx, c) })
	initAndLog(c, "accounting", func(c *cfg) { initAccountingService(ctx, c) })
	initAndLog(c, "container", func(c *cfg) { initContainerService(ctx, c) })
	initAndLog(c, "session", initSessionService)
	initAndLog(c, "notification", func(c *cfg) { initNotifications(ctx, c) })
	initAndLog(c, "object", initObjectService)
	initAndLog(c, "tree", initTreeService)
	initAndLog(c, "control", initControlService)

	initAndLog(c, "morph notifications", func(c *cfg) { listenMorphNotifications(ctx, c) })
}

func runAndLog(ctx context.Context, c *cfg, name string, logSuccess bool, starter func(context.Context, *cfg)) {
	c.log.Info(fmt.Sprintf("starting %s service...", name))
	starter(ctx, c)

	if logSuccess {
		c.log.Info(fmt.Sprintf("%s service started successfully", name))
	}
}

func stopAndLog(c *cfg, name string, stopper func() error) {
	c.log.Debug(fmt.Sprintf("shutting down %s service", name))

	err := stopper()
	if err != nil {
		c.log.Debug(fmt.Sprintf("could not shutdown %s server", name),
			zap.String("error", err.Error()),
		)
	}

	c.log.Debug(fmt.Sprintf("%s service has been stopped", name))
}

func bootUp(ctx context.Context, c *cfg) {
	runAndLog(ctx, c, "NATS", true, connectNats)
	runAndLog(ctx, c, "gRPC", false, func(_ context.Context, c *cfg) { serveGRPC(c) })
	runAndLog(ctx, c, "notary", true, makeAndWaitNotaryDeposit)

	bootstrapNode(c)
	startWorkers(ctx, c)
}

func wait(c *cfg) {
	c.log.Info(logs.CommonApplicationStarted,
		zap.String("version", misc.Version))

	<-c.done // graceful shutdown

	c.log.Debug(logs.FrostFSNodeWaitingForAllProcessesToStop)

	c.wg.Wait()
}

func (c *cfg) onShutdown(f func()) {
	c.closers = append(c.closers, closer{"", f})
}