package main

import "os"
import "fmt"
import "flag"
import "strconv"
import "os/exec"
import "os/user"
import "hnakra/cli"
import "path/filepath"
import "golang.org/x/crypto/bcrypt"

func tryCommand (cmd *exec.Cmd, failReason string) {
	output, err := cmd.CombinedOutput()
	if err != nil {
		cli.Sayf("%s: %s\n", failReason, string(output))
		os.Exit(1)
	}
}

func ownOne (path string, uid, gid int) {
	file, err := os.Stat(path)
	if err != nil {
		cli.Sayf("could not stat %s: %v\n", path, err)
		return
	}

	err = os.Chown(path, uid, gid)
	if err != nil {
		cli.Sayf("could not change ownership of %s: %v\n", path, err)
		return
	}

	if file.IsDir() {
		err = os.Chmod(path, 0770)
	} else {
		err = os.Chmod(path, 0660)
	}
	if err != nil {
		cli.Sayf("could not change mode of %s: %v\n", path, err)
		return
	}
}

func main () {
	user, err := user.Current()
	if err != nil {
		cli.Sayf("could not get username %v\n", err)
		os.Exit(1)
	}

	flag.Usage = func () {
		cli.Printf("Usage of %s:\n", os.Args[0])
		cli.Printf("  hash\n")
		cli.Printf("    Generate a bcrypt hash of a key\n")
		cli.Printf("  adduser\n")
		cli.Printf("    Add a system user to run a service as\n")
		cli.Printf("  deluser\n")
		cli.Printf("    Remove a user added with adduser\n")
		cli.Printf("  auth\n")
		cli.Printf("    Authorize a system user to access a service's files\n")
		cli.Printf("  own\n")
		cli.Printf("    Give ownership of a file to a service\n")
		os.Exit(1)
	}

	// define commands
	hashCommand := flag.NewFlagSet("hash", flag.ExitOnError)
	hashCost    := hashCommand.Uint("cost", uint(bcrypt.DefaultCost), "Cost of the hash")
	hashText    := hashCommand.String("k", "", "Text content of the key")

	addUserCommand := flag.NewFlagSet("adduser", flag.ExitOnError)
	addUserService := addUserCommand.String ("s", "router",
		"Service to add a user for")

	delUserCommand := flag.NewFlagSet("deluser", flag.ExitOnError)
	delUserService := delUserCommand.String ("s", "router",
		"Service to delete the user for")
	delUserRmData  := delUserCommand.Bool ("D", false,
		"Whether to remove the service's data directory")

	authCommand := flag.NewFlagSet("auth", flag.ExitOnError)
	authService := authCommand.String ("s", "router",
		"Service to authorize the user to access")
	authUser := authCommand.String ("u", user.Username,
		"User to be given access")

	ownCommand := flag.NewFlagSet("own", flag.ExitOnError)
	ownService := ownCommand.String ("s", "router",
		"Service to give ownership of the file to")
	ownFile := ownCommand.String ("f", ".",
		"File to take ownership of")
	ownRecursive := ownCommand.Bool ("r", false,
		"Whether or not to recurse into sub-directories")

	flag.Parse()

	// execute correct command
	if len(os.Args) < 2 {
		flag.Usage()
		os.Exit(1)
	}
	subCommandArgs := os.Args[2:]
	switch os.Args[1] {
	case "hash":
		hashCommand.Parse(subCommandArgs)
		execHash(int(*hashCost), *hashText)
	case "adduser":
		addUserCommand.Parse(subCommandArgs)
		execAdduser(*addUserService)
	case "deluser":
		delUserCommand.Parse(subCommandArgs)
		execDeluser(*delUserService, *delUserRmData)
	case "auth":
		authCommand.Parse(subCommandArgs)
		execAuth(*authService, *authUser)
	case "own":
		ownCommand.Parse(subCommandArgs)
		execOwn(*ownService, *ownFile, *ownRecursive)
	}
}

func execHash (cost int, key string) {
	if key == "" {
		cli.Sayf("please specify key text content\n")
		os.Exit(1)
	}

	if cost < bcrypt.MinCost {
		cli.Sayf("cost is too low, must be at least %v\n", bcrypt.MinCost)
		os.Exit(1)
	}
	if cost > bcrypt.MaxCost {
		cli.Sayf("cost is too hight, can be at most %v\n", bcrypt.MaxCost)
		os.Exit(1)
	}

	hash, err := bcrypt.GenerateFromPassword([]byte(key), cost)
	if err != nil {
		cli.Sayf("could not hash key: %v\n", err)
		os.Exit(1)
	}

	fmt.Println(string(hash))
}

func execAdduser (service string) {
	fullName := cli.ServiceUser(service)
	dataDir  := cli.ServiceDir(service)
	
	if adduser, err := exec.LookPath("adduser"); err == nil {
		// BUSYBOX
		addgroup, _ := exec.LookPath("addgroup")
		tryCommand (exec.Command(addgroup, fullName, "-S"),
			"could not add group")
		tryCommand (exec.Command (
			adduser, fullName, "-SHDG", fullName, "-h", dataDir),
			"could not add user")
	} else if useradd, err := exec.LookPath("useradd"); err == nil {
		// GNU
		tryCommand (exec.Command (
			useradd, fullName, "-rUM",
			"--shell", "/sbin/nologin",
			"-d", dataDir), "could not add user")
	} else {
		cli.Sayf("could not add user: no command adduser or useradd\n")
		os.Exit(1)
	}

	// create data directory
	uid, gid, err := cli.LookupUID(fullName)
	if err != nil {
		cli.Sayf("could not create data dir: %v\n", err)
		os.Exit(1)
	}
	err = cli.MkdirFor(dataDir, int(uid), int(gid))
	if err != nil {
		cli.Sayf("could not create data dir: %v\n", err)
		os.Exit(1)
	}
}

func execDeluser (service string, rmData bool) {
	fullName := cli.ServiceUser(service)
	dataDir  := cli.ServiceDir(service)
	
	if deluser, err := exec.LookPath("deluser"); err == nil {
		// BUSYBOX
		tryCommand (exec.Command(deluser, fullName, "--remove-home"),
			"could not delete user")
	} else if userdel, err := exec.LookPath("userdel"); err == nil {
		// GNU
		tryCommand (exec.Command(userdel, fullName, "-r"),
			"could not delete user")
		groupdel, _ := exec.LookPath("groupdel")
		tryCommand (exec.Command(groupdel, fullName),
			"could not delete group")
	} else {
		cli.Sayf("could not delete user: no command deluser or userdel\n")
		os.Exit(1)
	}

	// delete data directory
	if rmData {
		err := os.RemoveAll(dataDir)
		if err != nil {
			cli.Sayf("could not delete data dir: %v\n", err)
			os.Exit(1)
		}
	}
}

func execAuth (service, user string) {
	fullName := cli.ServiceUser(service)
	
	adduser, err := exec.LookPath("adduser")
	if err == nil {
		tryCommand (exec.Command(adduser, user, fullName),
			"could not add user to group " + fullName)
		return
	}

	// GNU
	useradd, err := exec.LookPath("usermod")
	if err == nil {
		tryCommand (exec.Command(useradd, "-a", "-g", fullName, user),
			"could not add user to group " + fullName)
		return
	}

	cli.Sayf("could not auth user: no command adduser or usermod\n")
	os.Exit(1)
}

func execOwn (service, file string, recurse bool) {
	fullName := cli.ServiceUser(service)
	
	userInfo, err := user.Lookup(fullName)
	uid, _ := strconv.Atoi(userInfo.Uid)
	gid, _ := strconv.Atoi(userInfo.Gid)

	if err != nil {
		cli.Sayf("could not get user info: %v\n", err)
		os.Exit(1)
	}

	if !recurse {
		ownOne(file, uid, gid)
		return
	}

	err = filepath.Walk(file, func(
		filePath string,
		file os.FileInfo,
		err error,
	) error {
		if err != nil {
			cli.Sayf("could not traverse filesystem: %v\n", err)
			return nil
		}

		ownOne(filePath, uid, gid)
		return nil
	})

	if err != nil {
		cli.Sayf("could not traverse filesystem: %v\n", err)
		os.Exit(1)
	}
}
