package exec

import (
	"bytes"
	"fmt"
	"os"
	"os/exec"
	"os/signal"
	"path"
	"runtime"
	"strconv"
	"strings"
	"syscall"

	"github.com/pkg/errors"
	"github.com/smallstep/cli/utils/sysutils"
)

// LookPath is an alias for exec.LookPath. It searches for an executable named
// file in the directories named by the PATH environment variable. If file
// contains a slash, it is tried directly and the PATH is not consulted. The
// result may be an absolute path or a path relative to the current directory.
func LookPath(file string) (string, error) {
	return exec.LookPath(file)
}

// IsWSL returns true if Windows Subsystem for Linux is detected.
//
// "Official" way of detecting WSL
// https://github.com/Microsoft/WSL/issues/423#issuecomment-221627364
func IsWSL() bool {
	b, err := os.ReadFile("/proc/sys/kernel/osrelease")
	if err != nil {
		return false
	}
	return strings.Contains(strings.ToLower(string(b)), "microsoft") || strings.Contains(strings.ToLower(string(b)), "wsl")
}

// Exec is wrapper over syscall.Exec, invokes the execve(2) system call. On
// windows it executes Run with the same arguments.
func Exec(name string, arg ...string) {
	if runtime.GOOS == "windows" {
		Run(name, arg...)
		return
	}
	args := append([]string{name}, arg...)
	if err := sysutils.Exec(name, args, os.Environ()); err != nil {
		errorAndExit(name, err)
	}
}

// Run is a wrapper over os/exec Cmd.Run that configures Stderr/Stdin/Stdout
// to the current ones and wait until the process finishes, exiting with the
// same code. Run will also forward all the signals sent to step to the
// command.
func Run(name string, arg ...string) {
	cmd, exitCh, err := run(name, arg...)
	if err != nil {
		errorAndExit(name, err)
	}

	if err = cmd.Wait(); err != nil {
		errorf(name, err)
	}

	// exit and wait until os.Exit
	exitCh <- getExitStatus(cmd)
	exitCh <- 0
}

// RunWithPid calls Run and writes the process ID in pidFile.
func RunWithPid(pidFile, name string, arg ...string) {
	f, err := os.OpenFile(pidFile, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600)
	if err != nil {
		errorAndExit(name, err)
	}

	// Run process
	cmd, exitCh, err := run(name, arg...)
	if err != nil {
		f.Close()
		os.Remove(f.Name())
		errorAndExit(name, err)
	}

	// Write pid
	f.WriteString(strconv.Itoa(cmd.Process.Pid))

	f.Close()

	// Wait until it finishes
	if err = cmd.Wait(); err != nil {
		errorf(name, err)
	}

	// clean, exit and wait until os.Exit
	os.Remove(f.Name())
	exitCh <- getExitStatus(cmd)
	exitCh <- 0
}

// OpenInBrowser opens the given url on a web browser
func OpenInBrowser(url, browser string) error {
	var cmd *exec.Cmd
	switch runtime.GOOS {
	case "darwin":
		if browser == "" {
			cmd = exec.Command("open", url)
		} else {
			cmd = exec.Command("open", "-a", browser, url)
		}
	case "linux":
		if IsWSL() {
			cmd = exec.Command("rundll32.exe", "url.dll,FileProtocolHandler", url)
		} else {
			cmd = exec.Command("xdg-open", url)
		}
	case "windows":
		cmd = exec.Command("rundll32", "url.dll,FileProtocolHandler", url)
	default:
		return errors.Errorf("unsupported platform '%s'", runtime.GOOS)
	}

	return errors.WithStack(cmd.Start())
}

// Step executes step with the given commands and returns the standard output.
func Step(args ...string) ([]byte, error) {
	var stdout bytes.Buffer
	//nolint:gosec // arguments controlled by step.
	cmd := exec.Command(os.Args[0], args...)
	cmd.Stdin = os.Stdin
	cmd.Stderr = os.Stderr
	cmd.Stdout = &stdout

	if err := cmd.Start(); err != nil {
		return nil, errors.Wrapf(err, "error starting: %s %s", os.Args[0], strings.Join(args, " "))
	}
	if err := cmd.Wait(); err != nil {
		return nil, errors.Wrapf(err, "error running: %s %s", os.Args[0], strings.Join(args, " "))
	}

	return stdout.Bytes(), nil
}

// Command executes the given command with it's arguments and returns the
// standard output.
func Command(name string, args ...string) ([]byte, error) {
	var stderr bytes.Buffer
	cmd := exec.Command(name, args...)
	cmd.Stderr = &stderr
	out, err := cmd.Output()
	if err != nil {
		return nil, errors.Wrapf(err, "error running %s %s:\n%s", name, strings.Join(args, " "), stderr.String())
	}
	return out, nil
}

func run(name string, arg ...string) (*exec.Cmd, chan int, error) {
	cmd := exec.Command(name, arg...)
	cmd.Stderr = os.Stderr
	cmd.Stdin = os.Stdin
	cmd.Stdout = os.Stdout

	// Start process
	if err := cmd.Start(); err != nil {
		return nil, nil, err
	}

	// Forward signals
	exitCh := make(chan int)
	go signalHandler(cmd, exitCh)

	return cmd, exitCh, nil
}

func getExitStatus(cmd *exec.Cmd) int {
	if cmd.ProcessState != nil {
		//nolint:gocritic // ignore single conditional in switch warning
		switch sys := cmd.ProcessState.Sys().(type) {
		case syscall.WaitStatus:
			return sys.ExitStatus()
		}
	}
	return 1
}

func errorf(name string, err error) {
	fmt.Fprintf(os.Stderr, "%s: %s\n", path.Base(name), err.Error())
}

func errorAndExit(name string, err error) {
	fmt.Fprintf(os.Stderr, "%s: %s\n", path.Base(name), err.Error())
	os.Exit(-1)
}

// signalHandler forwards all the signals to the cmd.
func signalHandler(cmd *exec.Cmd, exitCh chan int) {
	// signal.Notify prefers a buffered channel. From documentation: "For a
	// channel used for notification of just one signal value, a buffer of size
	// 1 is sufficient." As we do not know how many signal values the cmd is
	// expecting we select 1 as a sane default. In the future maybe we can make
	// this value configurable.
	signals := make(chan os.Signal, 1)
	signal.Notify(signals)
	defer signal.Stop(signals)
	for {
		select {
		case sig := <-signals:
			cmd.Process.Signal(sig)
		case code := <-exitCh:
			//nolint:gocritic // ignore exitAfterDefer error - defer is required for recovery.
			os.Exit(code)
		}
	}
}
