package cmd

import (
	"docker4ssh/api"
	c "docker4ssh/config"
	"docker4ssh/database"
	"docker4ssh/docker"
	"docker4ssh/logging"
	"docker4ssh/ssh"
	"docker4ssh/validate"
	"fmt"
	"github.com/spf13/cobra"
	"go.uber.org/zap"
	"os"
	"os/signal"
	"strings"
	"syscall"
	"time"
)

var startCmd = &cobra.Command{
	Use:   "start",
	Short: "Starts the docker4ssh server",
	Args:  cobra.MaximumNArgs(0),

	PreRunE: func(cmd *cobra.Command, args []string) error {
		return preStart()
	},
	Run: func(cmd *cobra.Command, args []string) {
		start()
	},
}

func preStart() error {
	if !docker.IsRunning() {
		return fmt.Errorf("docker daemon is not running")
	}

	cli, err := docker.InitCli()
	if err != nil {
		return err
	}

	config, err := c.InitConfig(true)
	if err != nil {
		return err
	}

	validator := validate.NewConfigValidator(cli, false, config)

	if result := validator.ValidateLogging(); !result.Ok() {
		return fmt.Errorf(result.String())
	}

	level := zap.NewAtomicLevel()
	level.UnmarshalText([]byte(config.Logging.Level))
	var outputFiles, errorFiles []string
	if config.Logging.ConsoleOutput {
		outputFiles = append(outputFiles, "/dev/stdout")
	}
	if config.Logging.OutputFile != "" {
		outputFiles = append(outputFiles, config.Logging.OutputFile)
	}
	if config.Logging.ConsoleError {
		errorFiles = append(errorFiles, "/dev/stderr")
	}
	if config.Logging.ErrorFile != "" {
		errorFiles = append(errorFiles, config.Logging.ErrorFile)
	}
	logging.InitLogging(level, outputFiles, errorFiles)

	if result := validator.Validate(); !result.Ok() {
		return fmt.Errorf(result.String())
	}
	c.SetConfig(config)

	db, err := database.NewSqlite3Connection(config.Database.Sqlite3File)
	if err != nil {
		zap.S().Fatalf("Failed to initialize database: %v", err)
	}
	database.SetDatabase(db)

	return nil
}

func start() {
	config := c.GetConfig()

	if config.SSH.Passphrase == "" {
		zap.S().Warn("YOU HAVE AN EMPTY PASSPHRASE WHICH IS INSECURE, SUGGESTING CREATING A NEW SSH KEY WITH A PASSPHRASE.\n" +
			"IF YOU'RE DOWNLOADED THIS VERSION FROM THE RELEASES (https://github.com/ByteDream/docker4ssh/releases/latest), MAKE SURE TO CHANGE YOUR SSH KEY IMMEDIATELY BECAUSE ANYONE COULD DECRYPT THE SSH SESSION!!\n" +
			"USE 'ssh-keygen -t ed25519 -f /etc/docker4ssh/docker4ssh.key -b 4096' AND UPDATE THE PASSPHRASE IN /etc/docker4ssh/docker4ssh.conf UNDER ssh.Passphrase")
	}

	serverConfig, err := ssh.NewSSHConfig(config)
	if err != nil {
		zap.S().Fatalf("Failed to initialize ssh server config: %v", err)
	}

	sshErrChan, sshCloser := ssh.StartServing(config, serverConfig)
	zap.S().Infof("Started ssh serving on port %d", config.SSH.Port)
	apiErrChan, apiCloser := api.ServeAPI(config)
	zap.S().Infof("Started api serving on port %d", config.Api.Port)

	done := make(chan struct{})
	sig := make(chan os.Signal)
	signal.Notify(sig, syscall.SIGUSR1, os.Interrupt, os.Kill, syscall.SIGINT, syscall.SIGTERM)
	go func() {
		s := <-sig

		if sshCloser != nil {
			sshCloser()
		}
		if apiCloser != nil {
			apiCloser()
		}

		database.GetDatabase().Close()

		if s != syscall.SIGUSR1 {
			// Errorf is called here instead of Fatalf because the original exit signal should be kept to exit with it later
			zap.S().Errorf("(FATAL actually) received abort signal %d: %s", s.(syscall.Signal), strings.ToUpper(s.String()))
			os.Exit(int(s.(syscall.Signal)))
		}

		done <- struct{}{}
	}()

	select {
	case err = <-sshErrChan:
	case err = <-apiErrChan:
	}

	if err != nil {
		zap.S().Errorf("Failed to start working: %v", err)
		sig <- os.Interrupt
	} else {
		select {
		case <-sig:
			if err != nil {
				zap.S().Errorf("Serving failed due error: %v", err)
			} else {
				zap.S().Info("Serving stopped")
			}
		default:
			sig <- syscall.SIGUSR1
		}
	}

	select {
	case <-done:
	case <-time.After(5 * time.Second):
		// if the timeout of 5 seconds expires, forcefully exit
		os.Exit(int(syscall.SIGKILL))
	}
}

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