package envs

import (
	`bytes`
	`errors`
	`fmt`
	`io/ioutil`
	`os`
	`reflect`
	`strings`
	`sync`

	`r00t2.io/goutils/multierr`
	`r00t2.io/goutils/structutils`
	`r00t2.io/sysutils/errs`
	`r00t2.io/sysutils/internal`
	`r00t2.io/sysutils/paths`
)

/*
	DefEnv operates like Python's .get() method on dicts (maps);
	if the environment variable specified by key does not exist/is not specified,
	then the value specified by fallback will be returned instead
	otherwise key's value is returned.
*/
func DefEnv(key, fallback string) (value string) {

	var exists bool

	if value, exists = os.LookupEnv(key); !exists {
		value = fallback
	}

	return
}

// DefEnvBlank is like DefEnv but will ADDITIONALLY/ALSO apply fallback if key is *defined/exists but is an empty string*.
func DefEnvBlank(key, fallback string) (value string) {

	value = DefEnv(key, fallback)
	if value == "" {
		value = fallback
	}

	return
}

// GetEnvMap returns a map of all environment variables. All values are strings.
func GetEnvMap() (envVars map[string]string) {

	var envList []string = os.Environ()

	envVars = envListToMap(envList)

	return
}

/*
	GetEnvMapNative returns a map of all environment variables, but attempts to "nativize" them.
	All values are interfaces. It is up to the caller to typeswitch them to proper types.

	Note that the PATH/Path environment variable (for *Nix and Windows, respectively) will be
	a []string (as per GetPathEnv). No other env vars, even if they contain os.PathListSeparator,
	will be transformed to a slice or the like.
	If an error occurs during parsing the path env var, it will be rendered	as a string.

	All number types will attempt to be their 64-bit version (i.e. int64, uint64, float64, etc.).

	If a type cannot be determined for a value, its string form will be used
	(as it would be found in GetEnvMap).
*/
func GetEnvMapNative() (envMap map[string]interface{}) {

	var stringMap map[string]string = GetEnvMap()

	envMap = nativizeEnvMap(stringMap)

	return
}

/*
	GetFirst gets the first instance if populated/set occurrence of varNames.

	For example, if you have three potential env vars, FOO, FOOBAR, FOOBARBAZ,
	and want to follow the logic flow of:

		1.) Check if FOO is set. If not,
		2.) Check if FOOBAR is set. If not,
		3.) Check if FOOBARBAZ is set.

	Then this would be specified as:

		GetFirst([]string{"FOO", "FOOBAR", "FOOBARBAZ"})

	If val is "" and ok is true, this means that one of the specified variable names IS
	set but is set to an empty value. If ok is false, none of the specified variables
	are set.

	It is a thin wrapper around GetFirstWithRef.
*/
func GetFirst(varNames []string) (val string, ok bool) {

	val, ok, _ = GetFirstWithRef(varNames)

	return
}

/*
	GetFirstWithRef behaves exactly like GetFirst, but with an additional returned value, idx,
	which specifies the index in varNames in which a set variable was found. e.g. if:

		GetFirstWithRef([]string{"FOO", "FOOBAR", "FOOBAZ"})

	is called and FOO is not set but FOOBAR is, idx will be 1.

	If ok is false, idx will always be -1 and should be ignored.
*/
func GetFirstWithRef(varNames []string) (val string, ok bool, idx int) {

	idx = -1

	for i, vn := range varNames {
		if HasEnv(vn) {
			ok = true
			idx = i
			val = os.Getenv(vn)
			return
		}
	}

	return
}

// GetPathEnv returns a slice of the PATH variable's items.
func GetPathEnv() (pathList []string, err error) {

	var pathVar string = internal.GetPathEnvName()

	pathList = make([]string, 0)

	for _, p := range strings.Split(os.Getenv(pathVar), string(os.PathListSeparator)) {
		if err = paths.RealPath(&p); err != nil {
			return
		}
		pathList = append(pathList, p)
	}
	return
}

/*
	GetPidEnvMap will only work on *NIX-like systems with procfs.
	It gets the environment variables of a given process' PID.
*/
func GetPidEnvMap(pid uint32) (envMap map[string]string, err error) {

	var envBytes []byte
	var envList []string
	var envArr [][]byte
	var procPath string
	var exists bool

	envMap = make(map[string]string, 0)

	procPath = fmt.Sprintf("/proc/%v/environ", pid)

	if exists, err = paths.RealPathExists(&procPath); err != nil {
		return
	}
	if !exists {
		err = errors.New(fmt.Sprintf("information for pid %v does not exist", pid))
		return
	}

	if envBytes, err = ioutil.ReadFile(procPath); err != nil {
		return
	}

	envArr = bytes.Split(envBytes, []byte{0x0})
	envList = make([]string, len(envArr))
	for idx, b := range envArr {
		envList[idx] = string(b)
	}

	envMap = envListToMap(envList)

	return
}

/*
	GetPidEnvMapNative, like GetEnvMapNative, returns a map of all environment variables, but attempts to "nativize" them.
	All values are interfaces. It is up to the caller to typeswitch them to proper types.

	See the documentation for GetEnvMapNative for details.
*/
func GetPidEnvMapNative(pid uint32) (envMap map[string]interface{}, err error) {

	var stringMap map[string]string

	if stringMap, err = GetPidEnvMap(pid); err != nil {
		return
	}

	envMap = nativizeEnvMap(stringMap)

	return
}

/*
	HasEnv is much like os.LookupEnv, but only returns a boolean for
	if the environment variable key exists or not.

	This is useful anywhere you may need to set a boolean in a func call
	depending on the *presence* of an env var or not.
*/
func HasEnv(key string) (envIsSet bool) {

	_, envIsSet = os.LookupEnv(key)

	return
}

/*
	Interpolate takes one of:

		- a string (pointer only)
		- a struct (pointer only)
		- a map (applied to both keys *and* values)
		- a slice

	and performs variable substitution on strings from environment variables.

	It supports both UNIX/Linux/POSIX syntax formats (e.g. $VARNAME, ${VARNAME}) and,
	if on Windows, it *additionally* supports the EXPAND_SZ format (e.g. %VARNAME%).

	For structs, the tag name used can be changed by setting the StructTagInterpolate
	variable in this submodule; the default is `envsub`.
	If the tag value is "-", the field will be skipped.
	For map fields within structs etc., the default is to apply interpolation to both keys and values.
	All other tag value(s) are ignored.

	For maps and slices, Interpolate will recurse into values (e.g. [][]string will work as expected).

	If s is nil, no interpolation will be performed. No error will be returned.
	If s is not a valid/supported type, no interpolation will be performed. No error will be returned.
*/
func Interpolate[T any](s T) (err error) {

	var ptrVal reflect.Value
	var ptrType reflect.Type
	var ptrKind reflect.Kind
	var sVal reflect.Value = reflect.ValueOf(s)
	var sType reflect.Type = sVal.Type()
	var kind reflect.Kind = sType.Kind()

	switch kind {
	case reflect.Ptr:
		if sVal.IsNil() || sVal.IsZero() || !sVal.IsValid() {
			return
		}
		ptrVal = sVal.Elem()
		ptrType = ptrVal.Type()
		ptrKind = ptrType.Kind()
		if ptrKind == reflect.String {
			err = interpolateStringReflect(ptrVal)
		} else {
			// Otherwise, it should be a struct ptr.
			if ptrKind != reflect.Struct {
				return
			}
			err = interpolateStruct(ptrVal)
		}
	case reflect.Map:
		if sVal.IsNil() || sVal.IsZero() || !sVal.IsValid() {
			return
		}
		err = interpolateMap(sVal)
	case reflect.Slice:
		if sVal.IsNil() || sVal.IsZero() || !sVal.IsValid() {
			return
		}
		err = interpolateSlice(sVal)
		/*
			case reflect.Struct:
				if sVal.IsZero() || !sVal.IsValid() {
					return
				}
				err = interpolateStruct(sVal)

		*/
	}

	return
}

/*
	InterpolateString takes (a pointer to) a struct or string and performs variable substitution on it
	from environment variables.

	It supports both UNIX/Linux/POSIX syntax formats (e.g. $VARNAME, ${VARNAME}) and,
	if on Windows, it *additionally* supports the EXPAND_SZ format (e.g. %VARNAME%).

	If s is nil, nothing will be done and err will be ErrNilPtr.

	This is a standalone function that is much more performant than Interpolate
	at the cost of rigidity.
*/
func InterpolateString(s *string) (err error) {

	var newStr string

	if s == nil {
		err = errs.ErrNilPtr
		return
	}

	if newStr, err = interpolateString(*s); err != nil {
		return
	}
	*s = newStr

	return
}

// interpolateMap is used by Interpolate for maps. v should be a reflect.Value of a map.
func interpolateMap(v reflect.Value) (err error) {

	var kVal reflect.Value
	var vVal reflect.Value
	var newMap reflect.Value
	var wg sync.WaitGroup
	var numJobs int
	var errChan chan error
	var doneChan chan bool = make(chan bool, 1)
	var mErr *multierr.MultiError = multierr.NewMultiError(nil)
	var t reflect.Type = v.Type()
	var kind reflect.Kind = t.Kind()

	if kind != reflect.Map {
		err = errs.ErrBadType
		return
	}

	if v.IsNil() || v.IsZero() || !v.IsValid() {
		return
	}

	numJobs = v.Len()
	errChan = make(chan error, numJobs)
	wg.Add(numJobs)

	newMap = reflect.MakeMap(v.Type())

	for _, kVal = range v.MapKeys() {
		vVal = v.MapIndex(kVal)
		go func(key, val reflect.Value) {
			var mapErr error
			var newKey reflect.Value
			var newVal reflect.Value

			newKey = reflect.New(key.Type()).Elem()
			newVal = reflect.New(val.Type()).Elem()

			newKey.Set(key.Convert(newKey.Type()))
			newVal.Set(val.Convert(newVal.Type()))

			defer wg.Done()

			// key
			if key.Kind() == reflect.String {
				if mapErr = interpolateStringReflect(newKey); mapErr != nil {
					errChan <- mapErr
					return
				}
			} else {
				if mapErr = interpolateValue(newKey); mapErr != nil {
					errChan <- mapErr
					return
				}
			}
			// value
			if val.Kind() == reflect.String {
				if mapErr = interpolateStringReflect(newVal); mapErr != nil {
					errChan <- mapErr
					return
				}
			} else {
				if mapErr = interpolateValue(newVal); mapErr != nil {
					errChan <- mapErr
					return
				}
			}

			newMap.SetMapIndex(newKey.Convert(key.Type()), newVal.Convert(key.Type()))
		}(kVal, vVal)
	}

	go func() {
		wg.Wait()
		close(errChan)
		doneChan <- true
	}()

	<-doneChan

	for i := 0; i < numJobs; i++ {
		if err = <-errChan; err != nil {
			mErr.AddError(err)
			err = nil
		}
	}

	if !mErr.IsEmpty() {
		err = mErr
		return
	}

	v.Set(newMap.Convert(v.Type()))

	return
}

// interpolateSlice is used by Interpolate for slices and arrays. v should be a reflect.Value of a slice/array.
func interpolateSlice(v reflect.Value) (err error) {

	var wg sync.WaitGroup
	var errChan chan error
	var numJobs int
	var doneChan chan bool = make(chan bool, 1)
	var mErr *multierr.MultiError = multierr.NewMultiError(nil)
	var t reflect.Type = v.Type()
	var kind reflect.Kind = t.Kind()

	switch kind {
	case reflect.Slice:
		if v.IsNil() || v.IsZero() || !v.IsValid() {
			return
		}
	case reflect.Array:
		if v.IsZero() || !v.IsValid() {
			return
		}
	default:
		err = errs.ErrBadType
		return
	}

	numJobs = v.Len()
	errChan = make(chan error, numJobs)
	wg.Add(numJobs)

	for i := 0; i < v.Len(); i++ {
		go func(idx int) {
			var sErr error

			defer wg.Done()

			if v.Index(idx).Kind() == reflect.String {
				if sErr = interpolateStringReflect(v.Index(idx)); sErr != nil {
					errChan <- sErr
					return
				}
			} else {
				if sErr = interpolateValue(v.Index(idx)); sErr != nil {
					errChan <- sErr
					return
				}
			}
		}(i)
	}

	go func() {
		wg.Wait()
		close(errChan)
		doneChan <- true
	}()

	<-doneChan

	for i := 0; i < numJobs; i++ {
		if err = <-errChan; err != nil {
			mErr.AddError(err)
			err = nil
		}
	}

	if !mErr.IsEmpty() {
		err = mErr
		return
	}

	return
}

// interpolateStringReflect is used for structs/nested strings using reflection.
func interpolateStringReflect(v reflect.Value) (err error) {

	var strVal string

	if v.Kind() != reflect.String {
		err = errs.ErrBadType
		return
	}

	if strVal, err = interpolateString(v.String()); err != nil {
		return
	}

	v.Set(reflect.ValueOf(strVal).Convert(v.Type()))

	return
}

// interpolateStruct is used by Interpolate for structs. v should be a reflect.Value of a struct.
func interpolateStruct(v reflect.Value) (err error) {

	var field reflect.StructField
	var fieldVal reflect.Value
	var wg sync.WaitGroup
	var errChan chan error
	var numJobs int
	var doneChan chan bool = make(chan bool, 1)
	var mErr *multierr.MultiError = multierr.NewMultiError(nil)
	var t reflect.Type = v.Type()
	var kind reflect.Kind = t.Kind()

	if kind != reflect.Struct {
		err = errs.ErrBadType
		return
	}

	numJobs = v.NumField()
	wg.Add(numJobs)
	errChan = make(chan error, numJobs)

	for i := 0; i < v.NumField(); i++ {
		field = t.Field(i)
		fieldVal = v.Field(i)

		go func(f reflect.StructField, fv reflect.Value) {
			var fErr error

			defer wg.Done()

			if fErr = interpolateStructField(f, fv); fErr != nil {
				errChan <- fErr
				return
			}
		}(field, fieldVal)
	}

	go func() {
		wg.Wait()
		close(errChan)
		doneChan <- true
	}()

	<-doneChan

	for i := 0; i < numJobs; i++ {
		if err = <-errChan; err != nil {
			mErr.AddError(err)
			err = nil
		}
	}

	if !mErr.IsEmpty() {
		err = mErr
		return
	}

	return
}

// interpolateStructField interpolates a struct field.
func interpolateStructField(field reflect.StructField, v reflect.Value) (err error) {

	var parsedTagOpts map[string]bool

	if !v.CanSet() {
		return
	}

	// Skip if explicitly instructed to do so.
	parsedTagOpts = structutils.TagToBoolMap(field, StructTagInterpolate, structutils.TagMapTrim)
	if parsedTagOpts["-"] {
		return
	}

	if v.Kind() == reflect.Ptr {
		err = interpolateStructField(field, v.Elem())
	} else {
		err = interpolateValue(v)
	}

	return
}

// interpolateValue is a dispatcher for a reflect value.
func interpolateValue(v reflect.Value) (err error) {

	var kind reflect.Kind = v.Kind()

	switch kind {
	case reflect.Ptr:
		if v.IsNil() || v.IsZero() || !v.IsValid() {
			return
		}
		v = v.Elem()
		if err = interpolateValue(v); err != nil {
			return
		}
	case reflect.String:
		if err = interpolateStringReflect(v); err != nil {
			return
		}
		return
	case reflect.Slice, reflect.Array:
		if err = interpolateSlice(v); err != nil {
		}
	case reflect.Map:
		if err = interpolateMap(v); err != nil {
			return
		}
	case reflect.Struct:
		if err = interpolateStruct(v); err != nil {
			return
		}
	}

	return
}