package node

import (
	"errors"
	"fmt"
	"os"
	"path"
	"sort"
	"strings"

	warewulfconf "github.com/warewulf/warewulf/internal/pkg/config"
	"github.com/warewulf/warewulf/internal/pkg/wwlog"

	"gopkg.in/yaml.v2"
)

var (
	ConfigFile    string
	DefaultConfig string
)

// used as fallback if DefaultConfig can't be read
var FallBackConf = `---
defaultnode:
  runtime overlay:
  - generic
  system overlay:
  - wwinit
  kernel:
    args: quiet crashkernel=no vga=791 net.naming-scheme=v238
  init: /sbin/init
  root: initramfs
  ipxe template: default
  boot method: ipxe
  profiles:
  - default
  network devices:
    dummy:
      type: ethernet
      netmask: 255.255.255.0
      onboot: true`

/*
Creates a new nodeDb object from the on-disk configuration
*/
func New() (NodeYaml, error) {
	conf := warewulfconf.Get()
	if ConfigFile == "" {
		ConfigFile = path.Join(conf.Paths.Sysconfdir, "warewulf/nodes.conf")
	}
	if DefaultConfig == "" {
		DefaultConfig = path.Join(conf.Warewulf.DataStore, "warewulf/defaults.conf")
	}
	wwlog.Verbose("Opening node configuration file: %s", ConfigFile)
	data, err := os.ReadFile(ConfigFile)
	if err != nil {
		return NodeYaml{}, err
	}
	return Parse(data)
}

// Parse constructs a new nodeDb object from an input YAML
// document. Passes any errors return from yaml.Unmarshal. Returns an
// error if any parsed value is not of a valid type for the given
// parameter.
func Parse(data []byte) (NodeYaml, error) {
	var ret NodeYaml
	var err error
	wwlog.Debug("Unmarshaling the node configuration")
	err = yaml.Unmarshal(data, &ret)
	if err != nil {
		return ret, err
	}
	wwlog.Debug("Checking nodes for types")
	if ret.Nodes == nil {
		ret.Nodes = map[string]*NodeConf{}
	}
	for nodeName, node := range ret.Nodes {
		err = node.Check()
		if err != nil {
			wwlog.Warn("node: %s parsing error: %s", nodeName, err)
			return ret, err
		}
	}
	if ret.NodeProfiles == nil {
		ret.NodeProfiles = map[string]*NodeConf{}
	}
	for profileName, profile := range ret.NodeProfiles {
		err = profile.Check()
		if err != nil {
			wwlog.Warn("node: %s parsing error: %s", profileName, err)
			return ret, err
		}
	}

	wwlog.Debug("Returning node object")
	return ret, nil
}

/*
Get all the nodes of a configuration. This function also merges
the nodes with the given profiles and set the default values
for every node
*/
func (config *NodeYaml) FindAllNodes() ([]NodeInfo, error) {
	var ret []NodeInfo
	var defConf map[string]*NodeConf
	wwlog.Verbose("Opening defaults from file failed %s\n", DefaultConfig)
	defData, err := os.ReadFile(DefaultConfig)
	if err != nil {
		wwlog.Verbose("Couldn't read DefaultConfig :%s\n", err)
		wwlog.Verbose("Using building defaults")
		defData = []byte(FallBackConf)
	}
	wwlog.Debug("Unmarshalling default config\n")
	err = yaml.Unmarshal(defData, &defConf)
	if err != nil {
		wwlog.Verbose("Couldn't unmarshall defaults from file :%s\n", err)
		err = yaml.Unmarshal([]byte(FallBackConf), &defConf)
		if err != nil {
			wwlog.Warn("Could not get any defaults")
		}
	}
	var defConfNet *NetDevs
	if _, ok := defConf["defaultnode"]; ok {
		if _, ok := defConf["defaultnode"].NetDevs["dummy"]; ok {
			defConfNet = defConf["defaultnode"].NetDevs["dummy"]
		}
		defConf["defaultnode"].NetDevs = nil
	}

	wwlog.Debug("Finding all nodes...")
	for nodename, node := range config.Nodes {
		var n NodeInfo

		wwlog.Debug("In node loop: %s", nodename)
		n.NetDevs = make(map[string]*NetDevEntry)
		n.Tags = make(map[string]*Entry)
		n.Kernel = new(KernelEntry)
		n.Ipmi = new(IpmiEntry)
		n.SetDefFrom(defConf["defaultnode"])
		fullname := strings.SplitN(nodename, ".", 2)
		if len(fullname) > 1 {
			n.ClusterName.SetDefault(fullname[1])
		}
		// special handling for profile to get the default one
		if len(node.Profiles) == 0 {
			n.Profiles.SetSlice([]string{"default"})
		} else {
			n.Profiles.SetSlice(node.Profiles)
		}
		// node explicitly nodename field in NodeConf
		n.Id.Set(nodename)
		// backward compatibility
		for keyname, key := range node.Keys {
			node.Tags[keyname] = key
			delete(node.Keys, keyname)
		}
		err = node.Check()
		if err != nil {
			return nil, fmt.Errorf("node: %s check error: %s", nodename, err)
		}
		n.SetFrom(node)
		// only now the netdevs start to exist so that default values can be set
		for _, netdev := range n.NetDevs {
			SetDefFrom(defConfNet, netdev)
		}
		// backward compatibility
		n.Ipmi.Ipaddr.Set(node.IpmiIpaddr)
		n.Ipmi.Netmask.Set(node.IpmiNetmask)
		n.Ipmi.Port.Set(node.IpmiPort)
		n.Ipmi.Gateway.Set(node.IpmiGateway)
		n.Ipmi.UserName.Set(node.IpmiUserName)
		n.Ipmi.Password.Set(node.IpmiPassword)
		n.Ipmi.Interface.Set(node.IpmiInterface)
		n.Ipmi.Write.Set(node.IpmiWrite)
		n.Kernel.Args.Set(node.KernelArgs)
		n.Kernel.Override.Set(node.KernelOverride)
		n.Kernel.Override.Set(node.KernelVersion)
		// delete deprecated structures so that they do not get unmarshalled
		node.IpmiIpaddr = ""
		node.IpmiNetmask = ""
		node.IpmiGateway = ""
		node.IpmiUserName = ""
		node.IpmiPassword = ""
		node.IpmiInterface = ""
		node.IpmiWrite = ""
		node.KernelArgs = ""
		node.KernelOverride = ""
		node.KernelVersion = ""
		// Merge Keys into Tags for backwards compatibility
		if len(node.Tags) == 0 {
			node.Tags = make(map[string]string)
		}

		for _, profileName := range n.Profiles.GetSlice() {
			if _, ok := config.NodeProfiles[profileName]; !ok {
				wwlog.Warn("Profile not found for node '%s': %s", nodename, profileName)
				continue
			}
			// can't call setFrom() as we have to use SetAlt instead of Set for an Entry
			wwlog.Verbose("Merging profile into node: %s <- %s", nodename, profileName)
			n.SetAltFrom(config.NodeProfiles[profileName], profileName)
		}
		// set default/primary network is just one network exist
		if len(n.NetDevs) >= 1 && !n.PrimaryNetDev.Defined() {
			tmpNets := make([]string, 0, len(n.NetDevs))
			for key := range node.NetDevs {
				tmpNets = append(tmpNets, key)
			}
			sort.Strings(tmpNets)
			// if a value is present in profile or node, default is not visible
			wwlog.Debug("%s setting primary network device: %s", n.Id.Get(), tmpNets[0])
			n.PrimaryNetDev.SetDefault(tmpNets[0])
		}
		if dev, ok := n.NetDevs[n.PrimaryNetDev.Get()]; ok {
			dev.Primary.SetDefaultB(true)
		}
		ret = append(ret, n)
	}

	sort.Slice(ret, func(i, j int) bool {
		if ret[i].ClusterName.Get() < ret[j].ClusterName.Get() {
			return true
		} else if ret[i].ClusterName.Get() == ret[j].ClusterName.Get() {
			if ret[i].Id.Get() < ret[j].Id.Get() {
				return true
			}
		}
		return false
	})

	return ret, nil
}

/*
Return all profiles as NodeInfo
*/
func (config *NodeYaml) FindAllProfiles() ([]NodeInfo, error) {
	var ret []NodeInfo

	for name, profile := range config.NodeProfiles {
		var p NodeInfo
		p.NetDevs = make(map[string]*NetDevEntry)
		p.Tags = make(map[string]*Entry)
		p.Kernel = new(KernelEntry)
		p.Ipmi = new(IpmiEntry)
		p.Id.Set(name)
		for keyname, key := range profile.Keys {
			profile.Tags[keyname] = key
			delete(profile.Keys, keyname)
		}

		p.SetFrom(profile)
		p.Ipmi.Ipaddr.Set(profile.IpmiIpaddr)
		p.Ipmi.Netmask.Set(profile.IpmiNetmask)
		p.Ipmi.Port.Set(profile.IpmiPort)
		p.Ipmi.Gateway.Set(profile.IpmiGateway)
		p.Ipmi.UserName.Set(profile.IpmiUserName)
		p.Ipmi.Password.Set(profile.IpmiPassword)
		p.Ipmi.Interface.Set(profile.IpmiInterface)
		p.Ipmi.Write.Set(profile.IpmiWrite)
		p.Kernel.Args.Set(profile.KernelArgs)
		p.Kernel.Override.Set(profile.KernelOverride)
		p.Kernel.Override.Set(profile.KernelVersion)
		// delete deprecated stuff
		profile.IpmiIpaddr = ""
		profile.IpmiNetmask = ""
		profile.IpmiGateway = ""
		profile.IpmiUserName = ""
		profile.IpmiPassword = ""
		profile.IpmiInterface = ""
		profile.IpmiWrite = ""
		profile.KernelArgs = ""
		profile.KernelOverride = ""
		profile.KernelVersion = ""
		// Merge Keys into Tags for backwards compatibility
		if len(profile.Tags) == 0 {
			profile.Tags = make(map[string]string)
		}

		ret = append(ret, p)
	}
	sort.Slice(ret, func(i, j int) bool {
		if ret[i].ClusterName.Get() < ret[j].ClusterName.Get() {
			return true
		} else if ret[i].ClusterName.Get() == ret[j].ClusterName.Get() {
			if ret[i].Id.Get() < ret[j].Id.Get() {
				return true
			}
		}
		return false
	})

	return ret, nil
}

/*
Return the names of all available profiles
*/
func (config *NodeYaml) ListAllProfiles() []string {
	var ret []string
	for name := range config.NodeProfiles {
		ret = append(ret, name)
	}
	return ret
}

/*
return a map where the key is the profile id
*/
func (config *NodeYaml) MapAllProfiles() (retMap map[string]*NodeInfo, err error) {
	retMap = make(map[string]*NodeInfo)
	profileList, err := config.FindAllProfiles()
	if err != nil {
		return
	}
	for _, pr := range profileList {
		retMap[pr.Id.Get()] = &pr
	}
	return
}

/*
return a map where the key is the node id
*/
func (config *NodeYaml) MapAllNodes() (retMap map[string]*NodeInfo, err error) {
	retMap = make(map[string]*NodeInfo)
	nodeList, err := config.FindAllNodes()
	if err != nil {
		return
	}
	for _, nd := range nodeList {
		retMap[nd.Id.Get()] = &nd
	}
	return
}

/*
FindDiscoverableNode returns the first discoverable node and an
interface to associate with the discovered interface. If the node has
a primary interface, it is returned; otherwise, the first interface
without a hardware address is returned.

If no unconfigured nodes are found, an error is returned.
*/
func (config *NodeYaml) FindDiscoverableNode() (NodeInfo, string, error) {
	var ret NodeInfo

	nodes, _ := config.FindAllNodes()

	for _, node := range nodes {
		if !node.Discoverable.GetB() {
			continue
		}
		if _, ok := node.NetDevs[node.PrimaryNetDev.Get()]; ok {
			return node, node.PrimaryNetDev.Get(), nil
		}
		for netdev, dev := range node.NetDevs {
			if !dev.Hwaddr.Defined() {
				return node, netdev, nil
			}
		}
	}

	return ret, "", errors.New("no unconfigured nodes found")
}
