//go:build freebsd || linux || netbsd || openbsd
// +build freebsd linux netbsd openbsd

package osdetect

import (
	"errors"
	"fmt"
	"os"
	"os/exec"
	"regexp"
	"strings"

	"github.com/aws/amazon-ssm-agent/agent/log"

	c "github.com/aws/amazon-ssm-agent/agent/plugins/configurepackage/envdetect/constants"
	"github.com/aws/amazon-ssm-agent/agent/plugins/configurepackage/envdetect/utils"
)

func DetectPkgManager(platform string, version string, family string) (string, error) {
	switch family {
	case c.PlatformFamilyDebian:
		return c.PackageManagerApt, nil
	case c.PlatformFamilyAlpine:
		return c.PackageManagerAlpine, nil
	case c.PlatformFamilyArch:
		return c.PackageManagerPacman, nil
	case c.PlatformFamilySuse:
		return c.PackageManagerZipper, nil
	case c.PlatformFamilyGentoo:
		return c.PackageManagerEmerge, nil
	case c.PlatformFamilyFedora:
		return c.PackageManagerDnf, nil
	case c.PlatformFamilyRhel:
		return c.PackageManagerYum, nil
	default:
		return "", fmt.Errorf("could not detect package manager for: `%s`, `%s`, `%s`", platform, version, family)
	}
}

func DetectInitSystem() (string, error) {
	var cmdOut []byte
	var err error
	var data string

	data, err = utils.ReadFileTrim("/proc/1/comm")
	if err == nil && strings.Contains(strings.ToLower(data), "systemd") {
		return data, nil
	}

	cmdOut, err = exec.Command("ps", "-1").Output()
	if err == nil && strings.Contains(strings.ToLower(string(cmdOut)), "systemd") {
		return c.InitSystemd, nil
	}

	data, err = utils.ReadFileTrim("/proc/1/cgroup")
	if err == nil && strings.Contains(strings.ToLower(data), "docker") {
		return c.InitDocker, nil
	}

	cmdOut, err = exec.Command("service", "--version").Output()
	if err == nil && strings.Contains(strings.ToLower(string(cmdOut)), "systemd") {
		return c.InitSystemd, nil
	} else if err == nil && strings.Contains(strings.ToLower(string(cmdOut)), "openrc") {
		return c.InitOpenrc, nil
	}

	cmdOut, err = exec.Command("/sbin/initctl", "--version").Output()
	if err == nil && strings.Contains(strings.ToLower(string(cmdOut)), "upstart") {
		return c.InitUpstart, nil
	}

	// systemctl might be in /bin, /usr/bin
	cmdOut, err = exec.Command("/bin/systemctl", "--version").Output()
	if err == nil && strings.Contains(strings.ToLower(string(cmdOut)), "systemd") {
		return c.InitSystemd, nil
	}
	cmdOut, err = exec.Command("/usr/bin/systemctl", "--version").Output()
	if err == nil && strings.Contains(strings.ToLower(string(cmdOut)), "systemd") {
		return c.InitSystemd, nil
	}

	// systems which have update-rc.d might also have chkconfig but rarely the
	// other way round - so checking update-rc.d first
	_, err = exec.Command("/sbin/update-rc.d").Output()
	if err == nil {
		return c.InitUpdatercd, nil
	}

	// chkconfig does not return any useful help/version
	_, err = exec.Command("/sbin/chkconfig").Output()
	if err == nil {
		return c.InitChkconfig, nil
	}
	_, err = exec.Command("/usr/bin/chkconfig").Output()
	if err == nil {
		return c.InitChkconfig, nil
	}

	return "", errors.New("could not determine init system")
}

func DetectPlatform(_ log.T) (string, string, string, error) {
	var platform, version, platformFamily string
	var err error

	platform, version, err = scanOSrelease()
	if err != nil {
		platform, version, err = scanLSB()
		if err != nil {
			platform, version, err = scanDistributionReleaseFiles()
		}
	}

	if err != nil {
		return "", "", "", nil
	}

	platformFamily, err = platformFamilyForPlatform(platform)

	return platform, version, platformFamily, err
}

///////////////////////////
// /etc/os-release
// http://0pointer.de/blog/projects/os-release.html
// http://0pointer.de/public/systemd-man/os-release.html
///////////////////////////

func scanOSrelease() (string, string, error) {
	var lines []string
	var err error

	if _, err = os.Stat("/etc/os-release"); err == nil {
		lines, err = utils.ReadFileLines("/etc/os-release")
	} else if _, err = os.Stat("/usr/lib/os-release"); err == nil {
		lines, err = utils.ReadFileLines("/usr/lib/os-release")
	} else {
		err = errors.New("no os-release file found")
	}

	if err != nil {
		return "", "", err
	}

	return parseOSreleaseFile(lines)
}

func parseOSreleaseFile(lines []string) (string, string, error) {
	var platform, platformVersion, name string
	// TODO: Shell special characters ("$", quotes, backslash, backtick) must be escaped with backslashes
	NameRexp := regexp.MustCompile(`^NAME=["']?(.+?)["']?\n?$`)
	platformRexp := regexp.MustCompile(`^ID=["']?(.+?)["']?\n?$`)
	platformVersionRexp := regexp.MustCompile(`^VERSION_ID=["']?(.+?)["']?\n?$`)

	for _, line := range lines {
		match := platformRexp.FindStringSubmatch(line)
		if len(match) > 0 {
			platform = match[1]
		}

		match = platformVersionRexp.FindStringSubmatch(line)
		if len(match) > 0 {
			platformVersion = match[1]
		}

		match = NameRexp.FindStringSubmatch(line)
		if len(match) > 0 {
			name = match[1]
		}
	}

	if platform == "" && platformVersion == "" {
		return "", "", fmt.Errorf("could not find platform information in os-release: %v", lines)
	}

	switch platform {
	case "rhel":
		platform = c.PlatformRedhat
	case "ol":
		platform = c.PlatformOracleLinux
	case "amzn":
		platform = c.PlatformAmazon
	case "sles", "suse":
		platform = c.PlatformSuse
	case "opensuse", "opensuse-leap":
		if strings.Contains(strings.ToLower(name), "leap") {
			platform = c.PlatformOpensuseLeap
		}
	}

	return platform, platformVersion, nil
}

///////////////////////////
// man lsb_release
//
// via command:
//
//   $ ./lsb_release --all
//   Distributor ID:	Ubuntu
//   Description:	Ubuntu Zesty Zapus (development branch)
//   Release:	17.04
//   Codename:	zesty
//
// via file:
//
//   $ cat /etc/lsb-release
//   DISTRIB_ID=Ubuntu
//   DISTRIB_RELEASE=17.04
//   DISTRIB_CODENAME=zesty
//   DISTRIB_DESCRIPTION="Ubuntu Zesty Zapus (development branch)"
///////////////////////////

func scanLSB() (string, string, error) {
	var platform, platformVersion string
	var err error

	platform, platformVersion, err = scanLSBreleaseCMD()

	if err != nil {
		// If lsb-release command is not available try reading lsb file
		platform, platformVersion, err = scanLSBreleaseFile()
		if err != nil {
			return "", "", err
		}
	}

	return platform, platformVersion, nil
}

func scanLSBreleaseCMD() (string, string, error) {
	cmdOutID, err := exec.Command("lsb_release", "--short", "--id").Output()
	if err != nil {
		return "", "", err
	}

	cmdOutRelease, err := exec.Command("lsb_release", "--short", "--release").Output()
	if err != nil {
		return "", "", err
	}

	platform, version := parseLSBreleaseCMD(cmdOutID, cmdOutRelease)
	return platform, version, nil
}

func parseLSBreleaseCMD(id []byte, release []byte) (string, string) {
	var platform, platformVersion string

	platform = strings.ToLower(strings.TrimSpace(string(id)))

	// substitutions to match expected ohai identifier
	platformSubstitutions := map[string]string{
		"redhatenterpriseserver": c.PlatformRedhat,
		"suse linux":             c.PlatformSuse,
		"opensuse project":       c.PlatformOpensuse,
	}

	if result, ok := platformSubstitutions[platform]; ok {
		platform = result // overwrite platform with substitution
	}

	platformVersion = strings.ToLower(strings.TrimSpace(string(release)))

	return platform, platformVersion
}

func scanLSBreleaseFile() (string, string, error) {
	lines, err := utils.ReadFileLines("/etc/lsb-release")
	if err != nil {
		return "", "", err
	}
	return parseLSBreleaseFile(lines)
}

func parseLSBreleaseFile(lines []string) (string, string, error) {
	var platform, platformVersion string
	platformRexp := regexp.MustCompile(`^DISTRIB_ID=["']?(.+?)["']?\n?$`)
	platformVersionRexp := regexp.MustCompile(`^DISTRIB_RELEASE=["']?(.+?)["']?\n?$`)

	for _, line := range lines {
		match := platformRexp.FindStringSubmatch(line)
		if len(match) > 0 {
			platform = match[1]
		}

		match = platformVersionRexp.FindStringSubmatch(line)
		if len(match) > 0 {
			platformVersion = match[1]
		}
	}

	if platform == "" && platformVersion == "" {
		return "", "", fmt.Errorf("could not find platform information from /etc/lsb-release: %v", lines)
	}

	platform = strings.ToLower(platform)
	return platform, platformVersion, nil
}

///////////////////////////
// /etc/${distrib}-release
//
// man 1 lsb_release
//
//   The "/etc/[distrib]-release" file contains a description line which is
//   parsed to get information (especially on currently non-LSB compliant
//   systems).
//
//   The required line style is:
//   "Distributor release x.x (Codename)"
///////////////////////////

func scanRhelFile(file string) (string, string, error) {
	var platform, platformVersion string
	var err error

	data, err := utils.ReadFileTrim(file)
	if err != nil {
		return "", "", err
	}

	platform, err = getRedhatishPlatform(data)
	if err != nil {
		return "", "", err
	}

	platformVersion, err = getRedhatishVersion(data)
	if err != nil {
		return "", "", err
	}

	return platform, platformVersion, nil
}

func getRedhatishPlatform(data string) (string, error) {
	mapping := []struct {
		regex    string
		platform string
	}{
		{`(?i)Red Hat Enterprise Linux`, c.PlatformRedhat},
		{`(?i)Oracle Linux`, c.PlatformOracleLinux},
		{`(?i)CentOS( Linux)?`, c.PlatformCentos},
		{`(?i)Rocky( Linux)?`, c.PlatformRockyLinux},
		{`(?i)Fedora( Linux)?`, c.PlatformFedora},
		{`(?i)Amazon Linux`, c.PlatformAmazon},
		{`(?i)Bottlerocket( OS)?`, c.PlatformBottlerocket},
		{`(?i)AlmaLinux`, c.PlatformAlmaLinux},
	}

	for _, m := range mapping {
		r, err := regexp.Compile(m.regex)
		if err != nil {
			// Regex pattern failed to compile
			return "", fmt.Errorf("invalid regex pattern %q: %w", m.regex, err)
		}
		if r.MatchString(data) {
			return m.platform, nil
		}
	}

	return "", fmt.Errorf("could not detect RedHat platform: %s", string(data))
}

func getRedhatishVersion(data string) (string, error) {
	versionRexp := regexp.MustCompile(`^.* ([0-9.]+).*$`)
	match := versionRexp.FindStringSubmatch(data)
	if len(match) > 0 {
		return string(match[1]), nil
	}
	return "", fmt.Errorf("could not detect RedHat Version: %s", string(data))
}

///////////////////////////
// distribution specifc /etc/${distri}-release files
// https://github.com/chef/ohai/blob/master/lib/ohai/plugins/linux/platform.rb#L131-L248
///////////////////////////

func scanDistributionReleaseFiles() (string, string, error) {
	var platform, platformVersion string
	var err error

	if _, err = os.Stat("/etc/debian_version"); err == nil {
		platform = c.PlatformDebian
		platformVersion, err = utils.ReadFileTrim("/etc/debian_version")
	} else if _, err = os.Stat("/etc/bottlerocket-release"); err == nil {
		platform, platformVersion, err = scanRhelFile("/etc/bottlerocket-release")
	} else if _, err = os.Stat("/etc/redhat-release"); err == nil {
		platform, platformVersion, err = scanRhelFile("/etc/redhat-release")
	} else if _, err = os.Stat("/etc/system-release"); err == nil {
		platform, platformVersion, err = scanRhelFile("/etc/system-release")
	} else if _, err = os.Stat("/etc/alpine-release"); err == nil {
		platform = c.PlatformAlpine
		platformVersion, err = utils.ReadFileTrim("/etc/alpine-release")
	} else {
		return "", "", errors.New("could not detect Linux platform")
	}

	return platform, platformVersion, err
}

///////////////////////////
// map platform to platform family
// https://github.com/chef/ohai/blob/master/lib/ohai/plugins/linux/platform.rb#L106-L129
///////////////////////////

func platformFamilyForPlatform(platform string) (string, error) {
	switch platform {
	case c.PlatformUbuntu, c.PlatformDebian, c.PlatformRaspbian:
		return c.PlatformFamilyDebian, nil
	case c.PlatformRedhat, c.PlatformCentos, c.PlatformAmazon, c.PlatformOracleLinux, c.PlatformRockyLinux, c.PlatformAlmaLinux:
		return c.PlatformFamilyRhel, nil
	case c.PlatformFedora:
		return c.PlatformFamilyFedora, nil
	case c.PlatformAlpine:
		return c.PlatformFamilyAlpine, nil
	case c.PlatformSuse, c.PlatformOpensuse, c.PlatformOpensuseLeap:
		return c.PlatformFamilySuse, nil
	case c.PlatformGentoo:
		return c.PlatformFamilyGentoo, nil
	case c.PlatformArch:
		return c.PlatformFamilyArch, nil
	case c.PlatformBottlerocket, c.PlatformFlatcar:
		return "", fmt.Errorf("configure package is not supported on %s", platform)
	default:
		return "", fmt.Errorf("unknown platform: %s", platform)
	}
}
