package main

import (
	"bufio"
	"bytes"
	"fmt"
	"io/ioutil"
	"net"
	"os"
	"strings"
	"text/template"

	"github.com/containernetworking/plugins/plugins/ipam/host-local/backend/disk"
	"github.com/coreos/go-iptables/iptables"
	"github.com/sirupsen/logrus"
)

// dnsNameLock embeds the CNI disk lock so we can hang methods from it
type dnsNameLock struct {
	lock *disk.FileLock
}

// release unlocks and closes the disk lock.
func (m *dnsNameLock) release() error {
	if err := m.lock.Unlock(); err != nil {
		return err
	}
	return m.lock.Close()
}

// acquire locks the disk lock.
func (m *dnsNameLock) acquire() error {
	return m.lock.Lock()
}

// getLock returns a dnsNameLock synchronizing the configuration directory for
// the domain.
func getLock(path string) (*dnsNameLock, error) {
	l, err := disk.NewFileLock(path)
	if err != nil {
		return nil, err
	}
	return &dnsNameLock{l}, nil
}

// checkFromDNSMasqConfFile ensures that the dnsmasq conf file for
// the network interface exists or it creates it
func checkForDNSMasqConfFile(conf dnsNameFile) error {
	if _, err := os.Stat(conf.ConfigFile); err == nil {
		// the file already exists, we can proceed
		return err
	}
	newConfig, err := generateDNSMasqConfig(conf)
	if err != nil {
		return err
	}
	ip, err := iptables.New()
	if err != nil {
		return err
	}
	args := []string{"-i", conf.NetworkInterface, "-p", "udp", "-m", "udp", "--dport", "53", "-j", "ACCEPT"}
	exists, err := ip.Exists("filter", "INPUT", args...)
	if err != nil {
		return err
	}
	if !exists {
		if err := ip.Insert("filter", "INPUT", 1, args...); err != nil {
			return err
		}
	}
	// Generate the template and compile it.
	return ioutil.WriteFile(conf.ConfigFile, newConfig, 0700)
}

// generateDNSMasqConfig fills out the configuration file template for the dnsmasq service
func generateDNSMasqConfig(config dnsNameFile) ([]byte, error) {
	var buf bytes.Buffer
	templ, err := template.New("dnsmasq-conf-file").Parse(dnsMasqTemplate)
	if err != nil {
		return nil, err
	}
	if err := templ.Execute(&buf, config); err != nil {
		return nil, err
	}
	buf.WriteByte('\n')
	return buf.Bytes(), nil
}

// appendToFile appends a new entry to the dnsmasqs hosts file
func appendToFile(path, podname string, aliases []string, ips []*net.IPNet) error {
	f, err := openFile(path)
	if err != nil {
		return err
	}
	defer func() {
		if err := f.Close(); err != nil {
			logrus.Errorf("failed to close file %q: %v", path, err)
		}
	}()
	for _, ip := range ips {
		entry := fmt.Sprintf("%s\t%s", ip.IP.String(), podname)
		for _, alias := range aliases {
			entry += fmt.Sprintf("\t%s", alias)
		}
		entry += "\n"
		if _, err = f.WriteString(entry); err != nil {
			return err
		}
		logrus.Debugf("appended %s: %s", path, entry)
	}
	return nil
}

// removeLineFromFile removes a given entry from the dnsmasq host file
func removeFromFile(path, podname string) (bool, error) {
	var (
		keepers []string
		found   bool
	)
	shouldHUP := false
	backup := fmt.Sprintf("%s.old", path)
	if err := os.Rename(path, backup); err != nil {
		return shouldHUP, err
	}
	f, err := os.Open(backup)
	if err != nil {
		//	if the open fails here, we need to revert things
		renameFile(backup, path)
		return shouldHUP, err
	}
	defer func() {
		if err := f.Close(); err != nil {
			logrus.Errorf("unable to close %q: %v", backup, err)
		}
	}()

	oldFile := bufio.NewScanner(f)
	// Iterate the old file
	for oldFile.Scan() {
		fields := strings.Fields(oldFile.Text())
		// if the IP of the entry and the given IP dont match, it should
		// go into the new file
		if len(fields) > 1 && fields[1] != podname {
			keepers = append(keepers, fmt.Sprintf("%s\n", oldFile.Text()))
			continue
		}
		found = true
	}
	if !found {
		// We never found a matching record; non-fatal
		logrus.Debugf("a record for %s was never found in %s", podname, path)
	}
	fileLength, err := writeFile(path, keepers)
	if err != nil {
		renameFile(backup, path)
		return shouldHUP, err
	}
	if fileLength > 0 {
		shouldHUP = true
	}
	if err := os.Remove(backup); err != nil {
		logrus.Errorf("unable to delete '%s': %q", backup, err)
	}
	return shouldHUP, nil
}

// renameFile renames a file to backup
func renameFile(oldpath, newpath string) {
	if renameError := os.Rename(oldpath, newpath); renameError != nil {
		logrus.Errorf("unable to restore %q to %q: %v", oldpath, newpath, renameError)
	}
}

// writeFile writes a []string to the given path and returns the number
// of lines in the file
func writeFile(path string, content []string) (int, error) {
	var counter int
	f, err := openFile(path)
	if err != nil {
		return 0, err
	}
	defer func() {
		if err := f.Close(); err != nil {
			logrus.Errorf("unable to close %q: %v", path, err)
		}
	}()

	for _, line := range content {
		if _, err := f.WriteString(line); err != nil {
			return 0, err
		}
		counter++
	}
	return counter, nil
}

// openFile opens a file for reading
func openFile(path string) (*os.File, error) {
	return os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
}
