// Package config reads, parses, and represents the sunhpc.conf
// config file.
//
// sunhpc.conf is a yaml-formatted configuration file that includes
// configuration for the sunhpc daemon and commands, as well as the
// DHCP, TFTP and NFS services that sunhpc manages.
package config

import (
	"bytes"
	"fmt"
	"net"
	"os"
	"reflect"
	"strconv"

	"github.com/creasty/defaults"
	"gitea.sunhpc.com/kelvin/sunhpc/internal/pkg/splog"
	"gopkg.in/yaml.v3"
)

var cachedConf SunhpcYaml

// SunhpcYaml is the main Sunhpc configuration structure. It stores
// some information about the Sunhpc server locally, and has
// [SunhpcConf], [DHCPConf], [TFTPConf], and [NFSConf] sub-sections.
type SunhpcYaml struct {
	Comment     string        `yaml:"comment,omitempty"`
	Ipaddr      string        `yaml:"ipaddr,omitempty"`
	Netmask     string        `yaml:"netmask,omitempty"`
	Network     string        `yaml:"network,omitempty"`
	Fqdn        string        `yaml:"fqdn,omitempty"`
	Ipaddr6     string        `yaml:"ipaddr6,omitempty"`
	PrefixLen6  string        `yaml:"prefixlen6,omitempty"`
	Sunhpc      *SunhpcConf   `yaml:"sunhpc,omitempty"`
	API         *APIConf      `yaml:"api,omitempty"`
	DNSMASQ     *DNSMASQConf  `yaml:"dhcp,omitempty"`
	DHCP        *DHCPConf     `yaml:"dhcp,omitempty"`
	TFTP        *TFTPConf     `yaml:"tftp,omitempty"`
	NFS         *NFSConf      `yaml:"nfs,omitempty"`
	SSH         *SSHConf      `yaml:"ssh,omitempty"`
	MountsImage []*MountEntry `yaml:"image mounts,omitempty" default:"[{\"source\": \"/etc/resolv.conf\", \"dest\": \"/etc/resolv.conf\"}]"`
	Paths       *BuildConfig  `yaml:"paths,omitempty"`
	//Client      *SPClientConf `yaml:"client,omitempty"`

	sunhpcconf   string
	autodetected bool
}

// New caches and returns a new [SunhpcYaml] initialized with empty
// values, clearing replacing any previously cached value.
func New() *SunhpcYaml {
	cachedConf = SunhpcYaml{}
	cachedConf.sunhpcconf = ""

	cachedConf.Sunhpc = new(SunhpcConf) 
	cachedConf.DNSMASQ = new(DNSMASQConf)
	cachedConf.Paths = new(BuildConfig)
	cachedConf.DHCP = new(DHCPConf)
	cachedConf.TFTP = new(TFTPConf)
	cachedConf.NFS = new(NFSConf)
	cachedConf.SSH = new(SSHConf)
	cachedConf.API = new(APIConf)
	if err := defaults.Set(&cachedConf); err != nil {
		panic(err)
	}
	return &cachedConf
}

// Get returns a previously cached [SunhpcYaml] if it exists, or returns
// a new SunhpcYaml.
func Get() *SunhpcYaml {
	// NOTE: This function can be called before any log level is set
	//       so using splog.Verbose or splog.Debug won't work
	if reflect.ValueOf(cachedConf).IsZero() {
		cachedConf = *New()
	}
	return &cachedConf
}

// Read populates [SunhpcYaml] with the values from a configuration
// file.
func (conf *SunhpcYaml) Read(confFileName string, autodetect bool) error {
	splog.Debug("Reading sunhpc.conf from: %s", confFileName)
	conf.sunhpcconf = confFileName
	if data, err := os.ReadFile(confFileName); err != nil {
		return err
	} else if err := conf.Parse(data, autodetect); err != nil {
		return err
	} else {
		return nil
	}
}

// Parse populates [SunhpcYaml] with the values from a yaml document.
func (conf *SunhpcYaml) Parse(data []byte, autodetect bool) error {
	// ipxe binaries are merged not overwritten, store defaults separate
	defIpxe := make(map[string]string)
	for k, v := range conf.TFTP.IpxeBinaries {
		defIpxe[k] = v
		delete(conf.TFTP.IpxeBinaries, k)
	}
	if err := yaml.Unmarshal(data, &conf); err != nil {
		return err
	}
	if len(conf.TFTP.IpxeBinaries) == 0 {
		conf.TFTP.IpxeBinaries = defIpxe
	}

	if ip, network, err := net.ParseCIDR(conf.Ipaddr); err == nil {
		conf.Ipaddr = ip.String()
		if conf.Network == "" {
			conf.Network = network.IP.String()
		}
		if conf.Netmask == "" {
			conf.Netmask = net.IP(network.Mask).String()
		}
	}

	if autodetect {
		if conf.Ipaddr == "" {
			if ip := GetOutboundIP(); ip != nil {
				conf.Ipaddr = ip.String()
				conf.autodetected = true
			}
		}

		if conf.Netmask == "" {
			if ip := net.ParseIP(conf.Ipaddr); ip != nil {
				if network, err := GetIPNetForIP(ip); err == nil {
					conf.Netmask = net.IP(network.Mask).String()
					conf.autodetected = true
				}
			}
		}

		if conf.Network == "" {
			if ip := net.ParseIP(conf.Ipaddr); ip != nil {
				if mask := net.IPMask(net.ParseIP(conf.Netmask)); mask != nil {
					conf.Network = ip.Mask(mask).String()
					conf.autodetected = true
				}
			}
		}
	}

	if ip := net.ParseIP(conf.Ipaddr6); ip != nil {
		if prefix, err := strconv.Atoi(conf.PrefixLen6); err == nil {
			conf.Ipaddr6 = ip.String()
			conf.PrefixLen6 = strconv.Itoa(prefix)
		}
	}

	if ip, net, err := net.ParseCIDR(conf.Ipaddr6); err == nil {
		conf.Ipaddr6 = ip.String()
		prefixLen6, _ := net.Mask.Size()
		conf.PrefixLen6 = strconv.Itoa(prefixLen6)
	}

	return nil
}

func (config *SunhpcYaml) Network6() string {
	cidr := fmt.Sprintf("%s/%s", config.Ipaddr6, config.PrefixLen6)
	_, ipnet, err := net.ParseCIDR(cidr)
	if err != nil {
		return ""
	}
	return ipnet.IP.String()
}

func (config *SunhpcYaml) NetworkCIDR() string {
	if config.Network == "" || config.Netmask == "" {
		return ""
	}
	cidr := net.IPNet{
		IP:   net.ParseIP(config.Network),
		Mask: net.IPMask(net.ParseIP(config.Netmask)),
	}
	if cidr.IP == nil || cidr.Mask == nil {
		return ""
	}
	return cidr.String()
}

func (config *SunhpcYaml) NetworkCIDR6() string {
	cidr := fmt.Sprintf("%s/%s", config.Ipaddr6, config.PrefixLen6)
	_, ipnet, err := net.ParseCIDR(cidr)
	if err != nil {
		return ""
	}
	return ipnet.String()
}

func (config *SunhpcYaml) IpCIDR() string {
	if config.Ipaddr == "" || config.Netmask == "" {
		return ""
	}
	cidr := net.IPNet{
		IP:   net.ParseIP(config.Ipaddr),
		Mask: net.IPMask(net.ParseIP(config.Netmask)),
	}
	if cidr.IP == nil || cidr.Mask == nil {
		return ""
	}
	return cidr.String()
}

func (config *SunhpcYaml) IpCIDR6() string {
	cidr := fmt.Sprintf("%s/%s", config.Ipaddr6, config.PrefixLen6)
	ip, _, err := net.ParseCIDR(cidr)
	if err != nil || ip == nil || ip.To4() != nil {
		return ""
	}
	return cidr
}

// InitializedFromFile returns true if [SunhpcYaml] memory was read from
// a file, or false otherwise.
func (conf *SunhpcYaml) InitializedFromFile() bool {
	return conf.sunhpcconf != ""
}

func (conf *SunhpcYaml) GetSunhpcConf() string {
	return conf.sunhpcconf
}

func (conf *SunhpcYaml) Autodetected() bool {
	return conf.autodetected
}

func (config *SunhpcYaml) Dump() ([]byte, error) {
	var buf bytes.Buffer
	yamlEncoder := yaml.NewEncoder(&buf)
	yamlEncoder.SetIndent(2)
	err := yamlEncoder.Encode(config)
	return buf.Bytes(), err
}

func (config *SunhpcYaml) PersistToFile(configFile string) (err error) {
	out, dumpErr := config.Dump()
	if dumpErr != nil {
		splog.Error("%s", dumpErr)
		return dumpErr
	}
	file, err := os.OpenFile(configFile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o644)
	if err != nil {
		splog.Error("%s", err)
		return err
	}
	defer func() {
		if cerr := file.Close(); cerr != nil && err == nil {
			err = cerr
		}
	}()
	_, err = file.WriteString(string(out))
	if err != nil {
		return err
	}
	splog.Debug("persisted: %s", configFile)
	return nil
}