package launch

import (
	"io"
	"os"
	"os/exec"

	"github.com/BurntSushi/toml"
	"github.com/pkg/errors"
)

// ExecDRunner is responsible for running ExecD binaries.
type ExecDRunner struct {
	Out, Err io.Writer // Out and Err can be used to configure Stdout and Stderr processes run by ExecDRunner.
}

// NewExecDRunner creates an ExecDRunner with Out and Err set to stdout and stderr
func NewExecDRunner() *ExecDRunner {
	return &ExecDRunner{
		Out: os.Stdout,
		Err: os.Stderr,
	}
}

// ExecD executes the executable file at path and sets the returned variables in env. The executable at path
// should implement the ExecD interface in the buildpack specification https://github.com/buildpacks/spec/blob/main/buildpack.md#execd
func (e *ExecDRunner) ExecD(path string, env Env) error {
	pr, pw, err := os.Pipe()
	if err != nil {
		return errors.Wrap(err, "failed to create pipe")
	}
	errChan := make(chan error, 1)
	go func() {
		defer pw.Close()
		cmd := exec.Command(path)
		cmd.Stdout = e.Out
		cmd.Stderr = e.Err
		cmd.Env = env.List()
		if err := setHandle(cmd, pw); err != nil {
			errChan <- err
		} else {
			errChan <- cmd.Run()
		}
	}()

	out, err := io.ReadAll(pr)
	if cmdErr := <-errChan; cmdErr != nil {
		// prefer the error from the command
		return errors.Wrapf(cmdErr, "failed to execute exec.d file at path '%s'", path)
	} else if err != nil {
		// return the read error only if the command succeeded
		return errors.Wrapf(err, "failed to read output from exec.d file at path '%s'", path)
	}

	envVars := map[string]string{}
	if _, err := toml.Decode(string(out), &envVars); err != nil {
		return errors.Wrapf(err, "failed to decode output from exec.d file at path '%s'", path)
	}
	for k, v := range envVars {
		env.Set(k, v)
	}
	return nil
}

func setHandle(cmd *exec.Cmd, f *os.File) error {
	cmd.ExtraFiles = []*os.File{f}
	return nil
}
