package ip

import (
	"bytes"
	"fmt"
	"os/exec"
	"regexp"
	"strconv"
	"strings"
	"syscall"

	log "github.com/golang/glog"
)

type IPTables struct {
	path string
}

func NewIPTables() (*IPTables, error) {
	path, err := exec.LookPath("iptables")
	if err != nil {
		return nil, err
	}

	return &IPTables{path}, nil
}

func (ipt *IPTables) Exists(table string, args ...string) (bool, error) {
	checkPresent, err := getIptablesHasCheckCommand()
	if err != nil {
		log.Warningf("Error checking iptables version, assuming version at least 1.4.11: %v\n", err)
		checkPresent = true
	}

	if !checkPresent {
		cmd := append([]string{"-A"}, args...)
		return existsForOldIpTables(table, strings.Join(cmd, " "))
	} else {
		cmd := append([]string{"-t", table, "-C"}, args...)
		err = exec.Command(ipt.path, cmd...).Run()
	}
	switch {
	case err == nil:
		return true, nil
	case err.(*exec.ExitError).Sys().(syscall.WaitStatus).ExitStatus() == 1:
		return false, nil
	default:
		return false, err
	}
}

func (ipt *IPTables) Append(table string, args ...string) error {
	cmd := append([]string{"-t", table, "-A"}, args...)
	return exec.Command(ipt.path, cmd...).Run()
}

// AppendUnique acts like Append except that it won't add a duplicate
func (ipt *IPTables) AppendUnique(table string, args ...string) error {
	exists, err := ipt.Exists(table, args...)
	if err != nil {
		return err
	}

	if !exists {
		return ipt.Append(table, args...)
	}

	return nil
}

func (ipt *IPTables) ClearChain(table, chain string) error {
	cmd := []string{"-t", table, "-N", chain}
	err := exec.Command(ipt.path, cmd...).Run()

	switch {
	case err == nil:
		return nil
	case err.(*exec.ExitError).Sys().(syscall.WaitStatus).ExitStatus() == 1:
		// chain already exists. Flush (clear) it.
		cmd := []string{"-t", table, "-F", chain}
		return exec.Command(ipt.path, cmd...).Run()
	default:
		return err
	}
}

// Checks if iptables has the "-C" flag
func getIptablesHasCheckCommand() (bool, error) {
	vstring, err := getIptablesVersionString()
	if err != nil {
		return false, err
	}

	v1, v2, v3, err := extractIptablesVersion(vstring)
	if err != nil {
		return false, err
	}

	return iptablesHasCheckCommand(v1, v2, v3), nil
}

// getIptablesVersion returns the first three components of the iptables version.
// e.g. "iptables v1.3.66" would return (1, 3, 66, nil)
func extractIptablesVersion(str string) (int, int, int, error) {
	versionMatcher := regexp.MustCompile("v([0-9]+)\\.([0-9]+)\\.([0-9]+)")
	result := versionMatcher.FindStringSubmatch(str)
	if result == nil {
		return 0, 0, 0, fmt.Errorf("no iptables version found in string: %s", str)
	}

	v1, err := strconv.Atoi(result[1])
	if err != nil {
		return 0, 0, 0, err
	}

	v2, err := strconv.Atoi(result[2])
	if err != nil {
		return 0, 0, 0, err
	}

	v3, err := strconv.Atoi(result[3])
	if err != nil {
		return 0, 0, 0, err
	}

	return v1, v2, v3, nil
}

// Runs "iptables --version" to get the version string
func getIptablesVersionString() (string, error) {
	cmd := exec.Command("iptables", "--version")
	var out bytes.Buffer
	cmd.Stdout = &out
	err := cmd.Run()
	if err != nil {
		return "", err
	}
	return out.String(), nil
}

// Checks if an iptables version is after 1.4.11, when --check was added
func iptablesHasCheckCommand(v1 int, v2 int, v3 int) bool {
	if v1 > 1 {
		return true
	}
	if v1 == 1 && v2 > 4 {
		return true
	}
	if v1 == 1 && v2 == 4 && v3 >= 11 {
		return true
	}
	return false
}

// Checks if a rule specification exists for a table
func existsForOldIpTables(table string, ruleSpec string) (bool, error) {
	cmd := exec.Command("iptables", "-t", table, "-S")
	var out bytes.Buffer
	cmd.Stdout = &out
	err := cmd.Run()
	if err != nil {
		return false, err
	}
	rules := out.String()
	return strings.Contains(rules, ruleSpec), nil
}
