package node

import (
	"fmt"
	"net"
	"reflect"
	"strconv"
	"strings"

	"github.com/spf13/cobra"
	"github.com/warewulf/warewulf/internal/pkg/util"
)

/*
Create cmd line flags from the NodeConf fields. Returns a []func() where every function
must be called, as the commandline parser returns e.g. netip.IP objects which must be parsedf
back to strings.
*/
func (nodeConf *NodeConf) CreateFlags(baseCmd *cobra.Command, excludeList []string) (converters []func() error) {
	return RecursiveCreateFlags(nodeConf, baseCmd, excludeList)
}

func RecursiveCreateFlags(obj interface{}, baseCmd *cobra.Command, excludeList []string) (converters []func() error) {
	// now iterate of every field
	nodeInfoType := reflect.TypeOf(obj)
	nodeInfoVal := reflect.ValueOf(obj)
	for i := 0; i < nodeInfoVal.Elem().NumField(); i++ {
		if nodeInfoType.Elem().Field(i).Tag.Get("comment") != "" &&
			!util.InSlice(excludeList, nodeInfoType.Elem().Field(i).Tag.Get("lopt")) {
			field := nodeInfoVal.Elem().Field(i)
			converters = append(converters, createFlags(baseCmd, excludeList, nodeInfoType.Elem().Field(i), &field)...)
		} else if nodeInfoType.Elem().Field(i).Type.Kind() == reflect.Ptr {
			newConv := RecursiveCreateFlags(nodeInfoVal.Elem().Field(i).Interface(), baseCmd, excludeList)
			converters = append(converters, newConv...)

		} else if nodeInfoType.Elem().Field(i).Type.Kind() == reflect.Map &&
			nodeInfoType.Elem().Field(i).Type != reflect.TypeOf(map[string]string{}) {
			// add a map with key UNDEF so that it can hold values N.B. UNDEF can never be added through command line
			key := reflect.ValueOf("UNDEF")
			if nodeInfoVal.Elem().Field(i).Len() == 0 {
				if nodeInfoVal.Elem().Field(i).IsNil() {
					nodeInfoVal.Elem().Field(i).Set(reflect.MakeMap(nodeInfoType.Elem().Field(i).Type))
				}
				newPtr := reflect.New(nodeInfoType.Elem().Field(i).Type.Elem().Elem())
				nodeInfoVal.Elem().Field(i).SetMapIndex(key, newPtr)
			} else {
				key = nodeInfoVal.Elem().Field(i).MapKeys()[0]
			}
			newConv := RecursiveCreateFlags(nodeInfoVal.Elem().Field(i).MapIndex(key).Interface(), baseCmd, excludeList)
			converters = append(converters, newConv...)
		}
	}
	return converters
}

/*
Helper function to create the different PersistentFlags() for different types.
*/
func createFlags(baseCmd *cobra.Command, excludeList []string,
	myType reflect.StructField, myVal *reflect.Value) (converters []func() error) {
	if myType.Tag.Get("lopt") != "" {
		if myType.Type.Kind() == reflect.String {
			ptr := myVal.Addr().Interface().(*string)
			switch myType.Tag.Get("type") {
			case "uint":
				converters = append(converters, func() error {
					if !util.InSlice(GetUnsetVerbs(), *ptr) && *ptr != "" {
						_, err := strconv.ParseUint(*ptr, 10, 32)
						if err != nil {
							return err
						}
					}
					return nil
				})
				if myType.Tag.Get("sopt") != "" {
					baseCmd.PersistentFlags().StringVarP(ptr,
						myType.Tag.Get("lopt"),
						myType.Tag.Get("sopt"),
						myType.Tag.Get("default"),
						myType.Tag.Get("comment"))
				} else {
					baseCmd.PersistentFlags().StringVar(ptr,
						myType.Tag.Get("lopt"),
						myType.Tag.Get("default"),
						myType.Tag.Get("comment"))
				}
			case "bool":
				/*
					Can't use the bool var from pflag as we need the UNSET verbs to be passwd correctly
				*/
				converters = append(converters, func() error {
					if !util.InSlice(GetUnsetVerbs(), *ptr) && *ptr != "" {
						if strings.ToLower(*ptr) == "yes" {
							*ptr = "true"
							return nil
						}
						if strings.ToLower(*ptr) == "no" {
							*ptr = "false"
							return nil
						}
						val, err := strconv.ParseBool(*ptr)
						if err != nil {
							return fmt.Errorf("commandline option %s needs to be bool", myType.Tag.Get("lopt"))
						}
						*ptr = strconv.FormatBool(val)
					}
					return nil
				})
				if myType.Tag.Get("sopt") != "" {
					baseCmd.PersistentFlags().StringVarP(ptr,
						myType.Tag.Get("lopt"),
						myType.Tag.Get("sopt"),
						"",
						myType.Tag.Get("comment"))
				} else {
					baseCmd.PersistentFlags().StringVar(ptr,
						myType.Tag.Get("lopt"),
						"",
						myType.Tag.Get("comment"))
				}
				baseCmd.PersistentFlags().Lookup(myType.Tag.Get("lopt")).NoOptDefVal = "true"
			case "IP":
				converters = append(converters, func() error {
					if !util.InSlice(GetUnsetVerbs(), *ptr) && *ptr != "" {
						ipval := net.ParseIP(*ptr)
						if ipval == nil {
							return fmt.Errorf("commandline option %s needs to be an IP address", myType.Tag.Get("lopt"))
						}
						*ptr = ipval.String()
					}
					return nil
				})
				if myType.Tag.Get("sopt") != "" {
					baseCmd.PersistentFlags().StringVarP(ptr,
						myType.Tag.Get("lopt"),
						myType.Tag.Get("sopt"),
						myType.Tag.Get("default"),
						myType.Tag.Get("comment"))
				} else {
					baseCmd.PersistentFlags().StringVar(ptr,
						myType.Tag.Get("lopt"),
						myType.Tag.Get("default"),
						myType.Tag.Get("comment"))
				}
			case "IPMask":
				defaultConv := net.ParseIP(myType.Tag.Get("default")).DefaultMask()
				var valueRaw net.IPMask
				converters = append(converters, func() error {
					if valueRaw != nil {
						*ptr = valueRaw.String()
						return nil
					} else {
						return fmt.Errorf("could not parse %s to IP", valueRaw.String())
					}
				})
				if myType.Tag.Get("sopt") != "" {
					baseCmd.PersistentFlags().IPMaskVarP(&valueRaw,
						myType.Tag.Get("lopt"),
						myType.Tag.Get("sopt"),
						defaultConv,
						myType.Tag.Get("comment"))
				} else {
					baseCmd.PersistentFlags().IPMaskVar(&valueRaw,
						myType.Tag.Get("lopt"),
						defaultConv,
						myType.Tag.Get("comment"))
				}
			case "MAC":
				converters = append(converters, func() error {
					if !util.InSlice(GetUnsetVerbs(), *ptr) && *ptr != "" {
						myMac, err := net.ParseMAC(*ptr)
						if err != nil {
							return err
						}
						*ptr = myMac.String()
					}
					return nil
				})
				if myType.Tag.Get("sopt") != "" {
					baseCmd.PersistentFlags().StringVarP(ptr,
						myType.Tag.Get("lopt"),
						myType.Tag.Get("sopt"),
						"",
						myType.Tag.Get("comment"))
				} else {
					baseCmd.PersistentFlags().StringVar(ptr,
						myType.Tag.Get("lopt"),
						"",
						myType.Tag.Get("comment"))
				}
			default:
				if myType.Tag.Get("sopt") != "" {
					baseCmd.PersistentFlags().StringVarP(ptr,
						myType.Tag.Get("lopt"),
						myType.Tag.Get("sopt"),
						myType.Tag.Get("default"),
						myType.Tag.Get("comment"))
				} else {
					baseCmd.PersistentFlags().StringVar(ptr,
						myType.Tag.Get("lopt"),
						myType.Tag.Get("default"),
						myType.Tag.Get("comment"))
				}
			}
		} else if myType.Type == reflect.TypeOf([]string{}) {
			ptr := myVal.Addr().Interface().(*[]string)
			if myType.Tag.Get("sopt") != "" {
				baseCmd.PersistentFlags().StringSliceVarP(ptr,
					myType.Tag.Get("lopt"),
					myType.Tag.Get("sopt"),
					[]string{myType.Tag.Get("default")},
					myType.Tag.Get("comment"))
			} else if !util.InSlice(excludeList, myType.Tag.Get("lopt")) {
				baseCmd.PersistentFlags().StringSliceVar(ptr,
					myType.Tag.Get("lopt"),
					[]string{myType.Tag.Get("default")},
					myType.Tag.Get("comment"))

			}
		} else if myType.Type == reflect.TypeOf(map[string]string{}) {
			ptr := myVal.Addr().Interface().(*map[string]string)
			if myType.Tag.Get("sopt") != "" {
				baseCmd.PersistentFlags().StringToStringVarP(ptr,
					myType.Tag.Get("lopt"),
					myType.Tag.Get("sopt"),
					map[string]string{}, // empty default!
					myType.Tag.Get("comment"))
			} else if !util.InSlice(excludeList, myType.Tag.Get("lopt")) {
				baseCmd.PersistentFlags().StringToStringVar(ptr,
					myType.Tag.Get("lopt"),
					map[string]string{}, // empty default!
					myType.Tag.Get("comment"))

			}
		}
	}
	return converters
}
