package firewall

import (
	"bufio"
	"fmt"
	"io/ioutil"
	"log"
	"os"
	"regexp"
	"strings"

	"github.com/bettercap/bettercap/v2/core"
	"github.com/bettercap/bettercap/v2/network"

	"github.com/evilsocket/islazy/str"
)

var (
	sysCtlParser = regexp.MustCompile(`([^:]+):\s*(.+)`)
	pfFilePath   = fmt.Sprintf("/tmp/bcap_pf_%d.conf", os.Getpid())
)

type PfFirewall struct {
	iface      *network.Endpoint
	filename   string
	forwarding bool
	enabled    bool
}

func Make(iface *network.Endpoint) FirewallManager {
	firewall := &PfFirewall{
		iface:      iface,
		filename:   pfFilePath,
		forwarding: false,
		enabled:    false,
	}

	firewall.forwarding = firewall.IsForwardingEnabled()

	return firewall
}

func (f PfFirewall) sysCtlRead(param string) (string, error) {
	if out, err := core.Exec("sysctl", []string{param}); err != nil {
		return "", err
	} else if m := sysCtlParser.FindStringSubmatch(out); len(m) == 3 && m[1] == param {
		return m[2], nil
	} else {
		return "", fmt.Errorf("Unexpected sysctl output: %s", out)
	}
}

func (f PfFirewall) sysCtlWrite(param string, value string) (string, error) {
	args := []string{"-w", fmt.Sprintf("%s=%s", param, value)}
	_, err := core.Exec("sysctl", args)
	if err != nil {
		return "", err
	}

	// make sure we actually wrote the value
	if out, err := f.sysCtlRead(param); err != nil {
		return "", err
	} else if out != value {
		return "", fmt.Errorf("Expected value for '%s' is %s, found %s", param, value, out)
	} else {
		return out, nil
	}
}

func (f PfFirewall) IsForwardingEnabled() bool {
	out, err := f.sysCtlRead("net.inet.ip.forwarding")
	if err != nil {
		log.Printf("ERROR: %s", err)
		return false
	}

	return strings.HasSuffix(out, ": 1")
}

func (f PfFirewall) enableParam(param string, enabled bool) error {
	var value string
	if enabled {
		value = "1"
	} else {
		value = "0"
	}

	if _, err := f.sysCtlWrite(param, value); err != nil {
		return err
	} else {
		return nil
	}
}

func (f PfFirewall) EnableForwarding(enabled bool) error {
	return f.enableParam("net.inet.ip.forwarding", enabled)
}

func (f PfFirewall) generateRule(r *Redirection) string {
	src_a := "any"
	dst_a := "any"

	if r.SrcAddress != "" {
		src_a = r.SrcAddress
	}

	if r.DstAddress != "" {
		dst_a = r.DstAddress
	}

	return fmt.Sprintf("rdr pass on %s proto %s from any to %s port %d -> %s port %d",
		r.Interface, r.Protocol, src_a, r.SrcPort, dst_a, r.DstPort)
}

func (f *PfFirewall) enable(enabled bool) {
	f.enabled = enabled
	if enabled {
		core.Exec("pfctl", []string{"-e"})
	} else {
		core.Exec("pfctl", []string{"-d"})
	}
}

func (f PfFirewall) EnableRedirection(r *Redirection, enabled bool) error {
	rule := f.generateRule(r)

	if enabled {
		fd, err := os.OpenFile(f.filename, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
		if err != nil {
			return err
		}
		defer fd.Close()

		if _, err = fd.WriteString(rule + "\n"); err != nil {
			return err
		}

		// enable pf
		f.enable(true)

		// load the rule
		if _, err := core.Exec("pfctl", []string{"-f", f.filename}); err != nil {
			return err
		}
	} else {
		fd, err := os.Open(f.filename)
		if err == nil {
			defer fd.Close()

			lines := ""
			scanner := bufio.NewScanner(fd)
			for scanner.Scan() {
				line := str.Trim(scanner.Text())
				if line != rule {
					lines += line + "\n"
				}
			}

			if str.Trim(lines) == "" {
				os.Remove(f.filename)
				f.enable(false)
			} else {
				ioutil.WriteFile(f.filename, []byte(lines), 0600)
			}
		}
	}

	return nil
}

func (f PfFirewall) Restore() {
	f.EnableForwarding(f.forwarding)
	if f.enabled {
		f.enable(false)
	}
	os.Remove(f.filename)
}
