package overlay

import (
	"fmt"
	"io/ioutil"
	"net"
	"os"
	"os/exec"
	"path"
	"path/filepath"
	"strconv"
	"strings"
	"text/template"

	"github.com/hpcng/warewulf/internal/pkg/node"
	"github.com/hpcng/warewulf/internal/pkg/util"
	"github.com/hpcng/warewulf/internal/pkg/wwlog"
	"github.com/pkg/errors"
)

type TemplateStruct struct {
	Id            string
	Hostname      string
	ClusterName   string
	Container     string
	Init          string
	Root          string
	IpmiIpaddr    string
	IpmiNetmask   string
	IpmiPort      string
	IpmiGateway   string
	IpmiUserName  string
	IpmiPassword  string
	IpmiInterface string
	NetDevs       map[string]*node.NetDevs
	Keys          map[string]string
	AllNodes      []node.NodeInfo
}

/*

func BuildSystemOverlay(nodeList []node.NodeInfo) error {
	return nil
}

func BuildRuntimeOverlay(nodeList []node.NodeInfo) error {
	return nil
}


func FindSystemOverlays() ([]string, error) {
	return findAllOverlays("system")
}

func FindRuntimeOverlays() ([]string, error) {
	return findAllOverlays("runtime")
}
*/

func BuildAllOverlays(nodes []node.NodeInfo) error {
	for _, n := range nodes {
		var overlays []string

		overlays = append(overlays, n.SystemOverlay.Get())
		overlays = append(overlays, n.RuntimeOverlay.Get())

		wwlog.Printf(wwlog.INFO, "Building overlays for %s: [%s]\n", n.Id.Get(), strings.Join(overlays, ", "))

		for _, overlay := range overlays {
			err := BuildOverlay(n, overlay)
			if err != nil {
				return errors.Wrap(err, "could not build overlay "+n.Id.Get()+"/"+overlay+".img")
			}
		}
	}
	return nil
}

// TODO: Add an Overlay Delete for both sourcedir and image

func BuildSpecificOverlays(nodes []node.NodeInfo, overlayName string) error {
	for _, n := range nodes {

		wwlog.Printf(wwlog.INFO, "Building overlay for %s: %s\n", n.Id.Get(), overlayName)
		err := BuildOverlay(n, overlayName)
		if err != nil {
			return errors.Wrap(err, "could not build overlay "+n.Id.Get()+"/"+overlayName+".img")
		}

	}
	return nil
}

func FindOverlays() ([]string, error) {
	var ret []string
	var files []os.FileInfo

	files, err := ioutil.ReadDir(OverlaySourceTopDir())
	if err != nil {
		return ret, errors.Wrap(err, "could not get list of overlays")
	}

	for _, file := range files {
		wwlog.Printf(wwlog.DEBUG, "Evaluating overlay source: %s\n", file.Name())
		if file.IsDir() {
			ret = append(ret, file.Name())
		}
	}

	return ret, nil
}

func OverlayInit(overlayName string) error {
	path := OverlaySourceDir(overlayName)

	if util.IsDir(path) {
		return errors.New("Overlay already exists: " + overlayName)
	}

	err := os.MkdirAll(path, 0755)

	return err
}

func BuildOverlay(nodeInfo node.NodeInfo, overlayName string) error {
	nodeDB, _ := node.New()
	allNodes, _ := nodeDB.FindAllNodes()
	var tstruct TemplateStruct
	OverlaySourceDir := OverlaySourceDir(overlayName)
	OverlayImage := OverlayImage(nodeInfo.Id.Get(), overlayName)
	OverlayImageDir := path.Dir(OverlayImage)

	if !util.ValidString(overlayName, "^[a-zA-Z0-9-._:]+$") {
		return errors.New("overlay name contains illegal characters: " + overlayName)
	}

	wwlog.Printf(wwlog.DEBUG, "Checking to see if overlay directory exists: %s\n", OverlaySourceDir)
	if !util.IsDir(OverlaySourceDir) {
		return errors.New("overlay does not exist: " + overlayName)
	}

	err := os.MkdirAll(OverlayImageDir, 0755)
	if err == nil {
		wwlog.Printf(wwlog.DEBUG, "Created parent directory for Overlay Images: %s\n", OverlayImageDir)
	} else {
		return errors.Wrap(err, "could not create overlay image directory")
	}

	tmpDir, err := ioutil.TempDir(os.TempDir(), ".wwctl-overlay-")
	if err == nil {
		wwlog.Printf(wwlog.DEBUG, "Creating temporary directory for overlay files: %s\n", tmpDir)
	} else {
		return errors.Wrap(err, "could not create overlay temporary directory")
	}

	wwlog.Printf(wwlog.VERBOSE, "Processing node/overlay: %s/%s\n", nodeInfo.Id.Get(), overlayName)

	tstruct.Id = nodeInfo.Id.Get()
	tstruct.Hostname = nodeInfo.Id.Get()
	tstruct.Id = nodeInfo.Id.Get()
	tstruct.Hostname = nodeInfo.Id.Get()
	tstruct.ClusterName = nodeInfo.ClusterName.Get()
	tstruct.Container = nodeInfo.ContainerName.Get()
	tstruct.Init = nodeInfo.Init.Get()
	tstruct.Root = nodeInfo.Root.Get()
	tstruct.IpmiIpaddr = nodeInfo.IpmiIpaddr.Get()
	tstruct.IpmiNetmask = nodeInfo.IpmiNetmask.Get()
	tstruct.IpmiPort = nodeInfo.IpmiPort.Get()
	tstruct.IpmiGateway = nodeInfo.IpmiGateway.Get()
	tstruct.IpmiUserName = nodeInfo.IpmiUserName.Get()
	tstruct.IpmiPassword = nodeInfo.IpmiPassword.Get()
	tstruct.IpmiInterface = nodeInfo.IpmiInterface.Get()
	tstruct.NetDevs = make(map[string]*node.NetDevs)
	tstruct.Keys = make(map[string]string)
	for devname, netdev := range nodeInfo.NetDevs {
		var nd node.NetDevs
		tstruct.NetDevs[devname] = &nd
		tstruct.NetDevs[devname].Device = netdev.Device.Get()
		tstruct.NetDevs[devname].Hwaddr = netdev.Hwaddr.Get()
		tstruct.NetDevs[devname].Ipaddr = netdev.Ipaddr.Get()
		tstruct.NetDevs[devname].Netmask = netdev.Netmask.Get()
		tstruct.NetDevs[devname].Gateway = netdev.Gateway.Get()
		tstruct.NetDevs[devname].Type = netdev.Type.Get()
		tstruct.NetDevs[devname].OnBoot = netdev.OnBoot.GetB()
		tstruct.NetDevs[devname].Default = netdev.Default.GetB()

		mask := net.IPMask(net.ParseIP(netdev.Netmask.Get()).To4())
		ipaddr := net.ParseIP(netdev.Ipaddr.Get()).To4()
		netaddr := net.IPNet{IP: ipaddr, Mask: mask}
		netPrefix, _ := net.IPMask(net.ParseIP(netdev.Netmask.Get()).To4()).Size()
		tstruct.NetDevs[devname].Prefix = strconv.Itoa(netPrefix)
		tstruct.NetDevs[devname].IpCIDR = netaddr.String()

	}
	for keyname, key := range nodeInfo.Keys {
		tstruct.Keys[keyname] = key.Get()
	}
	tstruct.AllNodes = allNodes

	wwlog.Printf(wwlog.DEBUG, "Changing directory to OverlayDir: %s\n", OverlaySourceDir)
	err = os.Chdir(OverlaySourceDir)
	if err != nil {
		return errors.Wrap(err, "could not change directory to overlay dir")
	}

	wwlog.Printf(wwlog.VERBOSE, "Walking the overlay structure: %s\n", OverlaySourceDir)
	err = filepath.Walk(".", func(location string, info os.FileInfo, err error) error {
		if err != nil {
			return err
		}

		wwlog.Printf(wwlog.DEBUG, "Found overlay file: %s\n", location)

		if info.IsDir() {
			wwlog.Printf(wwlog.DEBUG, "Found directory: %s\n", location)

			err = os.MkdirAll(path.Join(tmpDir, location), info.Mode())
			if err != nil {
				return errors.Wrap(err, "could not create directory within overlay")
			}
			err = util.CopyUIDGID(location, path.Join(tmpDir, location))
			if err != nil {
				return errors.Wrap(err, "failed setting permissions on overlay directory")
			}

			wwlog.Printf(wwlog.DEBUG, "Created directory in overlay: %s\n", location)

		} else if filepath.Ext(location) == ".ww" {
			wwlog.Printf(wwlog.VERBOSE, "Evaluating overlay template file: %s\n", location)

			destFile := strings.TrimSuffix(location, ".ww")

			tmpl, err := template.New(path.Base(location)).Option("missingkey=default").Funcs(template.FuncMap{
				// TODO: Fix for missingkey=zero
				"Include":     templateFileInclude,
				"IncludeFrom": templateContainerFileInclude,
				"inc":         func(i int) int { return i + 1 },
				"dec":         func(i int) int { return i - 1 },
				// }).ParseGlob(path.Join(OverlayDir, destFile+".ww*"))
			}).ParseGlob(location)
			if err != nil {
				return errors.Wrap(err, "could not parse template "+location)
			}

			w, err := os.OpenFile(path.Join(tmpDir, destFile), os.O_RDWR|os.O_CREATE, info.Mode())
			if err != nil {
				return errors.Wrap(err, "could not open new file for template")
			}
			defer w.Close()

			err = tmpl.Execute(w, tstruct)
			if err != nil {
				return errors.Wrap(err, "could not execute template")
			}

			err = util.CopyUIDGID(location, path.Join(tmpDir, destFile))
			if err != nil {
				return errors.Wrap(err, "failed setting permissions on template output file")
			}

			wwlog.Printf(wwlog.DEBUG, "Wrote template file into overlay: %s\n", destFile)

			//		} else if b, _ := regexp.MatchString(`\.ww[a-zA-Z0-9\-\._]*$`, location); b {
			//			wwlog.Printf(wwlog.DEBUG, "Ignoring WW template file: %s\n", location)
		} else {

			err := util.CopyFile(location, path.Join(tmpDir, location))
			if err == nil {
				wwlog.Printf(wwlog.DEBUG, "Copied file into overlay: %s\n", location)
			} else {
				return errors.Wrap(err, "could not copy file into overlay")
			}
		}

		return nil
	})
	if err != nil {
		return errors.Wrap(err, "failed to build overlay working directory")
	}

	wwlog.Printf(wwlog.DEBUG, "Finished generating overlay working directory for: %s/%s\n", nodeInfo.Id.Get(), overlayName)

	compressor, err := exec.LookPath("pigz")
	if err != nil {
		wwlog.Printf(wwlog.DEBUG, "Could not locate PIGZ, using GZIP\n")
		compressor = "gzip"
	} else {
		wwlog.Printf(wwlog.DEBUG, "Using PIGZ to compress the overlay: %s\n", compressor)
	}

	cmd := fmt.Sprintf("cd \"%s\"; find . | cpio --quiet -o -H newc | %s -c > \"%s\"", tmpDir, compressor, OverlayImage)

	wwlog.Printf(wwlog.DEBUG, "RUNNING: %s\n", cmd)
	err = exec.Command("/bin/sh", "-c", cmd).Run()
	if err != nil {
		return errors.Wrap(err, "could not generate compressed runtime image overlay")
	}
	wwlog.Printf(wwlog.VERBOSE, "Completed building overlay image: %s\n", OverlayImage)

	wwlog.Printf(wwlog.DEBUG, "Removing temporary directory: %s\n", tmpDir)
	os.RemoveAll(tmpDir)

	return nil
}
