/*
Copyright 2016 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package system

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

var _ Validator = &KernelValidator{}

// KernelValidator validates kernel. Currently only validate kernel version
// and kernel configuration.
type KernelValidator struct {
	kernelRelease string
	Reporter      Reporter
}

// Name is part of the system.Validator interface.
func (k *KernelValidator) Name() string {
	return "kernel"
}

// kConfigOption is the possible kernel config option.
type kConfigOption string

const (
	builtIn  kConfigOption = "y"
	asModule kConfigOption = "m"
	leftOut  kConfigOption = "n"

	// validKConfigRegex is the regex matching kernel configuration line.
	validKConfigRegex = "^CONFIG_[A-Z0-9_]+=[myn]"
	// kernelConfigPrefix is the prefix of kernel configuration.
	kernelConfigPrefix = "CONFIG_"
)

// Validate is part of the system.Validator interface.
func (k *KernelValidator) Validate(spec SysSpec) ([]error, []error) {
	helper := KernelValidatorHelperImpl{}
	release, err := helper.GetKernelReleaseVersion()
	if err != nil {
		return nil, []error{fmt.Errorf("failed to get kernel release: %w", err)}
	}
	k.kernelRelease = release
	var errs, warns []error
	if warn := k.validateKernelVersion(spec.KernelSpec); warn != nil {
		warns = append(warns, warn)
	}
	// only validate kernel config when necessary (currently no kernel config for windows)
	if len(spec.KernelSpec.Required) > 0 || len(spec.KernelSpec.Forbidden) > 0 || len(spec.KernelSpec.Optional) > 0 ||
		len(spec.KernelSpec.RequiredCgroupsV1) > 0 || len(spec.KernelSpec.RequiredCgroupsV2) > 0 {
		if err = k.validateKernelConfig(spec.KernelSpec); err != nil {
			errs = append(errs, err)
		}
	}
	return warns, errs
}

// validateKernelVersion validates the kernel version.
func (k *KernelValidator) validateKernelVersion(kSpec KernelSpec) error {
	versionRegexps := kSpec.Versions
	for _, versionRegexp := range versionRegexps {
		r := regexp.MustCompile(versionRegexp)
		if r.MatchString(k.kernelRelease) {
			k.Reporter.Report("KERNEL_VERSION", k.kernelRelease, good)
			return nil
		}
	}
	k.Reporter.Report("KERNEL_VERSION", k.kernelRelease, bad)
	return fmt.Errorf("kernel release %s is unsupported. %s", k.kernelRelease, kSpec.VersionsNote)
}

// validateKernelConfig validates the kernel configurations.
func (k *KernelValidator) validateKernelConfig(kSpec KernelSpec) error {
	allConfig, err := k.getKernelConfig()
	if err != nil {
		return fmt.Errorf("failed to parse kernel config: %w", err)
	}
	return k.validateCachedKernelConfig(allConfig, kSpec)
}

// validateCachedKernelConfig validates the kernel confgiurations cached in internal data type.
func (k *KernelValidator) validateCachedKernelConfig(allConfig map[string]kConfigOption, kSpec KernelSpec) error {
	badConfigs := []string{}
	// reportAndRecord is a helper function to record bad config when
	// report.
	reportAndRecord := func(name, msg, desc string, result ValidationResultType) {
		if result == bad {
			badConfigs = append(badConfigs, name)
		}
		// report description when the config is bad or warn.
		if result != good && desc != "" {
			msg = msg + " - " + desc
		}
		k.Reporter.Report(name, msg, result)
	}
	const (
		required = iota
		optional
		forbidden
	)
	validateOpt := func(config KernelConfig, expect int) {
		var found, missing ValidationResultType
		switch expect {
		case required:
			found, missing = good, bad
		case optional:
			found, missing = good, warn
		case forbidden:
			found, missing = bad, good
		}
		var name string
		var opt kConfigOption
		var ok bool
		for _, name = range append([]string{config.Name}, config.Aliases...) {
			name = kernelConfigPrefix + name
			if opt, ok = allConfig[name]; ok {
				break
			}
		}
		if !ok {
			reportAndRecord(name, "not set", config.Description, missing)
			return
		}
		switch opt {
		case builtIn:
			reportAndRecord(name, "enabled", config.Description, found)
		case asModule:
			reportAndRecord(name, "enabled (as module)", config.Description, found)
		case leftOut:
			reportAndRecord(name, "disabled", config.Description, missing)
		default:
			reportAndRecord(name, fmt.Sprintf("unknown option: %s", opt), config.Description, missing)
		}
	}
	for _, config := range kSpec.Required {
		validateOpt(config, required)
	}
	_, isCgroupsV2, err := getUnifiedMountpoint(mountsFilePath)
	if err != nil {
		return fmt.Errorf("failed to get unified mountpoint: %w", err)
	}
	if isCgroupsV2 {
		for _, config := range kSpec.RequiredCgroupsV2 {
			validateOpt(config, required)
		}
	} else {
		for _, config := range kSpec.RequiredCgroupsV1 {
			validateOpt(config, required)
		}
	}

	for _, config := range kSpec.Optional {
		validateOpt(config, optional)
	}
	for _, config := range kSpec.Forbidden {
		validateOpt(config, forbidden)
	}
	if len(badConfigs) > 0 {
		return fmt.Errorf("unexpected kernel config: %s", strings.Join(badConfigs, " "))
	}
	return nil
}

// getKernelConfigReader search kernel config file in a predefined list. Once the kernel config
// file is found it will read the configurations into a byte buffer and return. If the kernel
// config file is not found, it will try to load kernel config module and retry again.
func (k *KernelValidator) getKernelConfigReader() (io.Reader, error) {
	possibePaths := []string{
		"/proc/config.gz",
		"/boot/config-" + k.kernelRelease,
		"/usr/src/linux-" + k.kernelRelease + "/.config",
		"/usr/src/linux/.config",
		"/usr/lib/modules/" + k.kernelRelease + "/config",
		"/usr/lib/ostree-boot/config-" + k.kernelRelease,
		"/usr/lib/kernel/config-" + k.kernelRelease,
		"/usr/src/linux-headers-" + k.kernelRelease + "/.config",
		"/lib/modules/" + k.kernelRelease + "/build/.config",
	}
	configsModule := "configs"
	modprobeCmd := "modprobe"
	// loadModule indicates whether we've tried to load kernel config module ourselves.
	loadModule := false
	for {
		for _, path := range possibePaths {
			_, err := os.Stat(path)
			if err != nil {
				continue
			}
			// Buffer the whole file, so that we can close the file and unload
			// kernel config module in this function.
			b, err := os.ReadFile(path)
			if err != nil {
				return nil, err
			}
			var r io.Reader
			r = bytes.NewReader(b)
			// This is a gzip file (config.gz), unzip it.
			if filepath.Ext(path) == ".gz" {
				r, err = gzip.NewReader(r)
				if err != nil {
					return nil, err
				}
			}
			return r, nil
		}
		// If we've tried to load kernel config module, break and return error.
		if loadModule {
			break
		}
		// If the kernel config file is not found, try to load the kernel
		// config module and check again.
		output, err := exec.Command(modprobeCmd, configsModule).CombinedOutput()
		if err != nil {
			return nil, fmt.Errorf("unable to load kernel module: %q, output: %q, err: %w",
				configsModule, output, err)
		}
		// Unload the kernel config module to make sure the validation have no side effect.
		defer exec.Command(modprobeCmd, "-r", configsModule).Run()
		loadModule = true
	}
	return nil, fmt.Errorf("no config path in %v is available", possibePaths)
}

// getKernelConfig gets kernel config from kernel config file and convert kernel config to internal type.
func (k *KernelValidator) getKernelConfig() (map[string]kConfigOption, error) {
	r, err := k.getKernelConfigReader()
	if err != nil {
		return nil, err
	}
	return k.parseKernelConfig(r)
}

// parseKernelConfig converts kernel config to internal type.
func (k *KernelValidator) parseKernelConfig(r io.Reader) (map[string]kConfigOption, error) {
	config := map[string]kConfigOption{}
	regex := regexp.MustCompile(validKConfigRegex)
	s := bufio.NewScanner(r)
	for s.Scan() {
		if err := s.Err(); err != nil {
			return nil, err
		}
		line := strings.TrimSpace(s.Text())
		if !regex.MatchString(line) {
			continue
		}
		fields := strings.Split(line, "=")
		config[fields[0]] = kConfigOption(fields[1][0])
	}
	return config, nil
}
