// Copyright (C) 2019 Marius Schellenberger

package cmd

import (
	"encoding/json"
	"git.giftfish.de/ston1th/docstore/pkg/core"
	"git.giftfish.de/ston1th/docstore/pkg/db"
	"git.giftfish.de/ston1th/docstore/pkg/log"
	"git.giftfish.de/ston1th/docstore/pkg/server"
	"git.giftfish.de/ston1th/godrop/v2"
	"github.com/urfave/cli"
	stdlog "log"
	"net"
	"os"
	"os/signal"
	"path/filepath"
	"syscall"
	"time"
)

const (
	defRunDir  = "/var/lib/docstore"
	defListen  = "127.0.0.1:8080"
	defLogFile = "docstore.log"

	defRunUser  = "docstore"
	defRunGroup = "docstore"
)

var (
	config     core.Config
	dumpFile   string
	configFile string
)

func initServer(cfg core.Config) (err error) {
	// remove initial pledge due to violation with go1.12
	// sysctl kern.somaxconn
	//if err = godrop.PledgePromises("stdio rpath wpath cpath inet fattr flock proc exec id unveil"); err != nil {
	//	return
	//}
	dropCfg := godrop.Config{
		User:       cfg.User,
		Group:      cfg.Group,
		Foreground: cfg.Foreground,
	}
	err = godrop.Drop(dropCfg, func() (net.Listener, error) {
		return net.Listen("tcp", cfg.ListenAddr)
	})
	if err != nil {
		return
	}
	if err = os.Chdir(cfg.RunDir); err != nil {
		return
	}

	if err = godrop.PledgePromises("stdio rpath wpath cpath inet fattr flock unveil"); err != nil {
		return
	}

	l, err := godrop.GetListener()
	if err != nil {
		return
	}

	err = godrop.Unveil(cfg.RunDir, "rwc")
	if err != nil {
		return
	}
	err = godrop.UnveilBlock()
	if err != nil {
		return
	}

	log.InitLogger(cfg)

	cfg.DataDir = filepath.Join(cfg.RunDir, core.DataDir)
	srv := server.NewHTTPServer(cfg, l)
	err = srv.Start()
	if err != nil {
		return
	}
	log.Println("docstore " + cfg.Version + " started")
	sigs := make(chan os.Signal)
	signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
	sig := <-sigs
	log.Println("signal: " + sig.String())
	go func() {
		time.Sleep(time.Second * 20)
		log.Fatal("stop timed out: killing")
	}()
	err = srv.Stop()
	if err != nil {
		log.Println(err)
	}
	log.Println("docstore stopped")
	return
}

func loadConfig() error {
	if configFile == "" {
		return nil
	}
	file, err := os.Open(configFile)
	if err != nil {
		return err
	}
	err = json.NewDecoder(file).Decode(&config)
	if err != nil {
		return err
	}
	return file.Close()
}

func Run(version string) {
	app := cli.NewApp()
	app.Name = "docstore"
	app.Usage = "file server and indexing engine"
	app.Version = version
	app.Commands = []cli.Command{
		{
			Name:  "server",
			Usage: "start docstore server",
			Flags: defaultFlags(serverFlags()),
			Action: func(c *cli.Context) error {
				if err := loadConfig(); err != nil {
					stdlog.Fatal("server: ", err)
				}
				config.Version = app.Version
				if err := initServer(config); err != nil {
					stdlog.Fatal("server: ", err)
				}
				return nil
			},
		},
		{
			Name:  "dump",
			Usage: "create a json dump of all users (docstore needs to be stopped)",
			Flags: defaultFlags(dumpRestoreFlags()),
			Action: func(c *cli.Context) error {
				var (
					file = os.Stdout
					err  error
				)
				if err = loadConfig(); err != nil {
					stdlog.Fatal("dump: ", err)
				}
				if dumpFile != "-" {
					file, err = os.OpenFile(dumpFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0640)
					if err != nil {
						stdlog.Fatal("dump: ", err)
					}
				}
				DB, err := db.New(config)
				if err != nil {
					stdlog.Fatal("dump: ", err)
				}
				if err = DB.Dump(file); err != nil {
					stdlog.Fatal("dump: ", err)
				}
				file.Close()
				err = DB.Close()
				if err != nil {
					stdlog.Fatal("dump: ", err)
				}
				return nil
			},
		},
		{
			Name:  "restore",
			Usage: "restore a json dump (docstore needs to be stopped)",
			Flags: defaultFlags(dumpRestoreFlags()),
			Action: func(c *cli.Context) error {
				var (
					file = os.Stdin
					err  error
				)
				if err = loadConfig(); err != nil {
					stdlog.Fatal("restore: ", err)
				}
				if dumpFile != "-" {
					file, err = os.Open(dumpFile)
					if err != nil {
						stdlog.Fatal("restore: ", err)
					}
				}
				DB, err := db.New(config)
				if err != nil {
					stdlog.Fatal("restore: ", err)
				}
				if err = DB.Restore(file); err != nil {
					stdlog.Fatal("restore: ", err)
				}
				file.Close()
				err = DB.Close()
				if err != nil {
					stdlog.Fatal("restore: ", err)
				}
				return nil
			},
		},
	}
	app.Run(os.Args)
}

func serverFlags() []cli.Flag {
	return []cli.Flag{
		cli.StringFlag{
			Name:        "user, u",
			Value:       defRunUser,
			Usage:       "drop privileges to user",
			Destination: &config.User,
		},
		cli.StringFlag{
			Name:        "group, g",
			Value:       defRunGroup,
			Usage:       "drop privileges to group",
			Destination: &config.Group,
		},
		cli.StringFlag{
			Name:        "listen, l",
			Value:       defListen,
			Usage:       "listening <address>:<port>",
			Destination: &config.ListenAddr,
		},
		cli.StringFlag{
			Name:        "log",
			Value:       defLogFile,
			Usage:       "log file (use - for stdout, this only works in combination with -f)",
			Destination: &config.LogFile,
		},
		cli.StringFlag{
			Name:        "secret",
			Usage:       "static hmac secret (at least 64 chars, used for JWT signing)",
			Destination: &config.Secret,
		},
		cli.BoolFlag{
			Name:        "foreground, f",
			Usage:       "do not fork into the background",
			Destination: &config.Foreground,
		},
		cli.BoolFlag{
			Name:        "secure, s",
			Usage:       "enable secure cookie",
			Destination: &config.SecureCookie,
		},
		cli.BoolFlag{
			Name:        "debug",
			Usage:       "enable debugging",
			Destination: &config.Debug,
		},
	}
}

func dumpRestoreFlags() []cli.Flag {
	return []cli.Flag{
		cli.StringFlag{
			Name:        "file, f",
			Value:       "-",
			Usage:       "file to read/write dump (default stdin/stdout)",
			Destination: &dumpFile,
		},
	}
}

func defaultFlags(f []cli.Flag) []cli.Flag {
	return append([]cli.Flag{
		cli.StringFlag{
			Name:        "config, c",
			Usage:       "config file",
			Destination: &configFile,
		},
		cli.StringFlag{
			Name:        "run, r",
			Value:       defRunDir,
			Usage:       "run directory",
			Destination: &config.RunDir,
		},
	}, f...)
}
