package shell_local

import (
	"bufio"
	"context"
	"fmt"
	"log"
	"os"
	"path/filepath"
	"runtime"
	"sort"
	"strings"

	"github.com/hashicorp/packer/packer-plugin-sdk/multistep/commonsteps"
	packersdk "github.com/hashicorp/packer/packer-plugin-sdk/packer"
	"github.com/hashicorp/packer/packer-plugin-sdk/template/interpolate"
	"github.com/hashicorp/packer/packer-plugin-sdk/tmp"
)

func Run(ctx context.Context, ui packersdk.Ui, config *Config, generatedData map[string]interface{}) (bool, error) {
	if generatedData != nil {
		config.generatedData = generatedData
	} else {
		// No fear; probably just in the post-processor, not provisioner.
		// Make sure it's not a nil map so we can assign to it later.
		config.generatedData = make(map[string]interface{})
	}
	config.ctx.Data = generatedData
	// Check if shell-local can even execute against this runtime OS
	if len(config.OnlyOn) > 0 {
		runCommand := false
		for _, os := range config.OnlyOn {
			if os == runtime.GOOS {
				runCommand = true
				break
			}
		}
		if !runCommand {
			ui.Say(fmt.Sprintf("Skipping shell-local due to runtime OS"))
			log.Printf("[INFO] (shell-local): skipping shell-local due to missing runtime OS")
			return true, nil
		}
	}

	scripts := make([]string, len(config.Scripts))
	if len(config.Scripts) > 0 {
		copy(scripts, config.Scripts)
	} else if config.Inline != nil {
		// If we have an inline script, then turn that into a temporary
		// shell script and use that.
		tempScriptFileName, err := createInlineScriptFile(config)
		if err != nil {
			return false, err
		}

		// figure out what extension the file should have, and rename it.
		if config.TempfileExtension != "" {
			os.Rename(tempScriptFileName, fmt.Sprintf("%s.%s", tempScriptFileName, config.TempfileExtension))
			tempScriptFileName = fmt.Sprintf("%s.%s", tempScriptFileName, config.TempfileExtension)
		}

		scripts = append(scripts, tempScriptFileName)

		defer os.Remove(tempScriptFileName)
	}

	// Create environment variables to set before executing the command
	flattenedEnvVars, err := createFlattenedEnvVars(config)
	if err != nil {
		return false, err
	}

	for _, script := range scripts {
		// use absolute path in case the script is linked with forward slashes
		// on windows.
		absScript, err := filepath.Abs(script)
		if err != nil {
			return false, fmt.Errorf(
				"Error executing script: %s\n%v\n",
				absScript,
				err,
			)
		}
		interpolatedCmds, err := createInterpolatedCommands(config, absScript, flattenedEnvVars)
		if err != nil {
			return false, err
		}
		ui.Say(fmt.Sprintf("Running local shell script: %s", script))

		comm := &Communicator{
			ExecuteCommand: interpolatedCmds,
		}

		// The remoteCmd generated here isn't actually run, but it allows us to
		// use the same interafce for the shell-local communicator as we use for
		// the other communicators; ultimately, this command is just used for
		// buffers and for reading the final exit status.
		flattenedCmd := strings.Join(interpolatedCmds, " ")
		cmd := &packersdk.RemoteCmd{Command: flattenedCmd}
		log.Printf("[INFO] (shell-local): starting local command: %s", flattenedCmd)
		if err := cmd.RunWithUi(ctx, comm, ui); err != nil {
			return false, fmt.Errorf(
				"Error executing script: %s\n\n"+
					"Please see output above for more information.",
				absScript)
		}

		if err := config.ValidExitCode(cmd.ExitStatus()); err != nil {
			return false, err
		}
	}

	return true, nil
}

func createInlineScriptFile(config *Config) (string, error) {
	tf, err := tmp.File("packer-shell")
	if err != nil {
		return "", fmt.Errorf("Error preparing shell script: %s", err)
	}
	defer tf.Close()
	// Write our contents to it
	writer := bufio.NewWriter(tf)
	if config.InlineShebang != "" {
		shebang := fmt.Sprintf("#!%s\n", config.InlineShebang)
		log.Printf("[INFO] (shell-local): Prepending inline script with %s", shebang)
		writer.WriteString(shebang)
	}

	for _, command := range config.Inline {
		// interpolate command to check for template variables.
		command, err := interpolate.Render(command, &config.ctx)
		if err != nil {
			return "", err
		}

		if _, err := writer.WriteString(command + "\n"); err != nil {
			return "", fmt.Errorf("Error preparing shell script: %s", err)
		}
	}

	if err := writer.Flush(); err != nil {
		return "", fmt.Errorf("Error preparing shell script: %s", err)
	}

	err = os.Chmod(tf.Name(), 0700)
	if err != nil {
		log.Printf("[ERROR] (shell-local): error modifying permissions of temp script file: %s", err.Error())
	}
	return tf.Name(), nil
}

// Generates the final command to send to the communicator, using either the
// user-provided ExecuteCommand or defaulting to something that makes sense for
// the host OS
func createInterpolatedCommands(config *Config, script string, flattenedEnvVars string) ([]string, error) {
	config.generatedData["Vars"] = flattenedEnvVars
	config.generatedData["Script"] = script
	config.generatedData["Command"] = script

	config.ctx.Data = config.generatedData

	interpolatedCmds := make([]string, len(config.ExecuteCommand))
	for i, cmd := range config.ExecuteCommand {
		interpolatedCmd, err := interpolate.Render(cmd, &config.ctx)
		if err != nil {
			return nil, fmt.Errorf("Error processing command: %s", err)
		}
		interpolatedCmds[i] = interpolatedCmd
	}
	return interpolatedCmds, nil
}

func createFlattenedEnvVars(config *Config) (string, error) {
	flattened := ""
	envVars := make(map[string]string)

	// Always available Packer provided env vars
	envVars["PACKER_BUILD_NAME"] = config.PackerBuildName
	envVars["PACKER_BUILDER_TYPE"] = config.PackerBuilderType

	// expose ip address variables
	httpAddr := config.generatedData["PackerHTTPAddr"]
	if httpAddr != nil && httpAddr != commonsteps.HttpAddrNotImplemented {
		envVars["PACKER_HTTP_ADDR"] = httpAddr.(string)
	}
	httpIP := config.generatedData["PackerHTTPIP"]
	if httpIP != nil && httpIP != commonsteps.HttpIPNotImplemented {
		envVars["PACKER_HTTP_IP"] = httpIP.(string)
	}
	httpPort := config.generatedData["PackerHTTPPort"]
	if httpPort != nil && httpPort != commonsteps.HttpPortNotImplemented {
		envVars["PACKER_HTTP_PORT"] = httpPort.(string)
	}

	// Split vars into key/value components
	for _, envVar := range config.Vars {
		envVar, err := interpolate.Render(envVar, &config.ctx)
		if err != nil {
			return "", err
		}
		// Split vars into key/value components
		keyValue := strings.SplitN(envVar, "=", 2)
		// Store pair, replacing any single quotes in value so they parse
		// correctly with required environment variable format
		envVars[keyValue[0]] = strings.Replace(keyValue[1], "'", `'"'"'`, -1)
	}

	// Create a list of env var keys in sorted order
	var keys []string
	for k := range envVars {
		keys = append(keys, k)
	}
	sort.Strings(keys)

	for _, key := range keys {
		flattened += fmt.Sprintf(config.EnvVarFormat, key, envVars[key])
	}
	return flattened, nil
}
