package scanner

import (
	"fmt"
	"math/rand"
	"net/http"
	"os"
	ex "os/exec"
	"runtime"
	"strings"
	"time"

	"golang.org/x/exp/maps"
	"golang.org/x/xerrors"

	"github.com/future-architect/vuls/cache"
	"github.com/future-architect/vuls/config"
	"github.com/future-architect/vuls/constant"
	"github.com/future-architect/vuls/logging"
	"github.com/future-architect/vuls/models"
	"github.com/future-architect/vuls/util"
)

const (
	scannedViaRemote = "remote"
	scannedViaLocal  = "local"
	scannedViaPseudo = "pseudo"
)

var (
	errOSFamilyHeader   = xerrors.New("X-Vuls-OS-Family header is required")
	errOSReleaseHeader  = xerrors.New("X-Vuls-OS-Release header is required")
	errServerNameHeader = xerrors.New("X-Vuls-Server-Name header is required")
)

var servers, errServers []osTypeInterface

// Base Interface
type osTypeInterface interface {
	setServerInfo(config.ServerInfo)
	getServerInfo() config.ServerInfo
	setDistro(string, string)
	getDistro() config.Distro
	detectPlatform()
	detectIPS()
	getPlatform() models.Platform

	checkScanMode() error
	checkDeps() error
	checkIfSudoNoPasswd() error

	preCure() error
	postScan() error
	scanWordPress() error
	scanLibraries() error
	scanPorts() error
	scanPackages() error
	convertToModel() models.ScanResult

	parseInstalledPackages(string) (models.Packages, models.SrcPackages, error)

	runningContainers() ([]config.Container, error)
	exitedContainers() ([]config.Container, error)
	allContainers() ([]config.Container, error)

	setLogger(logging.Logger)
	getErrs() []error
	setErrs([]error)
}

// Scanner has functions for scan
type Scanner struct {
	ResultsDir     string
	TimeoutSec     int
	ScanTimeoutSec int
	CacheDBPath    string
	Debug          bool
	LogToFile      bool
	LogDir         string
	Quiet          bool
	DetectIPS      bool

	Targets map[string]config.ServerInfo
}

// Scan execute scan
func (s Scanner) Scan() error {
	logging.Log.Info("Detecting Server/Container OS... ")
	if err := s.initServers(); err != nil {
		return xerrors.Errorf("Failed to init servers. err: %w", err)
	}

	logging.Log.Info("Checking Scan Modes... ")
	if err := s.checkScanModes(); err != nil {
		return xerrors.Errorf("Fix config.toml. err: %w", err)
	}

	logging.Log.Info("Detecting Platforms... ")
	s.detectPlatform()

	if s.DetectIPS {
		logging.Log.Info("Detecting IPS identifiers... ")
		s.detectIPS()
	}

	if err := s.execScan(); err != nil {
		return xerrors.Errorf("Failed to scan. err: %w", err)
	}
	return nil
}

// Configtest checks if the server is scannable.
func (s Scanner) Configtest() error {
	logging.Log.Info("Detecting Server/Container OS... ")
	if err := s.initServers(); err != nil {
		return xerrors.Errorf("Failed to init servers. err: %w", err)
	}

	logging.Log.Info("Checking Scan Modes...")
	if err := s.checkScanModes(); err != nil {
		return xerrors.Errorf("Fix config.toml. err: %w", err)
	}

	logging.Log.Info("Checking dependencies...")
	s.checkDependencies()

	logging.Log.Info("Checking sudo settings...")
	s.checkIfSudoNoPasswd()

	logging.Log.Info("It can be scanned with fast scan mode even if warn or err messages are displayed due to lack of dependent packages or sudo settings in fast-root or deep scan mode")

	if len(servers) == 0 {
		return xerrors.Errorf("No scannable servers")
	}

	logging.Log.Info("Scannable servers are below...")
	for _, s := range servers {
		if s.getServerInfo().IsContainer() {
			fmt.Printf("%s@%s ",
				s.getServerInfo().Container.Name,
				s.getServerInfo().ServerName,
			)
		} else {
			fmt.Printf("%s ", s.getServerInfo().ServerName)
		}
	}
	fmt.Printf("\n")
	return nil
}

// ViaHTTP scans servers by HTTP header and body
func ViaHTTP(header http.Header, body string, toLocalFile bool) (models.ScanResult, error) {
	serverName := header.Get("X-Vuls-Server-Name")
	if toLocalFile && serverName == "" {
		return models.ScanResult{}, errServerNameHeader
	}

	family := header.Get("X-Vuls-OS-Family")
	if family == "" {
		return models.ScanResult{}, errOSFamilyHeader
	}

	switch family {
	case constant.Windows:
		osInfo, hotfixs, err := parseSystemInfo(toUTF8(body))
		if err != nil {
			return models.ScanResult{}, xerrors.Errorf("Failed to parse systeminfo.exe. err: %w", err)
		}

		release := header.Get("X-Vuls-OS-Release")
		if release == "" {
			logging.Log.Debugf("osInfo(systeminfo.exe): %+v", osInfo)
			release, err = detectOSName(osInfo)
			if err != nil {
				return models.ScanResult{}, xerrors.Errorf("Failed to detect os name. err: %w", err)
			}
		}

		kernelVersion := header.Get("X-Vuls-Kernel-Version")
		if kernelVersion == "" {
			kernelVersion = formatKernelVersion(osInfo)
		}

		w := &windows{
			base: base{
				Distro: config.Distro{Family: family, Release: release},
				osPackages: osPackages{
					Kernel: models.Kernel{Version: kernelVersion},
				},
				log: logging.Log,
			},
		}

		kbs, err := w.detectKBsFromKernelVersion()
		if err != nil {
			return models.ScanResult{}, xerrors.Errorf("Failed to detect KBs from kernel version. err: %w", err)
		}

		applied, unapplied := map[string]struct{}{}, map[string]struct{}{}
		for _, kb := range hotfixs {
			applied[kb] = struct{}{}
		}
		for _, kb := range kbs.Applied {
			applied[kb] = struct{}{}
		}
		for _, kb := range kbs.Unapplied {
			unapplied[kb] = struct{}{}
		}

		return models.ScanResult{
			ServerName: serverName,
			Family:     family,
			Release:    release,
			RunningKernel: models.Kernel{
				Version: kernelVersion,
			},
			WindowsKB:   &models.WindowsKB{Applied: maps.Keys(applied), Unapplied: maps.Keys(unapplied)},
			ScannedCves: models.VulnInfos{},
		}, nil
	default:
		release := header.Get("X-Vuls-OS-Release")
		if release == "" {
			return models.ScanResult{}, errOSReleaseHeader
		}

		kernelRelease := header.Get("X-Vuls-Kernel-Release")
		if kernelRelease == "" {
			logging.Log.Warn("If X-Vuls-Kernel-Release is not specified, there is a possibility of false detection")
		}

		kernelVersion := header.Get("X-Vuls-Kernel-Version")

		distro := config.Distro{
			Family:  family,
			Release: release,
		}

		kernel := models.Kernel{
			Release: kernelRelease,
			Version: kernelVersion,
		}
		installedPackages, srcPackages, err := ParseInstalledPkgs(distro, kernel, body)
		if err != nil {
			return models.ScanResult{}, err
		}

		return models.ScanResult{
			ServerName: serverName,
			Family:     family,
			Release:    release,
			RunningKernel: models.Kernel{
				Release: kernelRelease,
				Version: kernelVersion,
			},
			Packages:    installedPackages,
			SrcPackages: srcPackages,
			ScannedCves: models.VulnInfos{},
		}, nil
	}
}

// ParseInstalledPkgs parses installed pkgs line
func ParseInstalledPkgs(distro config.Distro, kernel models.Kernel, pkgList string) (models.Packages, models.SrcPackages, error) {
	base := base{
		Distro: distro,
		osPackages: osPackages{
			Kernel: kernel,
		},
		log: logging.Log,
	}

	var osType osTypeInterface
	switch distro.Family {
	case constant.Debian, constant.Ubuntu, constant.Raspbian:
		osType = &debian{base: base}
	case constant.RedHat:
		osType = &rhel{redhatBase: redhatBase{base: base}}
	case constant.CentOS:
		osType = &centos{redhatBase: redhatBase{base: base}}
	case constant.Alma:
		osType = &alma{redhatBase: redhatBase{base: base}}
	case constant.Rocky:
		osType = &rocky{redhatBase: redhatBase{base: base}}
	case constant.Oracle:
		osType = &oracle{redhatBase: redhatBase{base: base}}
	case constant.Amazon:
		osType = &amazon{redhatBase: redhatBase{base: base}}
	case constant.Fedora:
		osType = &fedora{redhatBase: redhatBase{base: base}}
	case constant.OpenSUSE, constant.OpenSUSELeap, constant.SUSEEnterpriseServer, constant.SUSEEnterpriseDesktop:
		osType = &suse{redhatBase: redhatBase{base: base}}
	default:
		return models.Packages{}, models.SrcPackages{}, xerrors.Errorf("Server mode for %s is not implemented yet", base.Distro.Family)
	}

	return osType.parseInstalledPackages(pkgList)
}

// initServers detect the kind of OS distribution of target servers
func (s Scanner) initServers() error {
	hosts, errHosts := s.detectServerOSes()
	if len(hosts) == 0 {
		return xerrors.New("No scannable host OS")
	}

	// to generate random color for logging
	rand.Seed(time.Now().UnixNano())
	for _, srv := range hosts {
		srv.setLogger(logging.NewCustomLogger(s.Debug, s.Quiet, s.LogToFile, s.LogDir, config.Colors[rand.Intn(len(config.Colors))], srv.getServerInfo().GetServerName()))
	}

	containers, errContainers := s.detectContainerOSes(hosts)
	for _, srv := range containers {
		srv.setLogger(logging.NewCustomLogger(s.Debug, s.Quiet, s.LogToFile, s.LogDir, config.Colors[rand.Intn(len(config.Colors))], srv.getServerInfo().GetServerName()))
	}

	// set to pkg global variable
	for _, host := range hosts {
		if !host.getServerInfo().ContainersOnly {
			servers = append(servers, host)
		}
	}
	servers = append(servers, containers...)
	errServers = append(errHosts, errContainers...)

	if len(servers) == 0 {
		return xerrors.New("No scannable servers")
	}
	return nil
}

func (s Scanner) detectServerOSes() (servers, errServers []osTypeInterface) {
	logging.Log.Info("Detecting OS of servers... ")
	osTypeChan := make(chan osTypeInterface, len(s.Targets))
	defer close(osTypeChan)
	for _, target := range s.Targets {
		go func(srv config.ServerInfo) {
			defer func() {
				if p := recover(); p != nil {
					logging.Log.Debugf("Panic: %s on %s", p, srv.ServerName)
				}
			}()
			if err := validateSSHConfig(&srv); err != nil {
				checkOS := unknown{base{ServerInfo: srv}}
				checkOS.setErrs([]error{err})
				osTypeChan <- &checkOS
				return
			}
			osTypeChan <- s.detectOS(srv)
		}(target)
	}

	timeout := time.After(time.Duration(s.TimeoutSec) * time.Second)
	for i := 0; i < len(s.Targets); i++ {
		select {
		case res := <-osTypeChan:
			if 0 < len(res.getErrs()) {
				errServers = append(errServers, res)
				logging.Log.Errorf("(%d/%d) Failed: %s, err: %+v", i+1, len(s.Targets), res.getServerInfo().ServerName, res.getErrs())
			} else {
				servers = append(servers, res)
				logging.Log.Infof("(%d/%d) Detected: %s: %s", i+1, len(s.Targets), res.getServerInfo().ServerName, res.getDistro())
			}
		case <-timeout:
			msg := "Timed out while detecting servers"
			logging.Log.Error(msg)
			for servername, sInfo := range s.Targets {
				found := false
				for _, o := range append(servers, errServers...) {
					if servername == o.getServerInfo().ServerName {
						found = true
						break
					}
				}
				if !found {
					u := &unknown{}
					u.setServerInfo(sInfo)
					u.setErrs([]error{xerrors.New("Timed out")})
					errServers = append(errServers, u)
					logging.Log.Errorf("(%d/%d) Timed out: %s", i+1, len(s.Targets), servername)
				}
			}
		}
	}
	return
}

func validateSSHConfig(c *config.ServerInfo) error {
	if isLocalExec(c.Port, c.Host) || c.Type == constant.ServerTypePseudo {
		return nil
	}

	logging.Log.Debugf("Validating SSH Settings for Server:%s ...", c.GetServerName())

	if runtime.GOOS == "windows" {
		c.Distro.Family = constant.Windows
	}
	defer func(c *config.ServerInfo) {
		c.Distro.Family = ""
	}(c)

	sshBinaryPath, err := lookpath(c.Distro.Family, "ssh")
	if err != nil {
		return xerrors.Errorf("Failed to lookup ssh binary path. err: %w", err)
	}

	sshConfigCmd := buildSSHConfigCmd(sshBinaryPath, c)
	logging.Log.Debugf("Executing... %s", strings.Replace(sshConfigCmd, "\n", "", -1))
	configResult := localExec(*c, sshConfigCmd, noSudo)
	if !configResult.isSuccess() {
		if strings.Contains(configResult.Stderr, "unknown option -- G") {
			logging.Log.Warn("SSH configuration validation is skipped. To enable validation, G option introduced in OpenSSH 6.8 must be enabled.")
			return nil
		}
		return xerrors.Errorf("Failed to print SSH configuration. err: %w", configResult.Error)
	}
	sshConfig := parseSSHConfiguration(configResult.Stdout)
	c.User = sshConfig.user
	logging.Log.Debugf("Setting SSH User:%s for Server:%s ...", sshConfig.user, c.GetServerName())
	c.Port = sshConfig.port
	logging.Log.Debugf("Setting SSH Port:%s for Server:%s ...", sshConfig.port, c.GetServerName())
	if c.User == "" || c.Port == "" {
		return xerrors.New("Failed to find User or Port setting. Please check the User or Port settings for SSH")
	}

	if sshConfig.strictHostKeyChecking == "false" {
		return nil
	}
	if sshConfig.proxyCommand != "" || sshConfig.proxyJump != "" {
		logging.Log.Debug("known_host check under Proxy is not yet implemented")
		return nil
	}

	logging.Log.Debugf("Checking if the host's public key is in known_hosts...")
	knownHostsPaths := []string{}
	for _, knownHost := range append(sshConfig.userKnownHosts, sshConfig.globalKnownHosts...) {
		if knownHost != "" && knownHost != "/dev/null" {
			knownHostsPaths = append(knownHostsPaths, knownHost)
		}
	}
	if len(knownHostsPaths) == 0 {
		return xerrors.New("Failed to find any known_hosts to use. Please check the UserKnownHostsFile and GlobalKnownHostsFile settings for SSH")
	}

	sshKeyscanBinaryPath, err := lookpath(c.Distro.Family, "ssh-keyscan")
	if err != nil {
		return xerrors.Errorf("Failed to lookup ssh-keyscan binary path. err: %w", err)
	}
	sshScanCmd := strings.Join([]string{sshKeyscanBinaryPath, "-p", c.Port, sshConfig.hostname}, " ")
	r := localExec(*c, sshScanCmd, noSudo)
	if !r.isSuccess() {
		return xerrors.Errorf("Failed to ssh-keyscan. cmd: %s, err: %w", sshScanCmd, r.Error)
	}
	serverKeys := parseSSHScan(r.Stdout)

	sshKeygenBinaryPath, err := lookpath(c.Distro.Family, "ssh-keygen")
	if err != nil {
		return xerrors.Errorf("Failed to lookup ssh-keygen binary path. err: %w", err)
	}
	for _, knownHosts := range knownHostsPaths {
		var hostname string
		if sshConfig.hostKeyAlias != "" {
			hostname = sshConfig.hostKeyAlias
		} else {
			if c.Port != "" && c.Port != "22" {
				hostname = fmt.Sprintf("\"[%s]:%s\"", sshConfig.hostname, c.Port)
			} else {
				hostname = sshConfig.hostname
			}
		}
		cmd := fmt.Sprintf("%s -F %s -f %s", sshKeygenBinaryPath, hostname, knownHosts)
		logging.Log.Debugf("Executing... %s", strings.Replace(cmd, "\n", "", -1))
		if r := localExec(*c, cmd, noSudo); r.isSuccess() {
			keyType, clientKey, err := parseSSHKeygen(r.Stdout)
			if err != nil {
				return xerrors.Errorf("Failed to parse ssh-keygen result. stdout: %s, err: %w", r.Stdout, r.Error)
			}
			if serverKey, ok := serverKeys[keyType]; ok && serverKey == clientKey {
				return nil
			}
			return xerrors.Errorf("Failed to find the server key that matches the key registered in the client. The server key may have been changed. Please exec `$ %s` and `$ %s` or `$ %s`",
				fmt.Sprintf("%s -R %s -f %s", sshKeygenBinaryPath, hostname, knownHosts),
				strings.Join(buildSSHBaseCmd(sshBinaryPath, c, nil), " "),
				buildSSHKeyScanCmd(sshKeyscanBinaryPath, c.Port, knownHostsPaths[0], sshConfig))
		}
	}
	return xerrors.Errorf("Failed to find the host in known_hosts. Please exec `$ %s` or `$ %s`",
		strings.Join(buildSSHBaseCmd(sshBinaryPath, c, nil), " "),
		buildSSHKeyScanCmd(sshKeyscanBinaryPath, c.Port, knownHostsPaths[0], sshConfig))
}

func lookpath(family, file string) (string, error) {
	switch family {
	case constant.Windows:
		return fmt.Sprintf("%s.exe", strings.TrimPrefix(file, ".exe")), nil
	default:
		p, err := ex.LookPath(file)
		if err != nil {
			return "", err
		}
		return p, nil
	}
}

func buildSSHBaseCmd(sshBinaryPath string, c *config.ServerInfo, options []string) []string {
	cmd := []string{sshBinaryPath}
	if len(options) > 0 {
		cmd = append(cmd, options...)
	}
	if c.SSHConfigPath != "" {
		cmd = append(cmd, "-F", c.SSHConfigPath)
	}
	if c.KeyPath != "" {
		cmd = append(cmd, "-i", c.KeyPath)
	}
	if c.Port != "" {
		cmd = append(cmd, "-p", c.Port)
	}
	if c.User != "" {
		cmd = append(cmd, "-l", c.User)
	}
	if len(c.JumpServer) > 0 {
		cmd = append(cmd, "-J", strings.Join(c.JumpServer, ","))
	}
	cmd = append(cmd, c.Host)
	return cmd
}

func buildSSHConfigCmd(sshBinaryPath string, c *config.ServerInfo) string {
	return strings.Join(buildSSHBaseCmd(sshBinaryPath, c, []string{"-G"}), " ")
}

func buildSSHKeyScanCmd(sshKeyscanBinaryPath, port, knownHosts string, sshConfig sshConfiguration) string {
	cmd := []string{sshKeyscanBinaryPath}
	if sshConfig.hashKnownHosts == "yes" {
		cmd = append(cmd, "-H")
	}
	if port != "" {
		cmd = append(cmd, "-p", port)
	}
	return strings.Join(append(cmd, sshConfig.hostname, ">>", knownHosts), " ")
}

type sshConfiguration struct {
	hostname              string
	hostKeyAlias          string
	hashKnownHosts        string
	user                  string
	port                  string
	strictHostKeyChecking string
	globalKnownHosts      []string
	userKnownHosts        []string
	proxyCommand          string
	proxyJump             string
}

func parseSSHConfiguration(stdout string) sshConfiguration {
	sshConfig := sshConfiguration{}
	for _, line := range strings.Split(stdout, "\n") {
		line = strings.TrimSuffix(line, "\r")
		switch {
		case strings.HasPrefix(line, "user "):
			sshConfig.user = strings.TrimPrefix(line, "user ")
		case strings.HasPrefix(line, "hostname "):
			sshConfig.hostname = strings.TrimPrefix(line, "hostname ")
		case strings.HasPrefix(line, "hostkeyalias "):
			sshConfig.hostKeyAlias = strings.TrimPrefix(line, "hostkeyalias ")
		case strings.HasPrefix(line, "hashknownhosts "):
			sshConfig.hashKnownHosts = strings.TrimPrefix(line, "hashknownhosts ")
		case strings.HasPrefix(line, "port "):
			sshConfig.port = strings.TrimPrefix(line, "port ")
		case strings.HasPrefix(line, "stricthostkeychecking "):
			sshConfig.strictHostKeyChecking = strings.TrimPrefix(line, "stricthostkeychecking ")
		case strings.HasPrefix(line, "globalknownhostsfile "):
			sshConfig.globalKnownHosts = strings.Split(strings.TrimPrefix(line, "globalknownhostsfile "), " ")
		case strings.HasPrefix(line, "userknownhostsfile "):
			sshConfig.userKnownHosts = strings.Split(strings.TrimPrefix(line, "userknownhostsfile "), " ")
		case strings.HasPrefix(line, "proxycommand "):
			sshConfig.proxyCommand = strings.TrimPrefix(line, "proxycommand ")
		case strings.HasPrefix(line, "proxyjump "):
			sshConfig.proxyJump = strings.TrimPrefix(line, "proxyjump ")
		}
	}
	return sshConfig
}

func parseSSHScan(stdout string) map[string]string {
	keys := map[string]string{}
	for _, line := range strings.Split(stdout, "\n") {
		line = strings.TrimSuffix(line, "\r")
		if line == "" || strings.HasPrefix(line, "# ") {
			continue
		}
		if ss := strings.Split(line, " "); len(ss) == 3 {
			keys[ss[1]] = ss[2]
		}
	}
	return keys
}

func parseSSHKeygen(stdout string) (string, string, error) {
	for _, line := range strings.Split(stdout, "\n") {
		line = strings.TrimSuffix(line, "\r")
		if line == "" || strings.HasPrefix(line, "# ") {
			continue
		}

		// HashKnownHosts yes
		if strings.HasPrefix(line, "|1|") {
			ss := strings.Split(line, "|")
			if ss := strings.Split(ss[len(ss)-1], " "); len(ss) == 3 {
				return ss[1], ss[2], nil
			}
		} else {
			if ss := strings.Split(line, " "); len(ss) == 3 {
				return ss[1], ss[2], nil
			}
		}
	}
	return "", "", xerrors.New("Failed to parse ssh-keygen result. err: public key not found")
}

func (s Scanner) detectContainerOSes(hosts []osTypeInterface) (actives, inactives []osTypeInterface) {
	logging.Log.Info("Detecting OS of containers... ")
	osTypesChan := make(chan []osTypeInterface, len(hosts))
	defer close(osTypesChan)
	for _, host := range hosts {
		go func(h osTypeInterface) {
			defer func() {
				if p := recover(); p != nil {
					logging.Log.Debugf("Panic: %s on %s",
						p, h.getServerInfo().GetServerName())
				}
			}()
			osTypesChan <- s.detectContainerOSesOnServer(h)
		}(host)
	}

	timeout := time.After(time.Duration(s.TimeoutSec) * time.Second)
	for i := 0; i < len(hosts); i++ {
		select {
		case res := <-osTypesChan:
			for _, osi := range res {
				sinfo := osi.getServerInfo()
				if 0 < len(osi.getErrs()) {
					inactives = append(inactives, osi)
					logging.Log.Errorf("Failed: %s err: %+v", sinfo.ServerName, osi.getErrs())
					continue
				}
				actives = append(actives, osi)
				logging.Log.Infof("Detected: %s@%s: %s",
					sinfo.Container.Name, sinfo.ServerName, osi.getDistro())
			}
		case <-timeout:
			logging.Log.Error("Some containers timed out")
		}
	}
	return
}

func (s Scanner) detectContainerOSesOnServer(containerHost osTypeInterface) (oses []osTypeInterface) {
	containerHostInfo := containerHost.getServerInfo()
	if len(containerHostInfo.ContainersIncluded) == 0 {
		return
	}

	running, err := containerHost.runningContainers()
	if err != nil {
		containerHost.setErrs([]error{xerrors.Errorf(
			"Failed to get running containers on %s. err: %w",
			containerHost.getServerInfo().ServerName, err)})
		return append(oses, containerHost)
	}

	if containerHostInfo.ContainersIncluded[0] == "${running}" {
		for _, containerInfo := range running {
			found := false
			for _, ex := range containerHost.getServerInfo().ContainersExcluded {
				if containerInfo.Name == ex || containerInfo.ContainerID == ex {
					found = true
				}
			}
			if found {
				continue
			}

			copied := containerHostInfo
			copied.SetContainer(config.Container{
				ContainerID: containerInfo.ContainerID,
				Name:        containerInfo.Name,
				Image:       containerInfo.Image,
			})
			os := s.detectOS(copied)
			oses = append(oses, os)
		}
		return oses
	}

	exitedContainers, err := containerHost.exitedContainers()
	if err != nil {
		containerHost.setErrs([]error{xerrors.Errorf(
			"Failed to get exited containers on %s. err: %w",
			containerHost.getServerInfo().ServerName, err)})
		return append(oses, containerHost)
	}

	var exited, unknown []string
	for _, container := range containerHostInfo.ContainersIncluded {
		found := false
		for _, c := range running {
			if c.ContainerID == container || c.Name == container {
				copied := containerHostInfo
				copied.SetContainer(c)
				os := s.detectOS(copied)
				oses = append(oses, os)
				found = true
				break
			}
		}

		if !found {
			foundInExitedContainers := false
			for _, c := range exitedContainers {
				if c.ContainerID == container || c.Name == container {
					exited = append(exited, container)
					foundInExitedContainers = true
					break
				}
			}
			if !foundInExitedContainers {
				unknown = append(unknown, container)
			}
		}
	}
	if 0 < len(exited) || 0 < len(unknown) {
		containerHost.setErrs([]error{xerrors.Errorf(
			"Some containers on %s are exited or unknown. exited: %s, unknown: %s",
			containerHost.getServerInfo().ServerName, exited, unknown)})
		return append(oses, containerHost)
	}
	return oses
}

func (s Scanner) detectOS(c config.ServerInfo) osTypeInterface {
	if itsMe, osType, _ := detectPseudo(c); itsMe {
		return osType
	}

	if !isLocalExec(c.Port, c.Host) {
		if err := testFirstSSHConnection(c); err != nil {
			osType := &unknown{base{ServerInfo: c}}
			osType.setErrs([]error{xerrors.Errorf("Failed to test first SSH Connection. err: %w", err)})
			return osType
		}
	}

	if itsMe, osType := detectWindows(c); itsMe {
		logging.Log.Debugf("Windows. Host: %s:%s", c.Host, c.Port)
		return osType
	}

	if itsMe, osType := detectDebian(c); itsMe {
		logging.Log.Debugf("Debian based Linux. Host: %s:%s", c.Host, c.Port)
		return osType
	}

	if itsMe, osType := detectRedhat(c); itsMe {
		logging.Log.Debugf("Redhat based Linux. Host: %s:%s", c.Host, c.Port)
		return osType
	}

	if itsMe, osType := detectSUSE(c); itsMe {
		logging.Log.Debugf("SUSE Linux. Host: %s:%s", c.Host, c.Port)
		return osType
	}

	if itsMe, osType := detectFreebsd(c); itsMe {
		logging.Log.Debugf("FreeBSD. Host: %s:%s", c.Host, c.Port)
		return osType
	}

	if itsMe, osType := detectAlpine(c); itsMe {
		logging.Log.Debugf("Alpine. Host: %s:%s", c.Host, c.Port)
		return osType
	}

	osType := &unknown{base{ServerInfo: c}}
	osType.setErrs([]error{xerrors.New("Unknown OS Type")})
	return osType
}

func testFirstSSHConnection(c config.ServerInfo) error {
	for i := 3; i > 0; i-- {
		rChan := make(chan execResult, 1)
		go func() {
			rChan <- exec(c, "exit", noSudo)
		}()
		select {
		case r := <-rChan:
			if r.ExitStatus == 255 {
				return xerrors.Errorf("Unable to connect via SSH. Scan with -vvv option to print SSH debugging messages and check SSH settings.\n%s", r)
			}
			return nil
		case <-time.After(time.Duration(3) * time.Second):
		}
	}
	logging.Log.Warnf("First SSH Connection to Host: %s:%s timeout", c.Host, c.Port)
	return nil
}

// checkScanModes checks scan mode
func (s Scanner) checkScanModes() error {
	for _, s := range servers {
		if err := s.checkScanMode(); err != nil {
			return xerrors.Errorf("servers.%s.scanMode err: %w",
				s.getServerInfo().GetServerName(), err)
		}
	}
	return nil
}

// checkDependencies checks dependencies are installed on target servers.
func (s Scanner) checkDependencies() {
	parallelExec(func(o osTypeInterface) error {
		return o.checkDeps()
	}, s.TimeoutSec)
	return
}

// checkIfSudoNoPasswd checks whether vuls can sudo with nopassword via SSH
func (s Scanner) checkIfSudoNoPasswd() {
	parallelExec(func(o osTypeInterface) error {
		return o.checkIfSudoNoPasswd()
	}, s.TimeoutSec)
	return
}

// detectPlatform detects the platform of each servers.
func (s Scanner) detectPlatform() {
	parallelExec(func(o osTypeInterface) error {
		o.detectPlatform()
		// Logging only if platform can not be specified
		return nil
	}, s.TimeoutSec)

	for i, s := range servers {
		if s.getServerInfo().IsContainer() {
			logging.Log.Infof("(%d/%d) %s on %s is running on %s",
				i+1, len(servers),
				s.getServerInfo().Container.Name,
				s.getServerInfo().ServerName,
				s.getPlatform().Name,
			)

		} else {
			logging.Log.Infof("(%d/%d) %s is running on %s",
				i+1, len(servers),
				s.getServerInfo().ServerName,
				s.getPlatform().Name,
			)
		}
	}
	return
}

// detectIPS detects the IPS of each servers.
func (s Scanner) detectIPS() {
	parallelExec(func(o osTypeInterface) error {
		o.detectIPS()
		// Logging only if IPS can not be specified
		return nil
	}, s.TimeoutSec)

	for i, s := range servers {
		if !s.getServerInfo().IsContainer() {
			logging.Log.Infof("(%d/%d) %s has %d IPS integration",
				i+1, len(servers),
				s.getServerInfo().ServerName,
				len(s.getServerInfo().IPSIdentifiers),
			)
		}
	}
}

// execScan scan
func (s Scanner) execScan() error {
	if len(servers) == 0 {
		return xerrors.New("No server defined. Check the configuration")
	}

	if err := s.setupChangelogCache(); err != nil {
		return err
	}
	defer func() {
		if cache.DB != nil {
			cache.DB.Close()
		}
	}()

	scannedAt := time.Now()
	dir, err := EnsureResultDir(s.ResultsDir, scannedAt)
	if err != nil {
		return err
	}

	results, err := s.getScanResults(scannedAt)
	if err != nil {
		return err
	}

	for i, r := range results {
		if server, ok := s.Targets[r.ServerName]; ok {
			results[i] = r.ClearFields(server.IgnoredJSONKeys)
		}
	}

	return writeScanResults(dir, results)
}

func (s Scanner) setupChangelogCache() error {
	needToSetupCache := false
	for _, s := range servers {
		switch s.getDistro().Family {
		case constant.Raspbian:
			needToSetupCache = true
			break
		case constant.Ubuntu, constant.Debian:
			//TODO changelog cache for RedHat, Oracle, Amazon, CentOS is not implemented yet.
			if s.getServerInfo().Mode.IsDeep() {
				needToSetupCache = true
			}
			break
		}
	}
	if needToSetupCache {
		if err := cache.SetupBolt(s.CacheDBPath, logging.Log); err != nil {
			return err
		}
	}
	return nil
}

// getScanResults returns ScanResults
func (s Scanner) getScanResults(scannedAt time.Time) (results models.ScanResults, err error) {
	parallelExec(func(o osTypeInterface) (err error) {
		if o.getServerInfo().Module.IsScanOSPkg() {
			if err = o.preCure(); err != nil {
				return err
			}
			if err = o.scanPackages(); err != nil {
				return err
			}
			if err = o.postScan(); err != nil {
				return err
			}
		}
		if o.getServerInfo().Module.IsScanPort() {
			if err = o.scanPorts(); err != nil {
				// continue scanning
				logging.Log.Warnf("Failed to scan Ports: %+v", err)
			}
		}
		if o.getServerInfo().Module.IsScanWordPress() {
			if err = o.scanWordPress(); err != nil {
				return xerrors.Errorf("Failed to scan WordPress: %w", err)
			}
		}
		if o.getServerInfo().Module.IsScanLockFile() {
			if err = o.scanLibraries(); err != nil {
				return xerrors.Errorf("Failed to scan Library: %w", err)
			}
		}
		return nil
	}, s.ScanTimeoutSec)

	hostname, _ := os.Hostname()
	ipv4s, ipv6s, err := util.IP()
	if err != nil {
		logging.Log.Warnf("Failed to get scannedIPs. err: %+v", err)
	}

	for _, s := range append(servers, errServers...) {
		r := s.convertToModel()
		r.CheckEOL()
		r.ScannedAt = scannedAt
		r.ScannedVersion = config.Version
		r.ScannedRevision = config.Revision
		r.ScannedBy = hostname
		r.ScannedIPv4Addrs = ipv4s
		r.ScannedIPv6Addrs = ipv6s
		r.Config.Scan = config.Conf
		results = append(results, r)

		if 0 < len(r.Warnings) {
			logging.Log.Warnf("Some warnings occurred during scanning on %s. Please fix the warnings to get a useful information. Execute configtest subcommand before scanning to know the cause of the warnings. warnings: %v",
				r.ServerName, r.Warnings)
		}
	}
	return results, nil
}
