package exec

import (
	"context"
	"fmt"
	"io"
	"os"
	osexec "os/exec"
)

type ExecutableType string

const BashExecutable ExecutableType = `bash`
const ShExecutable ExecutableType = `sh`
const CmdExecutable ExecutableType = `cmd`

var finders map[ExecutableType](func() (shell, error)) = map[ExecutableType](func() (shell, error)){
	BashExecutable: newBashShell,
	ShExecutable:   newShShell,
	CmdExecutable:  newCmdShell,
}

type Command interface {
	// Wait for command to terminate. It must have been previously started.
	Wait() error

	// StdinPipe returns a pipe that will be connected to the command's standard input when the command starts.
	Stdout() io.ReadCloser

	// StderrPipe returns a pipe that will be connected to the command's standard error when the command starts.
	Stderr() io.ReadCloser
}

type command struct {
	cmd         *osexec.Cmd
	execContext *execContext
	stdout      io.ReadCloser
	stderr      io.ReadCloser
}

func (c *command) Wait() error {
	// After the command has finished (cmd.Wait call), remove the temporary script file
	defer os.Remove(c.execContext.scriptFile)

	err := c.cmd.Wait()
	if err != nil {
		return err
	}

	return nil
}

func (c *command) Stdout() io.ReadCloser {
	return c.stdout
}

func (c *command) Stderr() io.ReadCloser {
	return c.stderr
}

type Executor struct {
	shell shell
	dir   string
}

func NewCommandExecutor(dir string) (*Executor, error) {
	shell, err := findShell()
	if err != nil {
		return nil, err
	}
	return &Executor{
		shell: shell,
		dir:   dir,
	}, nil
}

func NewCommandExecutorWithExecutable(dir string, execType ExecutableType) (*Executor, error) {
	f, ok := finders[execType]
	if !ok {
		return nil, fmt.Errorf("%s is not supported as an artifact executable, options are: %s, %s or %s", execType, BashExecutable, ShExecutable, CmdExecutable)
	}
	shell, err := f()
	if err != nil {
		return nil, err
	}

	return &Executor{
		shell: shell,
		dir:   dir,
	}, nil
}

func (e *Executor) prepareCommand(ctx context.Context, command string) (*osexec.Cmd, *execContext, error) {
	ec, err := e.shell.prepare(command)
	if err != nil {
		return nil, nil, err
	}
	cmd := osexec.CommandContext(ctx, ec.executable, ec.args...)
	cmd.Dir = e.dir
	return cmd, ec, nil
}

func (e *Executor) StartCommand(ctx context.Context, command string) (Command, error) {
	cmd, ec, err := e.prepareCommand(ctx, command)
	if err != nil {
		return nil, err
	}
	return e.start(ctx, cmd, ec)
}

func (e *Executor) start(ctx context.Context, cmd *osexec.Cmd, ec *execContext) (Command, error) {
	stdout, err := cmd.StdoutPipe()
	if err != nil {
		return nil, err
	}

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

	return &command{cmd, ec, stdout, stderr}, cmd.Start()
}

func (e *Executor) Exec(ctx context.Context, command string) ([]byte, error) {
	cmd, ec, err := e.prepareCommand(ctx, command)
	if err != nil {
		return nil, err
	}
	defer os.Remove(ec.scriptFile)
	return cmd.CombinedOutput()
}

func (e *Executor) ShellType() ExecutableType {
	return e.shell.getType()
}