package main

import (
	"context"
	"crypto/tls"
	"embed"
	"io/fs"
	"net/http"
	"time"
slog "go.unistack.org/micro/v4/logger/slog"
	appconfig "git.unistack.org/unistack-org/pkgdash/internal/config"
	"git.unistack.org/unistack-org/pkgdash/internal/database"
	"git.unistack.org/unistack-org/pkgdash/internal/handler"
	"git.unistack.org/unistack-org/pkgdash/internal/storage"
	_ "git.unistack.org/unistack-org/pkgdash/internal/storage/sqlite"
	"git.unistack.org/unistack-org/pkgdash/internal/worker"
	pb "git.unistack.org/unistack-org/pkgdash/proto"
	jsoncodec "go.unistack.org/micro-codec-json/v4"
	jsonpbcodec "go.unistack.org/micro-codec-jsonpb/v4"
	yamlcodec "go.unistack.org/micro-codec-yaml/v4"
	envconfig "go.unistack.org/micro-config-env/v4"
	fileconfig "go.unistack.org/micro-config-file/v4"
	vaultconfig "go.unistack.org/micro-config-vault/v4"
	victoriameter "go.unistack.org/micro-meter-victoriametrics/v4"
	httpsrv "go.unistack.org/micro-server-http/v4"
	healthhandler "go.unistack.org/micro-server-http/v4/handler/health"
	meterhandler "go.unistack.org/micro-server-http/v4/handler/meter"
	spahandler "go.unistack.org/micro-server-http/v4/handler/spa"
	swaggerui "go.unistack.org/micro-server-http/v4/handler/swagger-ui"
	"go.unistack.org/micro/v4"
	"go.unistack.org/micro/v4/config"
	"go.unistack.org/micro/v4/logger"
	"go.unistack.org/micro/v4/meter"
	"go.unistack.org/micro/v4/options"
	"go.unistack.org/micro/v4/server"
	rutil "go.unistack.org/micro/v4/util/reflect"
)

const appName = "pkgdash"

var (
	BuildDate  string = "now"    // filled when build
	AppVersion string = "latest" // filled when build
)

//go:generate rm -rf assets
//go:generate mkdir assets
//go:generate cp -vr ../../ui/dist/ui assets/
//go:embed assets/*
var assets embed.FS

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	logger.DefaultLogger = slog.NewLogger(logger.WithLevel(logger.DebugLevel))
	if err := logger.DefaultLogger.Init(); err != nil {
		logger.Fatal(ctx, "failed to init logger")
	}

	cfg := appconfig.NewConfig(appName, AppVersion) // create new empty config
	vc := vaultconfig.NewConfig(
		config.AllowFail(true),              // that may be not exists
		config.Struct(cfg),                  // load from vault
		options.Codec(jsoncodec.NewCodec()), // vault config in json
		config.BeforeLoad(func(ctx context.Context, c config.Config) error {
			return c.Init(
				vaultconfig.HTTPClient(&http.Client{
					Transport: &http.Transport{
						TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
					},
				}),
				vaultconfig.Address(cfg.Vault.Addr),
				vaultconfig.Timeout(5*time.Second),
				vaultconfig.Token(cfg.Vault.Token),
				vaultconfig.Path(cfg.Vault.Path),
			)
		}),
	)

	if err := config.Load(ctx,
		[]config.Config{
			config.NewConfig( // load from defaults
				config.Struct(cfg), // pass config struct
			),
			fileconfig.NewConfig( // load from file
				config.AllowFail(true),              // that may be not exists
				config.Struct(cfg),                  // pass config struct
				options.Codec(yamlcodec.NewCodec()), // file config in json
				fileconfig.Path("./local.yaml"),     // nearby file
			),
			envconfig.NewConfig( // load from environment
				config.Struct(cfg), // pass config struct
			),
			vc,
		}, config.LoadOverride(true),
	); err != nil {
		logger.Fatal(ctx, "failed to load config: %v", err)
	}

	if err := config.Validate(ctx, cfg); err != nil {
		logger.Fatal(ctx, "failed to validate config: %v", err)
	}

	swaggerui.Config["url"] = "../service.swagger.yaml"

	meter.DefaultMeter = victoriameter.NewMeter(
		meter.Path(cfg.Meter.Path),
		meter.WriteFDMetrics(true),
		meter.WriteProcessMetrics(true),
		options.Address(cfg.Meter.Addr),
	)

	svc := micro.NewService()

	if err := svc.Init(
		micro.Server(httpsrv.NewServer()),
		micro.Name(cfg.Server.Name),
		micro.Version(cfg.Server.Version),
	); err != nil {
		logger.Fatal(ctx, "failed to init service: %v", err)
	}

	assetsUI, err := fs.Sub(assets, "assets/ui")
	if err != nil {
		logger.Fatal(ctx, "failed to get assets: %v", err)
	}

	if err := svc.Server("http").Init(
		options.Address(cfg.Server.Addr),
		options.Name(cfg.Server.Name),
		server.Version(cfg.Server.Version),
		options.Codecs("application/json", jsonpbcodec.NewCodec()),
		options.Address(cfg.Server.Addr),
		options.Context(ctx),
		httpsrv.PathHandler(http.MethodGet, "/ui/*", spahandler.Handler("/ui/", assetsUI)),
		httpsrv.PathHandler(http.MethodHead, "/ui/*", spahandler.Handler("/ui/", assetsUI)),
		httpsrv.PathHandler(http.MethodGet, "/swagger-ui/*", swaggerui.Handler("/swagger-ui")),
	); err != nil {
		logger.Fatal(ctx, "failed to init service: %v", err)
	}

	if err := database.ParseDSN(cfg.Database); err != nil {
		logger.Fatal(ctx, "failed to init database: %v", err)
	}

	db, err := database.Connect(ctx, cfg.Database, logger.DefaultLogger)
	if err != nil {
		logger.Fatal(ctx, "failed to connect database: %v", err)
	}

	store, err := storage.NewStorage(cfg.Database.Type, db)
	if err != nil {
		logger.Fatal(ctx, "failed to init storage: %v", err)
	}

	h, err := handler.NewHandler(store)
	if err != nil {
		logger.Fatal(ctx, "failed to create handler: %v", err)
	}

	log := logger.NewLogger(
		logger.WithLevel(logger.ParseLevel(cfg.Server.LoggerLevel)),
		logger.WithCallerSkipCount(3),
	)
	if err := svc.Init(micro.Logger(log)); err != nil {
		logger.Fatal(ctx, "failed to init service: %v", err)
	}

	if err := pb.RegisterPkgdashServiceServer(svc.Server("http"), h); err != nil {
		logger.Fatal(ctx, "failed to register handler: %v", err)
	}

	intsvc := httpsrv.NewServer(
		options.Codecs("application/json", jsoncodec.NewCodec()),
		options.Address(cfg.Meter.Addr),
		options.Context(ctx),
	)

	if err := intsvc.Init(); err != nil {
		logger.Fatal(ctx, "failed to init http srv: %v", err)
	}

	if err := healthhandler.RegisterHealthServiceServer(intsvc, healthhandler.NewHandler()); err != nil {
		logger.Fatal(ctx, "failed to set http handler: %v", err)
	}

	if err := meterhandler.RegisterMeterServiceServer(intsvc, meterhandler.NewHandler()); err != nil {
		logger.Fatal(ctx, "failed to set http handler: %v", err)
	}

	if err := intsvc.Start(); err != nil {
		logger.Fatal(ctx, "failed to run http srv: %v", err)
	}

	cw, err := vc.Watch(ctx, config.WatchCoalesce(true), config.WatchInterval(1*time.Second, 5*time.Second))
	if err != nil {
		logger.Fatal(ctx, "failed to watch config: %v", err)
	}

	defer func() {
		if err := cw.Stop(); err != nil {
			logger.Error(ctx, err.Error())
		}
	}()

	go func() {
		for {
			changes, err := cw.Next()
			if err != nil {
				logger.Error(ctx, "failed to get config update: %v", err)
			}
			for k, v := range changes {
				if err = rutil.SetFieldByPath(cfg, v, k); err != nil {
					logger.Error(ctx, "failed to set config update: %v", err)
					break
				}
			}
			if err == nil {
				for k := range changes {
					switch k {
					case "Server.LoggerLevel":
						if lvl, ok := changes[k].(string); ok {
							logger.Info(ctx, "logger level changed to %s", lvl)
							logger.DefaultLogger.Level(logger.ParseLevel(lvl))
						}
					}
				}
			}
		}
	}()

	go func() {
		worker.Run(ctx, store, time.Duration(cfg.App.CheckInterval))
	}()

	if err = svc.Run(); err != nil {
		logger.Fatal(ctx, "failed to run svc: %v", err)
	}
}
