package core

import (
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"regexp"
	"strings"

	"github.com/evilsocket/opensnitch/daemon/log"
)

var (
	// IPv6Enabled indicates if IPv6 protocol is enabled in the system
	IPv6Enabled = Exists("/proc/sys/net/ipv6")
)

// GetHostname returns the name of the host where the daemon is running.
func GetHostname() string {
	hostname, _ := ioutil.ReadFile("/proc/sys/kernel/hostname")
	return strings.Replace(string(hostname), "\n", "", -1)
}

// GetKernelVersion returns the kernel version.
func GetKernelVersion() string {
	version, _ := ioutil.ReadFile("/proc/sys/kernel/osrelease")
	return strings.Replace(string(version), "\n", "", -1)
}

// GetMounts returns the mounts of the system
func GetMounts() []string {
	buf, _ := ioutil.ReadFile("/proc/mounts")
	return strings.Split(string(buf), "\n")
}

// HasTraceFS returns if tracefs is mounted
func IsTraceFSMounted() bool {
	for _, line := range GetMounts() {
		if strings.Contains(line, "tracefs") {
			return true
		}
	}

	return false
}

// CheckSysRequirements checks system features we need to work properly
func CheckSysRequirements() {
	type checksT struct {
		RegExps []string
		Reason  string
	}
	type ReqsList struct {
		Item   string
		Checks checksT
	}
	kVer := GetKernelVersion()

	log.Raw("\n\t%sChecking system requirements for kernel version %s%s\n", log.FG_WHITE+log.BG_LBLUE, kVer, log.RESET)
	log.Raw("%s------------------------------------------------------------------------------%s\n\n", log.FG_WHITE+log.BG_LBLUE, log.RESET)

	confPaths := []string{
		fmt.Sprint("/boot/config-", kVer),
		"/proc/config.gz",
		// Fedora SilverBlue
		fmt.Sprint("/usr/lib/modules/", kVer, "/config"),
	}

	var fileContent []byte
	var err error
	for _, confFile := range confPaths {
		if !Exists(confFile) {
			err = fmt.Errorf("%s not found", confFile)
			log.Debug(err.Error())
			continue
		}

		if confFile[len(confFile)-2:] == "gz" {
			fileContent, err = ReadGzipFile(confFile)
		} else {
			fileContent, err = ioutil.ReadFile(confFile)
		}
		if err == nil {
			break
		}
	}
	if err != nil {
		fmt.Printf("\n\t%s kernel config not found (%s) in any of the expected paths.\n", log.Bold(log.Red("✘")), kVer)
		fmt.Printf("\tPlease, open a new issue on github specifying your kernel and distro version (/etc/os-release).\n\n")
		return
	}

	// TODO: check loaded/configured modules (nfnetlink, nfnetlink_queue, xt_NFQUEUE, etc)
	// Other items to check:
	// CONFIG_NETFILTER_NETLINK
	// CONFIG_NETFILTER_NETLINK_QUEUE
	const reqsList = `
[
{
    "Item": "kprobes",
    "Checks": {
        "Regexps": [
            "CONFIG_KPROBES=y",
            "CONFIG_KPROBES_ON_FTRACE=y",
            "CONFIG_HAVE_KPROBES=y",
            "CONFIG_HAVE_KPROBES_ON_FTRACE=y",
            "CONFIG_KPROBE_EVENTS=y"
            ],
        "Reason": " - KPROBES not fully supported by this kernel."
    }
},
{
    "Item": "uprobes",
    "Checks": {
        "Regexps": [
            "CONFIG_UPROBES=y",
            "CONFIG_UPROBE_EVENTS=y"
            ],
        "Reason": " * UPROBES not supported. Common error => cannot open uprobe_events: open /sys/kernel/debug/tracing/uprobe_events"
    }
},
{
    "Item": "ftrace",
    "Checks": {
        "Regexps": [
            "CONFIG_FTRACE=y"
            ],
        "Reason": " - CONFIG_FTRACE=y not set. Common error => Error while loading kprobes: invalid argument."
    }
},
{
    "Item": "syscalls",
    "Checks": {
        "Regexps": [
            "CONFIG_HAVE_SYSCALL_TRACEPOINTS=y",
            "CONFIG_FTRACE_SYSCALLS=y",
			"CONFIG_TRACING=[my]",
			"CONFIG_EVENT_TRACING=[my]"
            ],
        "Reason": " - CONFIG_FTRACE_SYSCALLS, CONFIG_HAVE_SYSCALL_TRACEPOINTS, CONFIG_TRACE or CONFIG_EVENT_TRACING not set. Common error => error enabling tracepoint tracepoint/syscalls/sys_enter_execve: cannot read tracepoint id"
    }
},
{
    "Item": "nfqueue",
    "Checks": {
        "Regexps": [
			"CONFIG_NETFILTER_NETLINK_QUEUE=[my]",
			"CONFIG_NFT_QUEUE=[my]",
            "CONFIG_NETFILTER_XT_TARGET_NFQUEUE=[my]"
            ],
        "Reason": " * NFQUEUE netfilter extensions not supported by this kernel (CONFIG_NETFILTER_NETLINK_QUEUE, CONFIG_NFT_QUEUE, CONFIG_NETFILTER_XT_TARGET_NFQUEUE)."
    }
},
{
    "Item": "netlink",
    "Checks": {
        "Regexps": [
			"CONFIG_NETFILTER_NETLINK=[my]",
			"CONFIG_NETFILTER_NETLINK_QUEUE=[my]",
			"CONFIG_NETFILTER_NETLINK_ACCT=[my]",
			"CONFIG_PROC_EVENTS=[my]"
            ],
        "Reason": " * NETLINK extensions not supported by this kernel (CONFIG_NETFILTER_NETLINK, CONFIG_NETFILTER_NETLINK_QUEUE, CONFIG_NETFILTER_NETLINK_ACCT or CONFIG_PROC_EVENTS)."
    }
},
{
    "Item": "net diagnostics",
    "Checks": {
        "Regexps": [
			"CONFIG_INET_DIAG=[my]",
			"CONFIG_INET_TCP_DIAG=[my]",
			"CONFIG_INET_UDP_DIAG=[my]",
			"CONFIG_INET_DIAG_DESTROY=[my]"
            ],
        "Reason": " * One or more socket monitoring interfaces are not enabled (CONFIG_INET_DIAG, CONFIG_INET_TCP_DIAG, CONFIG_INET_UDP_DIAG, CONFIG_DIAG_DESTROY (Reject feature))."
    }
}
]
`

	reqsFullfiled := true
	dec := json.NewDecoder(strings.NewReader(reqsList))
	for {
		var reqs []ReqsList
		if err := dec.Decode(&reqs); err == io.EOF {
			break
		} else if err != nil {
			log.Error("%s", err)
			break
		}
		for _, req := range reqs {
			checkOk := true
			for _, trex := range req.Checks.RegExps {
				fmt.Printf("\tChecking => %s\n", trex)
				re, err := regexp.Compile(trex)
				if err != nil {
					fmt.Printf("\t%s %s\n", log.Bold(log.Red("Invalid regexp =>")), log.Red(trex))
					continue
				}
				if re.Find(fileContent) == nil {
					fmt.Printf("\t%s\n", log.Red(req.Checks.Reason))
					checkOk = false
				}
			}
			if checkOk {
				fmt.Printf("\n\t* %s\t %s\n", log.Bold(log.Green(req.Item)), log.Bold(log.Green("✔")))
			} else {
				reqsFullfiled = false
				fmt.Printf("\n\t* %s\t %s\n", log.Bold(log.Red(req.Item)), log.Bold(log.Red("✘")))
			}
			fmt.Println()
		}
	}

	if IsTraceFSMounted() {
		fmt.Printf("\t* %s\t %s\n\n", log.Bold(log.Green("tracefs mount")), log.Bold(log.Green("✔")))
	} else {
		reqsFullfiled = false
		fmt.Printf("\t* %s\t %s\n\n", log.Bold(log.Red("tracefs mount not found, needed for syscalls (mount -t tracefs none /sys/kernel/tracing/)")), log.Bold(log.Red("✘")))
	}

	if !reqsFullfiled {
		log.Raw("\n%sWARNING:%s Your kernel doesn't support some of the features OpenSnitch needs:\nRead more: https://github.com/evilsocket/opensnitch/issues/774\n", log.FG_WHITE+log.BG_YELLOW, log.RESET)
	}
}
