package main

import (
	"context"
	"fmt"
	"log/slog"
	"os"
	"os/signal"
	"syscall"

	"github.com/spf13/cobra"

	"git.wntrmute.dev/mc/metacrypt/internal/audit"
	"git.wntrmute.dev/mc/metacrypt/internal/auth"
	"git.wntrmute.dev/mc/metacrypt/internal/barrier"
	"git.wntrmute.dev/mc/metacrypt/internal/config"
	"git.wntrmute.dev/mc/metacrypt/internal/db"
	"git.wntrmute.dev/mc/metacrypt/internal/engine"
	"git.wntrmute.dev/mc/metacrypt/internal/engine/ca"
	"git.wntrmute.dev/mc/metacrypt/internal/engine/sshca"
	"git.wntrmute.dev/mc/metacrypt/internal/engine/transit"
	"git.wntrmute.dev/mc/metacrypt/internal/engine/user"
	"git.wntrmute.dev/mc/metacrypt/internal/grpcserver"
	"git.wntrmute.dev/mc/metacrypt/internal/policy"
	"git.wntrmute.dev/mc/metacrypt/internal/seal"
	"git.wntrmute.dev/mc/metacrypt/internal/server"
)

var serverCmd = &cobra.Command{
	Use:   "server",
	Short: "Start the Metacrypt server",
	Long:  "Start the Metacrypt HTTPS server. The service starts in sealed state.",
	RunE:  runServer,
}

func init() {
	rootCmd.AddCommand(serverCmd)
}

func runServer(cmd *cobra.Command, args []string) error {
	logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))

	configPath := cfgFile
	if configPath == "" {
		configPath = "/srv/metacrypt/metacrypt.toml"
	}

	cfg, err := config.Load(configPath)
	if err != nil {
		return err
	}

	database, err := db.Open(cfg.Database.Path)
	if err != nil {
		return err
	}
	defer func() { _ = database.Close() }()

	if err := db.Migrate(database); err != nil {
		return err
	}

	// Create audit logger.
	auditLog, err := createAuditLogger(cfg)
	if err != nil {
		return err
	}

	b := barrier.NewAESGCMBarrier(database)
	sealMgr := seal.NewManager(database, b, auditLog, logger)

	if err := sealMgr.CheckInitialized(); err != nil {
		return err
	}

	authenticator, err := auth.NewAuthenticator(auth.Config{
		ServerURL:   cfg.MCIAS.ServerURL,
		CACert:      cfg.MCIAS.CACert,
		ServiceName: "metacrypt",
	}, logger)
	if err != nil {
		return err
	}
	policyEngine := policy.NewEngine(b)
	engineRegistry := engine.NewRegistry(b, logger)
	engineRegistry.RegisterFactory(engine.EngineTypeCA, ca.NewCAEngine)
	engineRegistry.RegisterFactory(engine.EngineTypeSSHCA, sshca.NewSSHCAEngine)
	engineRegistry.RegisterFactory(engine.EngineTypeTransit, transit.NewTransitEngine)
	engineRegistry.RegisterFactory(engine.EngineTypeUser, user.NewUserEngine)

	srv := server.New(cfg, database, sealMgr, authenticator, policyEngine, engineRegistry, auditLog, logger, version)
	grpcSrv := grpcserver.New(cfg, sealMgr, authenticator, policyEngine, engineRegistry, auditLog, logger)

	ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
	defer stop()

	go func() {
		if err := grpcSrv.Start(); err != nil {
			logger.Error("gRPC server error", "error", err)
			os.Exit(1)
		}
	}()

	go func() {
		if err := srv.Start(); err != nil {
			logger.Error("server error", "error", err)
			os.Exit(1)
		}
	}()

	<-ctx.Done()
	logger.Info("shutting down")
	grpcSrv.Shutdown()
	return srv.Shutdown(context.Background())
}

func createAuditLogger(cfg *config.Config) (*audit.Logger, error) {
	switch cfg.Audit.Mode {
	case "":
		return nil, nil // disabled
	case "stdout":
		h := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
			Level: audit.LevelAudit,
		})
		return audit.New(h), nil
	case "file":
		f, err := os.OpenFile(cfg.Audit.Path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
		if err != nil {
			return nil, fmt.Errorf("audit: open log file %s: %w", cfg.Audit.Path, err)
		}
		h := slog.NewJSONHandler(f, &slog.HandlerOptions{
			Level: audit.LevelAudit,
		})
		return audit.New(h), nil
	default:
		return nil, fmt.Errorf("audit: unknown mode %q", cfg.Audit.Mode)
	}
}
