package collect

import (
	"bufio"
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"io/fs"
	"os"
	"os/exec"
	"path/filepath"
	"strconv"
	"strings"

	"github.com/pkg/errors"
	troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
	"k8s.io/klog/v2"
)

const (
	KernelModuleUnknown   = "unknown"
	KernelModuleLoaded    = "loaded"
	KernelModuleLoadable  = "loadable"
	KernelModuleLoading   = "loading"
	KernelModuleUnloading = "unloading"
)

type KernelModuleStatus string

type KernelModuleInfo struct {
	Size      uint64             `json:"size"`
	Instances uint               `json:"instances"`
	Status    KernelModuleStatus `json:"status"`
}

const HostKernelModulesPath = `host-collectors/system/kernel_modules.json`

// kernelModuleCollector defines the interface used to collect modules from the
// underlying host.
type kernelModuleCollector interface {
	collect(kernelRelease string) (map[string]KernelModuleInfo, []byte, error)
}

// CollectHostKernelModules is responsible for collecting kernel module status
// from the host.
type CollectHostKernelModules struct {
	hostCollector *troubleshootv1beta2.HostKernelModules
	BundlePath    string
	loadable      kernelModuleCollector
	loaded        kernelModuleCollector
}

// Title is the name of the collector.
func (c *CollectHostKernelModules) Title() string {
	return hostCollectorTitleOrDefault(c.hostCollector.HostCollectorMeta, "Kernel Modules")
}

// IsExcluded returns true if the collector has been excluded from the results.
func (c *CollectHostKernelModules) IsExcluded() (bool, error) {
	return isExcluded(c.hostCollector.Exclude)
}

// Collect the kernel module status from the host.   Modules are returned as a
// map keyed on the module name used by the kernel, e.g:
//
//	{
//	  "system/kernel_modules.json": {
//	    ...
//	    "dm_snapshot": {
//	      "instances": 8,
//	      "size": 45056,
//	      "status": "loaded"
//	    },
//	    ...
//	  },
//	}
//
// Module status may be: loaded, loadable, loading, unloading or unknown.  When
// a module is loaded, it may have one or more instances.  The size represents
// the amount of memory (in bytes) that the module is using.
func (c *CollectHostKernelModules) Collect(progressChan chan<- interface{}) (map[string][]byte, error) {
	out, err := exec.Command("uname", "-r").Output()
	if err != nil {
		return nil, errors.Wrap(err, "failed to determine kernel release")
	}
	kernelRelease := strings.TrimSpace(string(out))

	modules, loadableStderr, err := c.loadable.collect(kernelRelease)
	if err != nil {
		return nil, errors.Wrap(err, "failed to read loadable kernel modules")
	}
	if modules == nil {
		modules = map[string]KernelModuleInfo{}
	}
	loaded, _, err := c.loaded.collect(kernelRelease)
	if err != nil {
		return nil, errors.Wrap(err, "failed to read loaded kernel modules")
	}

	// Overlay with loaded modules.
	for name, module := range loaded {
		modules[name] = module
	}

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

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

	// Save stderr from loadable modules collection if present (captures permission errors)
	if len(loadableStderr) > 0 {
		stderrPath := strings.TrimSuffix(HostKernelModulesPath, ".json") + "-stderr.txt"
		klog.V(2).Infof("Saving kernel modules stderr to %q in bundle", stderrPath)
		output.SaveResult(c.BundlePath, stderrPath, bytes.NewBuffer(loadableStderr))
	}

	return output, nil
}

// kernelModulesLoadable retrieves the list of modules that can be loaded by
// the kernel.
type kernelModulesLoadable struct{}

// collect the list of modules that can be loaded by the kernel.
func (l kernelModulesLoadable) collect(kernelRelease string) (map[string]KernelModuleInfo, []byte, error) {
	modules := make(map[string]KernelModuleInfo)

	kernelPath := filepath.Join("/lib/modules", kernelRelease)
	if _, err := os.Stat(kernelPath); os.IsNotExist(err) {
		kernelPath = filepath.Join("/usr/lib/modules", kernelRelease)
		if _, err := os.Stat(kernelPath); os.IsNotExist(err) {
			kernelPath = filepath.Join("/lib/modules", kernelRelease)
			klog.V(2).Infof("kernel modules are not loadable because path %q does not exist, assuming we are in a container", kernelPath)
			return modules, nil, nil
		}
	}

	// Capture both stdout and stderr
	cmd := exec.Command("/usr/bin/find", kernelPath, "-type", "f", "-name", "*.ko*")
	var stdout, stderr bytes.Buffer
	cmd.Stdout = &stdout
	cmd.Stderr = &stderr

	err := cmd.Run()
	if err != nil {
		return nil, stderr.Bytes(), err
	}

	buf := bytes.NewBuffer(stdout.Bytes())
	scanner := bufio.NewScanner(buf)

	for scanner.Scan() {
		_, file := filepath.Split(scanner.Text())
		name := strings.TrimSuffix(file, filepath.Ext(file))

		if name == "" {
			continue
		}

		modules[name] = KernelModuleInfo{
			Status: KernelModuleLoadable,
		}
	}
	return modules, stderr.Bytes(), nil
}

// kernelModulesLoaded retrieves the list of modules that the kernel is aware of.  The
// modules will either be in loaded, loading or unloading state.
type kernelModulesLoaded struct {
	fs fs.FS
}

// collect the list of modules that the kernel is aware of.
func (l kernelModulesLoaded) collect(kernelRelease string) (map[string]KernelModuleInfo, []byte, error) {
	modules, err := l.collectProc()
	if err != nil {
		return nil, nil, fmt.Errorf("proc: %w", err)
	}

	builtin, err := l.collectBuiltin(kernelRelease)
	if err != nil {
		return nil, nil, fmt.Errorf("builtin: %w", err)
	}

	for name, module := range builtin {
		if _, ok := modules[name]; !ok {
			modules[name] = module
		}
	}

	// kernelModulesLoaded doesn't use exec commands, so no stderr to return
	return modules, nil, nil
}

func (l kernelModulesLoaded) collectProc() (map[string]KernelModuleInfo, error) {
	modules := make(map[string]KernelModuleInfo)

	file, err := l.fs.Open("proc/modules")
	if err != nil {
		return nil, err
	}
	defer file.Close()
	scanner := bufio.NewScanner(file)

	for scanner.Scan() {
		s := strings.Split(scanner.Text(), " ")
		name, size, instances, state := s[0], s[1], s[2], s[4]

		sizeInt, err := strconv.Atoi(size)
		if err != nil {
			sizeInt = 0
		}

		instancesInt, err := strconv.Atoi(instances)
		if err != nil {
			instancesInt = 0
		}

		var status KernelModuleStatus = KernelModuleUnknown
		switch state {
		case "Live":
			status = KernelModuleLoaded
		case "Loading":
			status = KernelModuleLoading
		case "Unloading":
			status = KernelModuleUnloading
		}

		modules[name] = KernelModuleInfo{
			Size:      uint64(sizeInt),
			Instances: uint(instancesInt),
			Status:    status,
		}
	}
	return modules, nil
}

func (l kernelModulesLoaded) collectBuiltin(kernelRelease string) (map[string]KernelModuleInfo, error) {
	builtinPath := filepath.Join("lib/modules", kernelRelease, "modules.builtin")
	if _, err := fs.Stat(l.fs, builtinPath); os.IsNotExist(err) {
		builtinPath = filepath.Join("usr/lib/modules", kernelRelease, "modules.builtin")
		if _, err := fs.Stat(l.fs, builtinPath); os.IsNotExist(err) {
			builtinPath = filepath.Join("lib/modules", kernelRelease, "modules.builtin")
			klog.V(2).Infof("kernel builtin modules path %q does not exist, assuming we are in a container", builtinPath)
			return nil, nil
		}
	}

	file, err := l.fs.Open(builtinPath)
	if err != nil {
		if os.IsNotExist(err) {
			return nil, nil
		}
		return nil, err
	}
	defer file.Close()

	return l.parseBuiltin(file)
}

func (l kernelModulesLoaded) parseBuiltin(r io.Reader) (map[string]KernelModuleInfo, error) {
	modules := make(map[string]KernelModuleInfo)

	scanner := bufio.NewScanner(r)
	for scanner.Scan() {
		_, file := filepath.Split(scanner.Text())
		name := strings.TrimSuffix(file, filepath.Ext(file))

		if name == "" {
			continue
		}

		modules[name] = KernelModuleInfo{
			Status: KernelModuleLoaded,
		}
	}
	if err := scanner.Err(); err != nil {
		return nil, err
	}

	return modules, nil
}

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