package manager

import (
	"fmt"
	"io/ioutil"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
)

func getService(system bool, home string, name string) (string, error) {
	sys, user, err := getMatchingSrvs(home, name)
	if nil != err {
		return "", err
	}

	var service string
	if system {
		service, err = getOneSysSrv(sys, user, name)
		if nil != err {
			return "", err
		}
	} else {
		service, err = getOneUserSrv(home, sys, user, name)
		if nil != err {
			return "", err
		}
	}

	return service, nil
}

// Runnable defines a command to run, along with its arguments,
// and whether or not failing to exit successfully matters.
// It also defines whether certains words must exist (or not exist)
// in its output, apart from existing successfully, to determine
// whether or not it was actually successful.
type Runnable struct {
	Exec     string
	Args     []string
	Must     bool
	Keywords []string
	Badwords []string
}

func (x Runnable) Run() error {
	cmd := exec.Command(x.Exec, x.Args...)
	out, err := cmd.CombinedOutput()
	if !x.Must {
		return nil
	}

	good := true
	str := string(out)
	for j := range x.Keywords {
		if !strings.Contains(str, x.Keywords[j]) {
			good = false
			break
		}
	}
	if good && 0 != len(x.Badwords) {
		for j := range x.Badwords {
			if "" != x.Badwords[j] && !strings.Contains(str, x.Badwords[j]) {
				good = false
				break
			}
		}
	}
	if nil != err {
		var comment string
		if len(x.Keywords) > 0 {
			comment += "# output must match all of:\n"
			comment += "# \t" + strings.Join(x.Keywords, "\n#\t") + "\n"
		}
		if len(x.Badwords) > 0 {
			comment += "# output must not match any of:\n"
			comment += "# \t" + strings.Join(x.Badwords, "\n#\t") + "\n"
		}
		return fmt.Errorf("Failed to run %s %s\n%s\n%s\n", x.Exec, strings.Join(x.Args, " "), str, comment)
	}

	return nil
}

func (x Runnable) String() string {
	var must = "true"

	if x.Must {
		must = "exit"
	}

	return strings.TrimSpace(fmt.Sprintf(
		"%s %s || %s\n",
		x.Exec,
		strings.Join(x.Args, " "),
		must,
	))
}

func getSrvs(dir string) ([]string, error) {
	plists := []string{}

	infos, err := ioutil.ReadDir(dir)
	if nil != err {
		return nil, err
	}

	for i := range infos {
		x := infos[i]
		fname := strings.ToLower(x.Name())
		if strings.HasSuffix(fname, srvExt) {
			plists = append(plists, x.Name())
		}
	}

	return plists, nil
}

func getSystemSrvs() ([]string, error) {
	return getSrvs(srvSysPath)
}

func getUserSrvs(home string) ([]string, error) {
	confDir := filepath.Join(home, srvUserPath)
	err := os.MkdirAll(confDir, 0755)
	if nil != err {
		return nil, err
	}
	return getSrvs(confDir)
}

// "come.example.foo.plist" matches "foo"
func filterMatchingSrvs(plists []string, name string) []string {
	filtered := []string{}

	for i := range plists {
		pname := plists[i]
		lname := strings.ToLower(pname)
		n := len(lname)
		if strings.HasSuffix(lname[:n-srvLen], strings.ToLower(name)) {
			filtered = append(filtered, pname)
		}
	}

	return filtered
}

func getMatchingSrvs(home string, name string) ([]string, []string, error) {
	sysPlists, err := getSystemSrvs()
	if nil != err {
		return nil, nil, err
	}

	var userPlists []string
	if "" != home {
		userPlists, err = getUserSrvs(home)
		if nil != err {
			return nil, nil, err
		}
	}

	return filterMatchingSrvs(sysPlists, name), filterMatchingSrvs(userPlists, name), nil
}

func getExactSrvMatch(srvs []string, name string) string {
	for i := range srvs {
		srv := srvs[i]
		n := len(srv)
		if srv[:n-srvLen] == strings.ToLower(name) {
			return srv
		}
	}

	return ""
}

func getOneSysSrv(sys []string, user []string, name string) (string, error) {
	if service := getExactSrvMatch(user, name); "" != service {
		return filepath.Join(srvSysPath, service), nil
	}

	n := len(sys)
	switch {
	case 0 == n:
		errstr := fmt.Sprintf("Didn't find user service matching %q\n", name)
		if 0 != len(user) {
			errstr += fmt.Sprintf("Did you intend to run a user service instead?\n\t%s\n", strings.Join(user, "\n\t"))
		}
		return "", fmt.Errorf(errstr)
	case n > 1:
		errstr := fmt.Sprintf("Found more than one matching service:\n\t%s\n", strings.Join(sys, "\n\t"))
		return "", fmt.Errorf(errstr)
	default:
		return filepath.Join(srvSysPath, sys[0]), nil
	}
}

func getOneUserSrv(home string, sys []string, user []string, name string) (string, error) {
	if service := getExactSrvMatch(user, name); "" != service {
		return filepath.Join(home, srvUserPath, service), nil
	}

	n := len(user)
	switch {
	case 0 == n:
		errstr := fmt.Sprintf("Didn't find user service matching %q\n", name)
		if 0 != len(sys) {
			errstr += fmt.Sprintf("Did you intend to run a system service instead?\n\t%s\n", strings.Join(sys, "\n\t"))
		}
		return "", fmt.Errorf(errstr)
	case n > 1:
		errstr := fmt.Sprintf("Found more than one matching service:\n\t%s\n", strings.Join(user, "\n\t"))
		return "", fmt.Errorf(errstr)
	default:
		return filepath.Join(home, srvUserPath, user[0]), nil
	}
}

func adjustPrivs(system bool, cmds []Runnable) []Runnable {
	if !system || isPrivileged() {
		return cmds
	}

	sudos := cmds
	cmds = []Runnable{}
	for i := range sudos {
		exe := sudos[i]
		exe.Args = append([]string{exe.Exec}, exe.Args...)
		exe.Exec = "sudo"
		cmds = append(cmds, exe)
	}

	return cmds
}

func Run(bin string, args ...string) error {
	cmd := exec.Command(bin, args...)
	// for debugging
	/*
		out, err := cmd.CombinedOutput()
		if nil != err {
			fmt.Println(err)
		}
		fmt.Println(string(out))
	*/

	err := cmd.Start()
	if nil != err {
		return err
	}
	return nil
}
