package collect

import (
	"bufio"
	"bytes"
	"compress/gzip"
	"encoding/json"
	"fmt"
	"io"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"strings"

	"github.com/pkg/errors"
	troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
)

type CollectHostKernelConfigs struct {
	hostCollector *troubleshootv1beta2.HostKernelConfigs
	BundlePath    string
}

type KConfigs map[string]string

const HostKernelConfigsPath = `host-collectors/system/kernel-configs.json`
const HostKernelConfigsFileName = `kernel-configs.json`

const (
	kConfigBuiltIn  string = "y"
	kConfigAsModule string = "m"
	kConfigLeftOut  string = "n"
)

func (c *CollectHostKernelConfigs) Title() string {
	return hostCollectorTitleOrDefault(c.hostCollector.HostCollectorMeta, "kernel-configs")
}

func (c *CollectHostKernelConfigs) IsExcluded() (bool, error) {
	return isExcluded(c.hostCollector.Exclude)
}

func (c *CollectHostKernelConfigs) Collect(progressChan chan<- interface{}) (map[string][]byte, error) {

	kernelRelease, err := getKernelRelease()
	if err != nil {
		return nil, errors.Wrap(err, "failed to get kernel release")
	}

	var kConfigs KConfigs
	kConfigs, err = loadKConfigs(kernelRelease)
	if err != nil {
		return nil, errors.Wrap(err, "failed to load kernel configs")
	}

	b, err := json.Marshal(kConfigs)
	if err != nil {
		return nil, errors.Wrap(err, "failed to marshal kernel configs")
	}

	output := NewResult()
	output.SaveResult(c.BundlePath, HostKernelConfigsPath, bytes.NewBuffer(b))

	return output, nil
}

func getKernelRelease() (string, error) {
	out, err := exec.Command("uname", "-r").Output()
	if err != nil {
		return "", errors.Wrap(err, "failed to determine kernel release using uname -r")
	}
	release := strings.TrimSpace(string(out))
	return release, nil
}

// https://github.com/k0sproject/k0s/blob/ddee3f980443e19620e678a6e1dc136ff053bff9/internal/pkg/sysinfo/probes/linux/kernel.go#L282
// loadKConfigs checks a list of well-known file system paths for kernel
// configuration files and tries to parse them.
func loadKConfigs(kernelRelease string) (KConfigs, error) {
	// At least some references to those paths may be fond here:
	// https://github.com/torvalds/linux/blob/v4.3/init/Kconfig#L794
	// https://github.com/torvalds/linux/blob/v4.3/init/Kconfig#L9
	possiblePaths := []string{
		"/boot/config-" + kernelRelease,
		"/usr/src/linux-" + kernelRelease + "/.config",
		"/usr/src/linux/.config",
		"/usr/lib/modules/" + kernelRelease + "/config",
		"/usr/lib/ostree-boot/config-" + kernelRelease,
		"/usr/lib/kernel/config-" + kernelRelease,
		"/usr/src/linux-headers-" + kernelRelease + "/.config",
		"/lib/modules/" + kernelRelease + "/build/.config",
	}

	// /proc/config.gz reflects the currently running kernel. Only use it when
	// the requested kernel release matches the running kernel.
	currentRelease, err := getKernelRelease()
	if err == nil && currentRelease == kernelRelease {
		possiblePaths = append([]string{"/proc/config.gz"}, possiblePaths...)
	}

	for _, path := range possiblePaths {
		// open file for reading
		f, err := os.Open(path)
		if err != nil {
			if os.IsNotExist(err) {
				continue
			}
			return nil, err
		}
		defer f.Close()

		r := io.Reader(bufio.NewReader(f))

		// This is a gzip file (config.gz), unzip it.
		if filepath.Ext(path) == ".gz" {
			gr, err := gzip.NewReader(r)
			if err != nil {
				return nil, err
			}
			defer gr.Close()
			r = gr
		}

		return parseKConfigs(r)
	}

	return nil, errors.Errorf("no kernel config files found for kernel release %q", kernelRelease)
}

// parseKConfigs parses `r` line by line, extracting all kernel config options.
func parseKConfigs(r io.Reader) (KConfigs, error) {
	configs := KConfigs{}
	kConfigLineRegex := regexp.MustCompile(fmt.Sprintf(
		"^(CONFIG_[A-Z0-9_]+)=([%s%s%s])$",
		string(kConfigBuiltIn), string(kConfigLeftOut), string(kConfigAsModule),
	))
	s := bufio.NewScanner(r)
	for s.Scan() {
		if err := s.Err(); err != nil {
			return nil, err
		}

		if matches := kConfigLineRegex.FindStringSubmatch(s.Text()); matches != nil {
			configs[matches[1]] = matches[2]
		}
	}
	return configs, nil
}

func (c *CollectHostKernelConfigs) RemoteCollect(progressChan chan<- interface{}) (map[string][]byte, error) {
	return nil, ErrRemoteCollectorNotImplemented
}
