// Mgmt
// Copyright (C) 2013-2024+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.
//
// Additional permission under GNU GPL version 3 section 7
//
// If you modify this program, or any covered work, by linking or combining it
// with embedded mcl code and modules (and that the embedded mcl code and
// modules which link with this program, contain a copy of their source code in
// the authoritative form) containing parts covered by the terms of any other
// license, the licensors of this program grant you additional permission to
// convey the resulting work. Furthermore, the licensors of this program grant
// the original author, James Shubin, additional permission to update this
// additional permission if he deems it necessary to achieve the goals of this
// additional permission.

// Package util contains a collection of miscellaneous utility functions.
package util

import (
	"bytes"
	"context"
	"encoding/base64"
	"encoding/gob"
	"fmt"
	"path"
	"sort"
	"strings"
	"time"

	"github.com/godbus/dbus/v5"
	"github.com/purpleidea/mgmt/util/errwrap"
)

// NumToAlpha returns a lower case string of letters representing a number. If
// you specify 0, you'll get `a`, 25 gives you `z`, and 26 gives you `aa` and so
// on...
func NumToAlpha(idx int) string {
	if idx < 0 {
		return "" // don't error or produce junk
	}
	var mod = idx % 26
	var div = idx / 26
	if div > 0 {
		return NumToAlpha(div-1) + string(rune(mod+int('a')))
	}
	return string(rune(mod + int('a')))
}

// FirstToUpper returns the string with the first character capitalized.
func FirstToUpper(str string) string {
	if str == "" {
		return ""
	}
	return strings.ToUpper(str[0:1]) + str[1:]
}

// StrInList returns true if a string exists inside a list, otherwise false.
func StrInList(needle string, haystack []string) bool {
	for _, x := range haystack {
		if needle == x {
			return true
		}
	}
	return false
}

// Uint64KeyFromStrInMap returns true if needle is found in haystack of keys
// that have uint64 type.
func Uint64KeyFromStrInMap(needle string, haystack map[uint64]string) (uint64, bool) {
	for k, v := range haystack {
		if v == needle {
			return k, true
		}
	}
	return 0, false
}

// StrRemoveDuplicatesInList removes any duplicate values in the list. This
// implementation is possibly sub-optimal (O(n^2)?) but preserves ordering.
func StrRemoveDuplicatesInList(list []string) []string {
	unique := []string{}
	for _, x := range list {
		if !StrInList(x, unique) {
			unique = append(unique, x)
		}
	}
	return unique
}

// StrFilterElementsInList removes any of the elements in filter, if they exist
// in the list.
func StrFilterElementsInList(filter []string, list []string) []string {
	result := []string{}
	for _, x := range list {
		if !StrInList(x, filter) {
			result = append(result, x)
		}
	}
	return result
}

// StrListIntersection removes any of the elements in filter, if they don't
// exist in the list. This is an in order intersection of two lists.
func StrListIntersection(list1 []string, list2 []string) []string {
	result := []string{}
	for _, x := range list1 {
		if StrInList(x, list2) {
			result = append(result, x)
		}
	}
	return result
}

// ReverseStringList reverses a list of strings.
func ReverseStringList(in []string) []string {
	var out []string // empty list
	l := len(in)
	for i := range in {
		out = append(out, in[l-i-1])
	}
	return out
}

// StrMapKeys return the sorted list of string keys in a map with string keys.
// NOTE: i thought it would be nice for this to use: map[string]interface{} but
// it turns out that's not allowed. I know we don't have generics, but come on!
func StrMapKeys(m map[string]string) []string {
	result := []string{}
	for k := range m {
		result = append(result, k)
	}
	sort.Strings(result) // deterministic order
	return result
}

// StrMapKeysUint64 return the sorted list of string keys in a map with string
// keys but uint64 values.
func StrMapKeysUint64(m map[string]uint64) []string {
	result := []string{}
	for k := range m {
		result = append(result, k)
	}
	sort.Strings(result) // deterministic order
	return result
}

// BoolMapValues returns the sorted list of bool values in a map with string
// values.
func BoolMapValues(m map[string]bool) []bool {
	result := []bool{}
	for _, v := range m {
		result = append(result, v)
	}
	//sort.Bools(result) // TODO: deterministic order
	return result
}

// StrMapValues returns the sorted list of string values in a map with string
// values.
func StrMapValues(m map[string]string) []string {
	result := []string{}
	for _, v := range m {
		result = append(result, v)
	}
	sort.Strings(result) // deterministic order
	return result
}

// StrMapValuesUint64 return the sorted list of string values in a map with
// string values.
func StrMapValuesUint64(m map[uint64]string) []string {
	result := []string{}
	for _, v := range m {
		result = append(result, v)
	}
	sort.Strings(result) // deterministic order
	return result
}

// BoolMapTrue returns true if everyone in the list is true.
func BoolMapTrue(l []bool) bool {
	for _, b := range l {
		if !b {
			return false
		}
	}
	return true
}

// Dirname is similar to the GNU dirname command.
func Dirname(p string) string {
	if p == "/" {
		return ""
	}
	d, _ := path.Split(path.Clean(p))
	return d
}

// Basename is the base of a path string.
func Basename(p string) string {
	_, b := path.Split(path.Clean(p))
	if p == "" {
		return ""
	}
	if p[len(p)-1:] == "/" { // don't lose the tail slash
		b += "/"
	}
	return b
}

// PathSplit splits a path into an array of tokens excluding any trailing empty
// tokens.
func PathSplit(p string) []string {
	if p == "/" { // TODO: can't this all be expressed nicely in one line?
		return []string{""}
	}
	return strings.Split(path.Clean(p), "/")
}

// HasPathPrefix tells us if a path string contain the given path prefix in it.
func HasPathPrefix(p, prefix string) bool {

	patharray := PathSplit(p)
	prefixarray := PathSplit(prefix)

	if len(prefixarray) > len(patharray) {
		return false
	}

	for i := 0; i < len(prefixarray); i++ {
		if prefixarray[i] != patharray[i] {
			return false
		}
	}

	return true
}

// StrInPathPrefixList returns true if the needle is a PathPrefix in the
// haystack.
func StrInPathPrefixList(needle string, haystack []string) bool {
	for _, x := range haystack {
		if HasPathPrefix(x, needle) {
			return true
		}
	}
	return false
}

// RemoveCommonFilePrefixes removes redundant file path prefixes that are under
// the tree of other files.
func RemoveCommonFilePrefixes(paths []string) []string {
	var result = make([]string, len(paths))
	for i := 0; i < len(paths); i++ { // copy, b/c append can modify the args!!
		result[i] = paths[i]
	}
	// is there a string path which is common everywhere?
	// if so, remove it, and iterate until nothing common is left
	// return what's left over, that's the most common superset
loop:
	for {
		if len(result) <= 1 {
			return result
		}
		for i := 0; i < len(result); i++ {
			var copied = make([]string, len(result))
			for j := 0; j < len(result); j++ { // copy, b/c append can modify the args!!
				copied[j] = result[j]
			}
			noi := append(copied[:i], copied[i+1:]...) // rm i
			if StrInPathPrefixList(result[i], noi) {
				// delete the element common to everyone
				result = noi
				continue loop
			}
		}
		break
	}
	return result
}

// SafePathClean does path.Clean, but it preserves any trailing slash if it was
// present in the initial path.
func SafePathClean(s string) string {
	hasSlash := strings.HasSuffix(s, "/")
	clean := path.Clean(s) // removes trailing slashes
	if hasSlash {          // add it back if it was taken off
		clean = clean + "/"
	}
	return clean
}

// SegmentedPathSplit splits an absolute path into chunks including their slash.
// This is similar to the PathSplit function, but the slashes are not lost here!
// TODO: There is likely a more efficient implementation of this function.
func SegmentedPathSplit(p string) []string {
	out := []string{}
	sponge := ""
	for i := 0; i < len(p); i++ {
		sponge = sponge + string(p[i])
		if string(p[i]) == "/" {
			out = append(out, sponge) // found one!
			sponge = ""               // reset
		}
	}
	if sponge != "" {
		out = append(out, sponge) // the last piece
	}

	return out
}

// CommonPathPrefix returns the longest common prefix directory out of all the
// input paths! This is always a directory and thus ends with a slash, unless
// all of the paths are identical and are files, or if only one path is given in
// which case that is returned, or unless no paths are given in which case the
// empty string is returned. If any of your input paths are not absolute, and as
// such do not begin with a slash, then the behaviour is undefined.
func CommonPathPrefix(paths ...string) string {
	// XXX: I am not a good algorithmist, there is probably a more efficient
	// way to write this algorithm. Patches are welcome!
	if len(paths) == 0 {
		return ""
	}
	if len(paths) == 1 {
		return paths[0]
	}

	sps := make([][]string, 0)
	for _, x := range paths {
		if !strings.HasPrefix(x, "/") {
			// TODO: panic? error?
			return "" // undefined behaviour!
		}
		//x = SafePathClean(x) // TODO: Should we "safe clean" each path?
		sp := SegmentedPathSplit(x) // compute once in advance
		sps = append(sps, sp)
	}

	z := sps[0] // SegmentedPathSplit(paths[0]) // arbitrarily choose the first one
	ix := 0
	ret := ""
	for {
		for i := range paths {
			sp := sps[i]       // SegmentedPathSplit(paths[i])
			if len(sp) <= ix { // one path is longer
				return ret
			}

			if sp[ix] != z[ix] { // the chunk differs!
				return ret
			}
		}

		ret = ret + z[ix] // append what we've got so far!
		ix++
	}
}

// PathPrefixDelta returns the delta of the path prefix, which tells you how
// many path tokens different the prefix is.
func PathPrefixDelta(p, prefix string) int {

	if !HasPathPrefix(p, prefix) {
		return -1
	}
	patharray := PathSplit(p)
	prefixarray := PathSplit(prefix)
	return len(patharray) - len(prefixarray)
}

// PathSplitFullReversed returns the full list of "dependency" paths for a given
// path in reverse order.
func PathSplitFullReversed(p string) []string {
	var result []string
	split := PathSplit(p)
	count := len(split)
	var x string
	for i := 0; i < count; i++ {
		x = "/" + path.Join(split[0:i+1]...)
		if i != 0 && !(i+1 == count && !strings.HasSuffix(p, "/")) {
			x += "/" // add trailing slash
		}
		result = append(result, x)
	}
	return ReverseStringList(result)
}

// DirifyFileList adds trailing slashes to any likely dirs in a package manager
// fileList if removeDirs is true, otherwise, don't keep the dirs in our output.
func DirifyFileList(fileList []string, removeDirs bool) []string {
	dirs := []string{}
	for _, file := range fileList {
		dir, _ := path.Split(file) // dir
		dir = path.Clean(dir)      // clean so cmp is easier
		if !StrInList(dir, dirs) {
			dirs = append(dirs, dir)
		}
	}

	result := []string{}
	for _, file := range fileList {
		cleanFile := path.Clean(file)
		if !StrInList(cleanFile, dirs) { // we're not a directory!
			result = append(result, file) // pass through
		} else if !removeDirs {
			result = append(result, cleanFile+"/")
		}
	}

	return result
}

// FlattenListWithSplit flattens a list of input by splitting each element by
// any and all of the strings listed in the split array
func FlattenListWithSplit(input []string, split []string) []string {
	if len(split) == 0 { // nothing to split by
		return input
	}
	out := []string{}
	for _, x := range input {
		var s []string
		if len(split) == 1 {
			s = strings.Split(x, split[0]) // split by only string
		} else {
			s = []string{x} // initial
			for i := range split {
				s = FlattenListWithSplit(s, []string{split[i]}) // recurse
			}
		}
		out = append(out, s...)
	}
	return out
}

// RemoveBasePath removes an absolute base path (directory prefix) from an
// absolute path that is any file or directory.
// Eg: RemoveBasePath("/usr/bin/foo", "/usr/") -> "bin/foo"
// Eg: RemoveBasePath("/usr/bin/project/", "/usr/") -> "bin/project/".
func RemoveBasePath(path, base string) (string, error) {
	if !strings.HasSuffix(base, "/") { // should end with a slash
		return "", fmt.Errorf("base is not a directory")
	}
	if !strings.HasPrefix(path, base) {
		return "", fmt.Errorf("path does not have base prefix")
	}
	return strings.TrimPrefix(path, base), nil
}

// Rebase takes an absolute base path (directory prefix) and removes it from an
// absolute path and then returns that path with a new root as an absolute path
// if root is an absolute dir, and as a relative path if root is a relative dir.
// Eg: Rebase("/usr/bin/foo", "/usr/", "/usr/local/") -> "/usr/local/bin/foo"
// Eg: Rebase("/var/lib/dir/file.conf", "/var/lib/", "") -> "dir/file.conf"
func Rebase(path, base, root string) (string, error) {
	// allow empty root's to rebase into a relative path if not a directory!
	if root != "" && !strings.HasSuffix(root, "/") { // should end with a slash
		return "", fmt.Errorf("root is not a directory")
	}
	s, err := RemoveBasePath(path, base)
	if err != nil {
		return "", err
	}
	return root + s, nil
}

// RemovePathPrefix takes an absolute path and removes the first chunk. It
// returns the remainder as an absolute path. This function is a bit of a hack,
// and could probably be re-written to support any kind of path, and return a
// relative path.
func RemovePathPrefix(s string) (string, error) {
	if !strings.HasPrefix(s, "/") {
		return "", fmt.Errorf("must be absolute")
	}
	// this is the PathSplit logic...
	x := []string{""} // assumes s == "/"
	if s != "/" {
		x = strings.Split(s, "/")
	}
	x = x[2:] // get rid of first two chunks, first is / and second is a dir name
	return "/" + strings.Join(x, "/"), nil
}

// RemovePathSuffix takes an absolute path and removes the last chunk. It
// returns the remainder as an absolute path. This function is a bit of a hack,
// and could probably be re-written to support any kind of path, and return a
// relative path.
func RemovePathSuffix(s string) (string, error) {
	if !strings.HasPrefix(s, "/") {
		return "", fmt.Errorf("must be absolute")
	}
	// this is the PathSplit logic...
	if s == "/" {
		//return "", nil // TODO: return this instead?
		return "", fmt.Errorf("input is /")
	}
	x := strings.Split(s, "/")

	// get rid of the last two chunks, last is / and second to last is a dir
	if strings.HasSuffix(s, "/") {
		_, x = x[len(x)-1], x[:len(x)-1] // pop the last slash
	}
	_, x = x[len(x)-1], x[:len(x)-1] // pop the last chunk
	return strings.Join(x, "/") + "/", nil
}

// DirParents returns a list of the parent directories in a given path. If you
// pass it an empty string, or a single slash, then you will get an empty list.
// If you pass it a malformed path, then you might get unexpected results.
func DirParents(p string) []string {
	if p == "" {
		return nil // TODO: should we error?
	}
	if p == "/" {
		return []string{}
	}
	d := Dirname(p)
	x := DirParents(d)
	x = append(x, d)
	return x
}

// MissingMkdirs takes a list of paths, and returns a list of any missing paths
// that would be needed to avoid having to `mkdir -p` to prevent missing parent
// directory errors from happening. This adds paths all the way up to the root,
// but without including it, because it's implied.
// TODO: do we want to include the root?
// TODO: this could probably be implemented in a more efficient way...
func MissingMkdirs(input []string) ([]string, error) {
	dirs := []string{}
	for _, p := range input {
		if p == "/" {
			continue
		}
		d := Dirname(p)
		dirs = append(dirs, d)
		if strings.HasSuffix(p, "/") { // it's a dir
			dirs = append(dirs, p)
		}
	}
	// TODO: remove duplicates for efficiency?

	result := []string{}
	for _, d := range dirs {
		p := DirParents(d) // TODO: memoize
		p = append(p, d)   // include self
		for _, x := range p {
			if StrInList(x, input) {
				continue
			}
			result = append(result, x)
		}
	}

	out := StrRemoveDuplicatesInList(result) // avoid duplicates
	sort.Sort(PathSlice(out))

	return out, nil
}

// TimeAfterOrBlock is aspecial version of time.After that blocks when given a
// negative integer. When used in a case statement, the timer restarts on each
// select call to it.
func TimeAfterOrBlock(t int) <-chan time.Time {
	if t < 0 {
		return make(chan time.Time) // blocks forever
	}
	return time.After(time.Duration(t) * time.Second)
}

// TimeAfterOrBlockCtx returns a channel that closes after a timeout. If you use
// a negative timeout, it will block forever. It can also unblock using context.
// Make sure to cancel the context when you're done, or you'll leak a goroutine.
func TimeAfterOrBlockCtx(ctx context.Context, t int) <-chan struct{} {
	ch := make(chan struct{})
	go func() {
		defer close(ch)
		if t < 0 {
			select {
			case <-ctx.Done():
			}
			return
		}

		select {
		case <-time.After(time.Duration(t) * time.Second):
		case <-ctx.Done():
		}
	}()
	return ch
}

// CloseAfter takes a duration, similarly to `time.After`, and returns a channel
// that closes when either the context is done, or the duration expires.
func CloseAfter(ctx context.Context, d time.Duration) <-chan struct{} {
	ch := make(chan struct{})
	go func() {
		defer close(ch)
		select {
		case <-time.After(d):
			// done
		case <-ctx.Done():
		}
	}()
	return ch
}

// SystemBusPrivateUsable makes using the private bus usable.
// TODO: should be upstream: https://github.com/godbus/dbus/issues/15
func SystemBusPrivateUsable() (conn *dbus.Conn, err error) {
	conn, err = dbus.SystemBusPrivate()
	if err != nil {
		return nil, err
	}
	if err = conn.Auth(nil); err != nil {
		conn.Close()
		conn = nil
		return
	}
	if err = conn.Hello(); err != nil {
		conn.Close()
		conn = nil
	}
	return conn, nil // success
}

// SessionBusPrivateUsable makes using the private bus usable.
// TODO: should be upstream: https://github.com/godbus/dbus/issues/15
func SessionBusPrivateUsable() (conn *dbus.Conn, err error) {
	conn, err = dbus.SessionBusPrivate()
	if err != nil {
		return nil, err
	}
	if err = conn.Auth(nil); err != nil {
		conn.Close()
		conn = nil
		return
	}
	if err = conn.Hello(); err != nil {
		conn.Close()
		conn = nil
	}
	return conn, nil // success
}

// PriorityStrSliceSort filters any elements matching fn to the end of the list.
// You can reverse the match result with a not to filter to the front instead! A
// copy of the list is returned, the original is not modified.
func PriorityStrSliceSort(input []string, fn func(string) bool) []string {
	output := []string{}
	found := []string{}
	for _, x := range input {
		if fn(x) { // if we find the key, don't include it just yet
			found = append(found, x) // save for later
			continue
		}
		output = append(output, x)
	}

	// include the keys at the end (if found)
	output = append(output, found...)

	return output
}

// SortedStrSliceCompare takes two lists of strings and returns whether or not
// they are equivalent. It will return nil if both sets contain the same
// elements, regardless of order, and an error if they do not.
func SortedStrSliceCompare(a, b []string) error {
	if len(a) != len(b) {
		return fmt.Errorf("slices have different lengths: %d vs %d", len(a), len(b))
	}

	// make a copy of each to sort, so we don't reorder the inputs
	x := make([]string, len(a))
	y := make([]string, len(b))
	copy(x, a)
	copy(y, b)

	sort.Strings(x)
	sort.Strings(y)
	for i := range x {
		if x[i] != y[i] {
			return fmt.Errorf("values do not match: %s vs %s", x[i], y[i])
		}
	}
	return nil
}

// PathSlice is a type used to implement sort.Interface on a slice of strings,
// where each string is a path. This allows you to call sort.Sort() on a list of
// paths, after casting the []string{} to this type. Paths will be sorted by
// depth in alphabetical order.
type PathSlice []string

// Len returns the length of obj. It is required to satisfy sort.Interface.
func (obj PathSlice) Len() int {
	return len(obj)
}

// Swap swaps obj[i] and obj[j]. it is required to satisfy sort.Interface.
func (obj PathSlice) Swap(i, j int) {
	obj[i], obj[j] = obj[j], obj[i]
}

// Less returns whether obj[i] is less than obj[j]. It performs the logic
// required to satisfy sort.Interface.
func (obj PathSlice) Less(i, j int) bool {
	x := PathSplitFullReversed(obj[i])
	y := PathSplitFullReversed(obj[j])
	if x[0] != y[0] {
		return x[0] < y[0]
	}
	if len(x) > len(y) {
		return false
	}
	for i := range x {
		if x[i] > y[i] {
			return false
		}
	}
	return true
}

// Sort is a convenience method.
func (obj PathSlice) Sort() { sort.Sort(obj) }

// UInt64Slice attaches the methods of sort.Interface to []uint64, sorting in
// increasing order.
type UInt64Slice []uint64

// Len returns the length of the slice of uint64's.
func (obj UInt64Slice) Len() int { return len(obj) }

// Swap swaps two elements in the slice.
func (obj UInt64Slice) Swap(i, j int) { obj[i], obj[j] = obj[j], obj[i] }

// Less returns the smaller element in the sort order.
func (obj UInt64Slice) Less(i, j int) bool { return obj[i] < obj[j] }

// Sort is a convenience method.
func (obj UInt64Slice) Sort() { sort.Sort(obj) }

// SortMapStringValuesByUInt64Keys builds a list of strings, sorted by the
// corresponding key that is associated with that value.
// TODO: add some tests
func SortMapStringValuesByUInt64Keys(m map[uint64]string) []string {
	//if m == nil { // no need to special case this, range handles it safely
	//	return []string{}
	//}
	keys := []uint64{}
	for i := range m {
		keys = append(keys, i)
	}
	sort.Sort(UInt64Slice(keys))

	result := []string{}
	for _, key := range keys {
		s := m[key]
		result = append(result, s)
	}

	return result
}

// ValueToB64 encodes a value to a base64 encoded string (after serialization).
func ValueToB64(value interface{}) (string, error) {
	b := bytes.Buffer{}
	e := gob.NewEncoder(&b)
	if err := e.Encode(&value); err != nil { // pass with &
		return "", errwrap.Wrapf(err, "gob failed to encode")
	}
	return base64.StdEncoding.EncodeToString(b.Bytes()), nil
}

// B64ToValue decodes a value from a base64 encoded string (after
// deserialization).
func B64ToValue(str string) (interface{}, error) {
	var output interface{}
	bb, err := base64.StdEncoding.DecodeString(str)
	if err != nil {
		return nil, errwrap.Wrapf(err, "base64 failed to decode")
	}
	b := bytes.NewBuffer(bb)
	d := gob.NewDecoder(b)
	if err := d.Decode(&output); err != nil { // pass with &
		return nil, errwrap.Wrapf(err, "gob failed to decode")
	}
	value, ok := output.(interface{})
	if !ok {
		return nil, fmt.Errorf("output `%v` is not a value", output)
	}
	return value, nil
}
