package overlay

import (
	"bufio"
	"bytes"
	"fmt"
	"io/fs"
	"io/ioutil"
	"net"
	"os"
	"os/exec"
	"path"
	"path/filepath"
	"regexp"
	"strconv"
	"strings"
	"text/template"
	"time"

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

/*

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")
}
*/

/*
Build all overlays (runtime and generic) for a node
*/
func BuildAllOverlays(nodes []node.NodeInfo) error {
	for _, n := range nodes {

		sysOverlays := n.SystemOverlay.GetSlice()
		wwlog.Printf(wwlog.INFO, "Building system overlays for %s: [%s]\n", n.Id.Get(), strings.Join(sysOverlays, ", "))
		err := BuildOverlay(n, sysOverlays)
		if err != nil {
			return errors.Wrap(err, fmt.Sprintf("could not build system overlays %v for nide %s\n", sysOverlays, n.Id.Get()))
		}
		runOverlays := n.RuntimeOverlay.GetSlice()
		wwlog.Printf(wwlog.INFO, "Building runtime overlays for %s: [%s]\n", n.Id.Get(), strings.Join(runOverlays, ", "))
		err = BuildOverlay(n, runOverlays)
		if err != nil {
			return errors.Wrap(err, fmt.Sprintf("could not build runtime overlays %v for nide %s\n", runOverlays, n.Id.Get()))
		}

	}
	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, []string{overlayName})
		if err != nil {
			return errors.Wrap(err, "could not build overlay "+n.Id.Get()+"/"+overlayName+".img")
		}

	}
	return nil
}

/*
Build overlay for the host, so no argument needs to be given
*/
func BuildHostOverlay() error {
	var host node.NodeInfo
	host.Kernel = new(node.KernelEntry)
	host.Ipmi = new(node.IpmiEntry)
	var idEntry node.Entry
	hostname, _ := os.Hostname()
	wwlog.Printf(wwlog.INFO, "Building overlay for %s: host\n", hostname)
	idEntry.Set(hostname)
	host.Id = idEntry
	hostdir := OverlaySourceDir("host")
	stats, err := os.Stat(hostdir)
	if err != nil {
		return errors.Wrap(err, "could not build host overlay ")
	}
	if !(stats.Mode() == os.FileMode(0750|os.ModeDir) || stats.Mode() == os.FileMode(0700|os.ModeDir)) {
		wwlog.Printf(wwlog.SECWARN, "Permissions of host overlay dir %s are %s (750 is considered as secure)\n", hostdir, stats.Mode())
	}
	return BuildOverlayIndir(host, []string{"host"}, "/")
}

/*
Get all overlays present in warewulf
*/
func FindOverlays() ([]string, error) {
	var ret []string
	var files []os.FileInfo
	dotfilecheck, _ := regexp.Compile(`^\..*`)

	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())
		isdotfile := dotfilecheck.MatchString(file.Name())

		if (file.IsDir()) && !(isdotfile) {
			ret = append(ret, file.Name())
		}
	}

	return ret, nil
}

/*
Creates an empty overlay
*/
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
}

/*
Build the given overlays for a node and create a Image for them
*/
func BuildOverlay(nodeInfo node.NodeInfo, overlayNames []string) error {
	// create the dir where the overlay images will reside
	overlayImage := OverlayImage(nodeInfo.Id.Get(), overlayNames)
	overlayImageDir := path.Dir(overlayImage)
	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")
	}

	outputDir, err := ioutil.TempDir(os.TempDir(), ".wwctl-overlay-")
	if err == nil {
		wwlog.Printf(wwlog.DEBUG, "Creating temporary directory for overlay files: %s\n", outputDir)
	} else {
		return errors.Wrap(err, "could not create overlay temporary directory")
	}
	err = BuildOverlayIndir(nodeInfo, overlayNames, outputDir)
	if err != nil {
		wwlog.Printf(wwlog.WARN, "Got following error when building overlay: %s\n", err)
	}

	wwlog.Printf(wwlog.DEBUG, "Finished generating overlay working directory for: %s/%v\n", nodeInfo.Id.Get(), overlayNames)
	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\"", outputDir, 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", outputDir)
	os.RemoveAll(outputDir)
	return nil
}

/*
Build the given overlays for a node in the given directory. If the given does not
exists it will be created.
*/
func BuildOverlayIndir(nodeInfo node.NodeInfo, overlayNames []string, outputDir string) error {
	if len(overlayNames) == 0 {
		return errors.New("At least one valid overlay is needed to build for a node\n")
	}
	if !util.IsDir(outputDir) {
		return errors.New(fmt.Sprintf("output %s must a be a directory\n", outputDir))
	}
	controller, err := warewulfconf.New()
	if err != nil {
		wwlog.Printf(wwlog.ERROR, "%s\n", err)
		os.Exit(1)
	}
	nodeDB, err := node.New()
	if err != nil {
		wwlog.Printf(wwlog.ERROR, "%s\n", err)
		os.Exit(1)
	}
	allNodes, err := nodeDB.FindAllNodes()
	if err != nil {
		wwlog.Printf(wwlog.ERROR, "%s\n", err)
		os.Exit(1)
	}

	if !util.ValidString(strings.Join(overlayNames, ""), "^[a-zA-Z0-9-._:]+$") {
		return errors.New(fmt.Sprintf("overlay names contains illegal characters: %v", overlayNames))
	}
	wwlog.Printf(wwlog.VERBOSE, "Processing node/overlay: %s/%s\n", nodeInfo.Id.Get(), strings.Join(overlayNames, "-"))
	var tstruct TemplateStruct
	tstruct.Kernel = new(node.KernelConf)
	tstruct.Ipmi = new(node.IpmiConf)
	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.Kernel.Version = nodeInfo.Kernel.Override.Get()
	tstruct.Kernel.Override = nodeInfo.Kernel.Override.Get()
	tstruct.Kernel.Args = nodeInfo.Kernel.Args.Get()
	tstruct.Init = nodeInfo.Init.Get()
	tstruct.Root = nodeInfo.Root.Get()
	tstruct.Ipmi.Ipaddr = nodeInfo.Ipmi.Ipaddr.Get()
	tstruct.Ipmi.Netmask = nodeInfo.Ipmi.Netmask.Get()
	tstruct.Ipmi.Port = nodeInfo.Ipmi.Port.Get()
	tstruct.Ipmi.Gateway = nodeInfo.Ipmi.Gateway.Get()
	tstruct.Ipmi.UserName = nodeInfo.Ipmi.UserName.Get()
	tstruct.Ipmi.Password = nodeInfo.Ipmi.Password.Get()
	tstruct.Ipmi.Interface = nodeInfo.Ipmi.Interface.Get()
	tstruct.Ipmi.Write = nodeInfo.Ipmi.Write.GetB()
	tstruct.RuntimeOverlay = nodeInfo.RuntimeOverlay.Print()
	tstruct.SystemOverlay = nodeInfo.SystemOverlay.Print()
	tstruct.NetDevs = make(map[string]*node.NetDevs)
	tstruct.Keys = make(map[string]string)
	tstruct.Tags = 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.Get()
		tstruct.NetDevs[devname].Default = netdev.Default.Get()
		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()
		tstruct.NetDevs[devname].Ipaddr6 = netdev.Ipaddr6.Get()
		tstruct.NetDevs[devname].Tags = make(map[string]string)
		for key, value := range netdev.Tags {
			tstruct.NetDevs[devname].Tags[key] = value.Get()
		}
	}
	// Backwards compatibility for templates using "Keys"
	for keyname, key := range nodeInfo.Tags {
		tstruct.Keys[keyname] = key.Get()
	}
	for keyname, key := range nodeInfo.Tags {
		tstruct.Tags[keyname] = key.Get()
	}
	tstruct.AllNodes = allNodes
	tstruct.Nfs = *controller.Nfs
	tstruct.Dhcp = *controller.Dhcp
	tstruct.Warewulf = *controller.Warewulf
	tstruct.Ipaddr = controller.Ipaddr
	tstruct.Ipaddr6 = controller.Ipaddr6
	tstruct.Netmask = controller.Netmask
	tstruct.Network = controller.Network
	if controller.Ipaddr6 != "" {
		tstruct.Ipv6 = true
	} else {
		tstruct.Ipv6 = false
	}
	hostname, _ := os.Hostname()
	tstruct.BuildHost = hostname
	dt := time.Now()
	tstruct.BuildTime = dt.Format("01-02-2006 15:04:05 MST")
	for _, overlayName := range overlayNames {
		wwlog.Printf(wwlog.VERBOSE, "Building overlay %s for node %s in %s\n", overlayName, nodeInfo.Id.Get(), outputDir)
		overlaySourceDir := OverlaySourceDir(overlayName)
		wwlog.Printf(wwlog.DEBUG, "Starting to build overlay %s\nChanging directory to OverlayDir: %s\n", overlayName, overlaySourceDir)
		err = os.Chdir(overlaySourceDir)
		if err != nil {
			return errors.Wrap(err, "could not change directory to overlay dir")
		}
		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)
		}

		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 errors.Wrap(err, "error for "+location)
			}

			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(outputDir, location), info.Mode())
				if err != nil {
					return errors.Wrap(err, "could not create directory within overlay")
				}
				err = util.CopyUIDGID(location, path.Join(outputDir, 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" {
				tstruct.BuildSource = path.Join(overlaySourceDir, location)
				wwlog.Printf(wwlog.VERBOSE, "Evaluating overlay template file: %s\n", location)
				destFile := strings.TrimSuffix(location, ".ww")
				backupFile := true
				writeFile := true
				tmpl, err := template.New(path.Base(location)).Option("missingkey=default").Funcs(template.FuncMap{
					// TODO: Fix for missingkey=zero
					"Include":      templateFileInclude,
					"IncludeFrom":  templateContainerFileInclude,
					"IncludeBlock": templateFileBlock,
					"inc":          func(i int) int { return i + 1 },
					"dec":          func(i int) int { return i - 1 },
					"file":         func(str string) string { return fmt.Sprintf("{{ /* file \"%s\" */ }}", str) },
					"abort": func() string {
						wwlog.Printf(wwlog.DEBUG, "abort file called in %s\n", location)
						writeFile = false
						return ""
					},
					"nobackup": func() string {
						wwlog.Printf(wwlog.DEBUG, "not backup for %s\n", location)
						backupFile = false
						return ""
					},
					// }).ParseGlob(path.Join(OverlayDir, destFile+".ww*"))
				}).ParseGlob(location)
				if err != nil {
					return errors.Wrap(err, "could not parse template "+location)
				}
				var buffer bytes.Buffer
				err = tmpl.Execute(&buffer, tstruct)
				if err != nil {
					return errors.Wrap(err, "could not execute template")

				}
				if writeFile {
					destFileName := destFile
					var fileBuffer bytes.Buffer
					// search for magic file name comment
					fileScanner := bufio.NewScanner(bytes.NewReader(buffer.Bytes()))
					fileScanner.Split(scanLines)
					reg := regexp.MustCompile(`.*{{\s*/\*\s*file\s*["'](.*)["']\s*\*/\s*}}.*`)
					foundFileComment := false
					for fileScanner.Scan() {
						line := fileScanner.Text()
						filenameFromTemplate := reg.FindAllStringSubmatch(line, -1)
						if len(filenameFromTemplate) != 0 {
							wwlog.Printf(wwlog.DEBUG, "Found multifile comment, new filename %s\n", filenameFromTemplate[0][1])
							if foundFileComment {
								err = carefulWriteBuffer(path.Join(outputDir, destFileName),
									fileBuffer, backupFile, info.Mode())
								if err != nil {
									return errors.Wrap(err, "could not write file from template")
								}
								err = util.CopyUIDGID(location, path.Join(outputDir, destFileName))
								if err != nil {
									return errors.Wrap(err, "failed setting permissions on template output file")
								}
								fileBuffer.Reset()
							}
							destFileName = path.Join(path.Dir(destFile), filenameFromTemplate[0][1])
							foundFileComment = true
						} else {
							_, _ = fileBuffer.WriteString(line)
						}
					}
					err = carefulWriteBuffer(path.Join(outputDir, destFileName), fileBuffer, backupFile, info.Mode())
					if err != nil {
						return errors.Wrap(err, "could not write file from template")
					}
					err = util.CopyUIDGID(location, path.Join(outputDir, destFileName))
					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 if info.Mode()&os.ModeSymlink == os.ModeSymlink {
				wwlog.Printf(wwlog.DEBUG, "Found symlink %s\n", location)
				destination, err := os.Readlink(location)
				if err != nil {
					wwlog.Printf(wwlog.ERROR, "%s\n", err)
				}
				err = os.Symlink(destination, path.Join(outputDir, location))
				if err != nil {
					wwlog.Printf(wwlog.ERROR, "%s\n", err)
				}
			} else {

				err := util.CopyFile(location, path.Join(outputDir, 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")
		}
	}

	return nil
}

/*
Writes buffer to the destination file. If wwbackup is set a wwbackup will be created.
*/
func carefulWriteBuffer(destFile string, buffer bytes.Buffer, backupFile bool, perm fs.FileMode) error {
	wwlog.Printf(wwlog.DEBUG, "Trying to careful write file %s\n", destFile)
	if backupFile {
		// if !util.IsFile(path.Join(outputDir, destFile+".wwbackup")) && util.IsFile(path.Join(outputDir, destFile)) {
		if !util.IsFile(destFile+".wwbackup") && util.IsFile(destFile) {
			err := util.CopyFile(destFile, destFile+".wwbackup")
			if err != nil {
				wwlog.Printf(wwlog.ERROR, "%s\n", err)
			}
		}

	}
	w, err := os.OpenFile(destFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm)
	if err != nil {
		return errors.Wrap(err, "could not open new file for template")
	}
	defer w.Close()
	_, err = buffer.WriteTo(w)
	return err
}

// Simple version of ScanLines, but include the line break
func scanLines(data []byte, atEOF bool) (advance int, token []byte, err error) {
	if atEOF && len(data) == 0 {
		return 0, nil, nil
	}
	if i := bytes.IndexByte(data, '\n'); i >= 0 {
		// We have a full newline-terminated line.
		return i + 1, data[0 : i+1], nil
	}
	// If we're at EOF, we have a final, non-terminated line. Return it.
	if atEOF {
		return len(data), data, nil
	}
	// Request more data.
	return 0, nil, nil
}
