package node

import (
	"fmt"
	"reflect"
	"strings"

	"github.com/warewulf/warewulf/internal/pkg/util"
	"github.com/warewulf/warewulf/internal/pkg/wwlog"
)

/*
Populates a NodeConf struct (the one which goes to disk) from a
NodeInfo (which just lives in memory), with the values from all
the underlying entries using GetReal, so just the explicit values
go do disk.
*/
func (nodeConf *NodeConf) GetRealFrom(nodeInfo NodeInfo) {
	recursiveGetter(&nodeInfo, nodeConf, (*Entry).GetReal, (*Entry).GetRealSlice)
}

/*
Populates a NodeConf struct from a NodeInfo, with the combined
values from the underlying entries using Get.
*/
func (nodeConf *NodeConf) GetFrom(nodeInfo NodeInfo) {
	recursiveGetter(&nodeInfo, nodeConf, (*Entry).Get, (*Entry).GetSlice)
}

/*
Abstract function which populates a NodeConf from the given NodeInfo
via getter functions. Calls recursive itself for nested structures.
Panics if the NodeConf has fields which are not type of string,[]string,map[string]*ptr
*/
func recursiveGetter(
	source, target interface{},
	getter func(*Entry) string,
	getterSlice func(*Entry) []string) {
	sourceValue := reflect.ValueOf(source)
	targetType := reflect.TypeOf(target)
	targetValue := reflect.ValueOf(target)
	if targetValue.Elem().Kind() == reflect.Struct && sourceValue.Elem().Kind() == reflect.Struct {
		for i := 0; i < targetType.Elem().NumField(); i++ {
			sourceValueMatched := sourceValue.Elem().FieldByName(targetType.Elem().Field(i).Name)
			if sourceValueMatched.IsValid() {
				if sourceValueMatched.Type() == reflect.TypeOf(Entry{}) {
					// get the fields which are part of the struct
					switch targetValue.Elem().Field(i).Type() {
					case reflect.TypeOf(""):
						newValue := (targetValue.Elem().Field(i).Addr().Interface()).(*string)
						source := sourceValueMatched.Interface().(Entry)
						*newValue = getter(&source)
					case reflect.TypeOf([]string{}):
						newValue := (targetValue.Elem().Field(i).Addr().Interface()).(*[]string)
						source := sourceValueMatched.Interface().(Entry)
						*newValue = getterSlice(&source)
					default:
						panic(fmt.Errorf("can't convert an Entry to %s", targetValue.Elem().Field(i).Type()))
					}
				} else if sourceValueMatched.Kind() == reflect.Ptr {
					// if we get a pointer, initialize if empty and then have a recursive call
					if targetValue.Elem().Field(i).IsZero() {
						targetValue.Elem().Field(i).Set(reflect.New(targetType.Elem().Field(i).Type.Elem()))
					}
					recursiveGetter(sourceValueMatched.Interface(), targetValue.Elem().Field(i).Interface(), getter, getterSlice)
				} else if sourceValueMatched.Type().Kind() == reflect.Map {
					if targetValue.Elem().Field(i).IsZero() {
						targetValue.Elem().Field(i).Set(reflect.MakeMap(targetType.Elem().Field(i).Type))
					}
					sourceIter := sourceValueMatched.MapRange()
					if sourceValueMatched.Type() == reflect.TypeOf(map[string]*Entry{}) {
						// go over a simple map with strings
						for sourceIter.Next() {
							if !targetValue.Elem().Field(i).MapIndex(sourceIter.Key()).IsValid() {
								// Only write entries for which have real values. This matters for
								// tags, as empty map elements can be created without this check
								// The alternative was following check:
								// if ((sourceIter.Value().Interface()).(*Entry)).GotReal() {
								//  but this one failed for tags in templates
								str := getter((sourceIter.Value().Interface()).(*Entry))
								if str != "" {
									targetValue.Elem().Field(i).SetMapIndex(sourceIter.Key(), reflect.ValueOf(str))
								}
							}
						}
					} else {
						// now the complicated map which contains pointers to objects
						for sourceIter.Next() {
							if !targetValue.Elem().Field(i).MapIndex(sourceIter.Key()).IsValid() {
								newPtr := reflect.New(targetType.Elem().Field(i).Type.Elem().Elem())
								targetValue.Elem().Field(i).SetMapIndex(sourceIter.Key(), newPtr)
							}
							recursiveGetter(sourceIter.Value().Interface(), targetValue.Elem().Field(i).MapIndex(sourceIter.Key()).Interface(), getter, getterSlice)

						}
					}
				}
			}
		}
	}
}

/*
Populates all fields of NodeInfo with Set from the
values of NodeConf.
*/
func (node *NodeInfo) SetFrom(n *NodeConf) {
	setWrap := func(entr *Entry, val string, nameArg string) {
		entr.Set(val)
	}
	setSliceWrap := func(entr *Entry, val []string, nameArg string) {
		entr.SetSlice(val)
	}
	recursiveSetter(n, node, "", setWrap, setSliceWrap)
}

/*
Populates all fields of NodeInfo with SetAlt from the
values of NodeConf. The string profileName is used to
determine from which source/NodeInfo the entry came
from.
*/
func (node *NodeInfo) SetAltFrom(n *NodeConf, profileName string) {
	recursiveSetter(n, node, profileName, (*Entry).SetAlt, (*Entry).SetAltSlice)
}

/*
Populates all fields of NodeInfo with SetDefault from the
values of NodeConf.
*/
func (node *NodeInfo) SetDefFrom(n *NodeConf) {
	setWrap := func(entr *Entry, val string, nameArg string) {
		entr.SetDefault(val)
	}
	setSliceWrap := func(entr *Entry, val []string, nameArg string) {
		entr.SetDefaultSlice(val)
	}
	recursiveSetter(n, node, "", setWrap, setSliceWrap)
}

func SetDefFrom(source, target interface{}) {
	setWrap := func(entr *Entry, val string, nameArg string) {
		entr.SetDefault(val)
	}
	setSliceWrap := func(entr *Entry, val []string, nameArg string) {
		entr.SetDefaultSlice(val)
	}
	recursiveSetter(source, target, "", setWrap, setSliceWrap)

}

/*
Abstract function which populates a NodeInfo from a NodeConf via
setter functions. Panics if other type than string, []string *ptr is used in NodeConf.
*/
func recursiveSetter(source, target interface{}, nameArg string, setter func(*Entry, string, string),
	setterSlice func(*Entry, []string, string)) {
	sourceValue := reflect.ValueOf(source)
	targetType := reflect.TypeOf(target)
	targetValue := reflect.ValueOf(target)
	if targetValue.Elem().Kind() == reflect.Struct && sourceValue.Elem().Kind() == reflect.Struct {
		for i := 0; i < targetType.Elem().NumField(); i++ {
			sourceValueMatched := sourceValue.Elem().FieldByName(targetType.Elem().Field(i).Name)
			if sourceValueMatched.IsValid() {
				if targetValue.Elem().Field(i).Type() == reflect.TypeOf(Entry{}) {
					// get the fields which are part of the struct
					switch sourceValueMatched.Type() {
					case reflect.TypeOf(""):
						setter(targetValue.Elem().Field(i).Addr().Interface().(*Entry), sourceValueMatched.String(), nameArg)
					case reflect.TypeOf([]string{}):
						setterSlice(targetValue.Elem().Field(i).Addr().Interface().(*Entry), sourceValueMatched.Interface().([]string), nameArg)
					default:
						panic(fmt.Errorf("can't convert an Entry to %s", targetValue.Elem().Field(i).Type()))
					}
				} else if sourceValueMatched.Kind() == reflect.Ptr {
					// if we get a pointer, initialize if empty and then have a recursive call
					if targetValue.Elem().Field(i).IsZero() {
						targetValue.Elem().Field(i).Set(reflect.New(targetType.Elem().Field(i).Type.Elem()))
					}
					recursiveSetter(sourceValueMatched.Interface(), targetValue.Elem().Field(i).Interface(), nameArg, setter, setterSlice)
				} else if sourceValueMatched.Type().Kind() == reflect.Map {
					if targetValue.Elem().Field(i).IsZero() {
						targetValue.Elem().Field(i).Set(reflect.MakeMap(targetType.Elem().Field(i).Type))
					}
					// delete a map element which is only in the target
					if targetValue.Elem().Field(i).Len() > 0 && targetValue.Elem().Field(i).Len() < 0 {
						sourceIter := sourceValueMatched.MapRange()
						targetIter := targetValue.Elem().Field(i).MapRange()
						for targetIter.Next() {
							sameKey := false
							for sourceIter.Next() {
								if sourceIter.Key() == targetIter.Key() {
									sameKey = true
								}
							}
							if !sameKey {
								targetValue.Elem().Field(i).SetMapIndex(targetIter.Key(), reflect.Value{})
							}
						}
					}
					sourceIter := sourceValueMatched.MapRange()
					if sourceValueMatched.Type().Elem() == reflect.TypeOf("") {
						// go over a simple map with strings
						for sourceIter.Next() {
							if !targetValue.Elem().Field(i).MapIndex(sourceIter.Key()).IsValid() {
								newEntr := new(Entry)
								setter(newEntr, sourceIter.Value().String(), nameArg)
								targetValue.Elem().Field(i).SetMapIndex(sourceIter.Key(), reflect.ValueOf(newEntr))
							} else {
								// update the entry with latest value
								if entry, ok := targetValue.Elem().Field(i).MapIndex(sourceIter.Key()).Interface().(*Entry); ok {
									setter(entry, sourceIter.Value().String(), nameArg)
								}
							}
						}
					} else {
						// now the complicated map which contains pointers to objects
						for sourceIter.Next() {
							if !targetValue.Elem().Field(i).MapIndex(sourceIter.Key()).IsValid() {
								newPtr := reflect.New(targetType.Elem().Field(i).Type.Elem().Elem())
								targetValue.Elem().Field(i).SetMapIndex(sourceIter.Key(), newPtr)
							}
							recursiveSetter(sourceIter.Value().Interface(), targetValue.Elem().Field(i).MapIndex(sourceIter.Key()).Interface(), nameArg, setter, setterSlice)

						}
					}
				}
			}
		}
	}

}

/*
Flattens out a NodeConf, which means if there are no explicit values in *IpmiConf
or *KernelConf, these pointer will set to nil. This will remove something like
ipmi: {} from nodes.conf
*/
func (info *NodeConf) Flatten() {
	recursiveFlatten(info)
}
func recursiveFlatten(strct interface{}) {
	confType := reflect.TypeOf(strct)
	confVal := reflect.ValueOf(strct)
	for j := 0; j < confType.Elem().NumField(); j++ {
		if confVal.Elem().Field(j).Type().Kind() == reflect.Ptr && !confVal.Elem().Field(j).IsNil() {
			// iterate now over the ptr fields
			setToNil := true
			nestedType := reflect.TypeOf(confVal.Elem().Field(j).Interface())
			nestedVal := reflect.ValueOf(confVal.Elem().Field(j).Interface())
			for i := 0; i < nestedType.Elem().NumField(); i++ {
				if nestedType.Elem().Field(i).Type.Kind() == reflect.String &&
					nestedVal.Elem().Field(i).Interface().(string) != "" {
					setToNil = false
				} else if nestedType.Elem().Field(i).Type == reflect.TypeOf([]string{}) &&
					len(nestedVal.Elem().Field(i).Interface().([]string)) != 0 {
					setToNil = false
				} else if nestedType.Elem().Field(i).Type == reflect.TypeOf(map[string]string{}) &&
					len(nestedVal.Elem().Field(i).Interface().(map[string]string)) != 0 {
					setToNil = false
				}
			}
			if setToNil {
				confVal.Elem().Field(j).Set(reflect.Zero(confVal.Elem().Field(j).Type()))
			}
		}
	}
}

/*
Create a string slice, where every element represents a yaml entry, used for node/profile edit
in order to get a summary of all available elements
*/
func UnmarshalConf(obj interface{}, excludeList []string) (lines []string) {
	objType := reflect.TypeOf(obj)
	// now iterate of every field
	for i := 0; i < objType.NumField(); i++ {
		if objType.Field(i).Tag.Get("comment") != "" {
			if ymlStr, ok := getYamlString(objType.Field(i), excludeList); ok {
				lines = append(lines, ymlStr...)
			}
		}
		if objType.Field(i).Type.Kind() == reflect.Ptr && objType.Field(i).Tag.Get("yaml") != "" {
			typeLine := objType.Field(i).Tag.Get("yaml")
			if len(strings.Split(typeLine, ",")) > 1 {
				typeLine = strings.Split(typeLine, ",")[0] + ":"
			}
			lines = append(lines, typeLine)
			nestedLine := UnmarshalConf(reflect.New(objType.Field(i).Type.Elem()).Elem().Interface(), excludeList)
			for _, ln := range nestedLine {
				lines = append(lines, "  "+ln)
			}
		} else if objType.Field(i).Type.Kind() == reflect.Map && objType.Field(i).Type.Elem().Kind() == reflect.Ptr {
			typeLine := objType.Field(i).Tag.Get("yaml")
			if len(strings.Split(typeLine, ",")) > 1 {
				typeLine = strings.Split(typeLine, ",")[0] + ":"
			}
			lines = append(lines, typeLine, "  element:")
			nestedLine := UnmarshalConf(reflect.New(objType.Field(i).Type.Elem().Elem()).Elem().Interface(), excludeList)
			for _, ln := range nestedLine {
				lines = append(lines, "    "+ln)
			}
		}
	}
	return lines
}

/*
Get the string of the yaml tag
*/

func getYamlString(myType reflect.StructField, excludeList []string) ([]string, bool) {
	ymlStr := myType.Tag.Get("yaml")
	if len(strings.Split(ymlStr, ",")) > 1 {
		ymlStr = strings.Split(ymlStr, ",")[0]
	}
	if util.InSlice(excludeList, ymlStr) {
		return []string{""}, false
	} else if myType.Tag.Get("comment") == "" && myType.Type.Kind() == reflect.String {
		return []string{""}, false
	}
	if myType.Type.Kind() == reflect.String {
		fieldType := myType.Tag.Get("type")
		if fieldType == "" {
			fieldType = "string"
		}
		ymlStr += ": " + fieldType
		return []string{ymlStr}, true
	} else if myType.Type == reflect.TypeOf([]string{}) {
		return []string{ymlStr + ":", "  - string"}, true
	} else if myType.Type == reflect.TypeOf(map[string]string{}) {
		return []string{ymlStr + ":", "  key: value"}, true
	} else if myType.Type.Kind() == reflect.Ptr {
		return []string{ymlStr + ":"}, true
	}
	return []string{ymlStr}, true
}

/*
Set the field of the NodeConf with the given lopt name, returns true if the
field was found. String slices must be comma separated. Network must have the form
net.$NETNAME.lopt or netname.$NETNAME.lopt
*/
func (nodeConf *NodeConf) SetLopt(lopt string, value string) (found bool) {
	found = false
	nodeInfoType := reflect.TypeOf(nodeConf)
	nodeInfoVal := reflect.ValueOf(nodeConf)
	// try to find the normal fields, networks come later
	for i := 0; i < nodeInfoVal.Elem().NumField(); i++ {
		if nodeInfoType.Elem().Field(i).Tag.Get("lopt") == lopt {
			if nodeInfoType.Elem().Field(i).Type.Kind() == reflect.String {
				wwlog.Verbose("Found lopt %s mapping to %s, setting to %s\n",
					lopt, nodeInfoType.Elem().Field(i).Name, value)
				confVal := nodeInfoVal.Elem().Field(i).Addr().Interface().(*string)
				*confVal = value
				found = true
			} else if nodeInfoType.Elem().Field(i).Type == reflect.TypeOf([]string{}) {
				wwlog.Verbose("Found lopt %s mapping to %s, setting to %s\n",
					lopt, nodeInfoType.Elem().Field(i).Name, value)
				confVal := nodeInfoVal.Elem().Field(i).Addr().Interface().(*[]string)
				*confVal = strings.Split(value, ",")
				found = true
			}
		}
	}
	// check network
	loptSlice := strings.Split(lopt, ".")
	wwlog.Debug("Trying to get network out of %s\n", loptSlice)
	if !found && len(loptSlice) == 3 && (loptSlice[0] == "net" || loptSlice[0] == "network" || loptSlice[0] == "netname") {
		if nodeConf.NetDevs == nil {
			nodeConf.NetDevs = make(map[string]*NetDevs)
		}
		if nodeConf.NetDevs[loptSlice[1]] == nil {
			nodeConf.NetDevs[loptSlice[1]] = new(NetDevs)
		}
		netInfoType := reflect.TypeOf(nodeConf.NetDevs[loptSlice[1]])
		netInfoVal := reflect.ValueOf(nodeConf.NetDevs[loptSlice[1]])
		for i := 0; i < netInfoVal.Elem().NumField(); i++ {
			if netInfoType.Elem().Field(i).Tag.Get("lopt") == loptSlice[2] {
				if netInfoType.Elem().Field(i).Type.Kind() == reflect.String {
					wwlog.Verbose("Found lopt %s for network %s mapping to %s, setting to %s\n",
						lopt, loptSlice[1], netInfoType.Elem().Field(i).Name, value)
					confVal := netInfoVal.Elem().Field(i).Addr().Interface().(*string)
					*confVal = value
					found = true
				} else if netInfoType.Elem().Field(i).Type == reflect.TypeOf([]string{}) {
					wwlog.Verbose("Found lopt %s for network %s mapping to %s, setting to %s\n",
						lopt, loptSlice[1], netInfoType.Elem().Field(i).Name, value)
					confVal := netInfoVal.Elem().Field(i).Addr().Interface().(*[]string)
					*confVal = strings.Split(value, ",")
					found = true
				}
			}
		}
	}
	return found
}
