package runtime

import (
	"errors"
	"fmt"
	"strings"

	"github.com/runatlantis/atlantis/server/core/config/valid"
	"github.com/runatlantis/atlantis/server/events/command"
)

// EnvStepRunner set environment variables.
type MultiEnvStepRunner struct {
	RunStepRunner *RunStepRunner
}

// Run runs the multienv step command.
// The command must return a json string containing the array of name-value pairs that are being added as extra environment variables
func (r *MultiEnvStepRunner) Run(
	ctx command.ProjectContext,
	shell *valid.CommandShell,
	command string,
	path string,
	envs map[string]string,
	postProcessOutput valid.PostProcessRunOutputOption,
) (string, error) {
	res, err := r.RunStepRunner.Run(ctx, shell, command, path, envs, false, postProcessOutput)
	if err != nil {
		return "", err
	}

	var sb strings.Builder
	if len(res) == 0 {
		sb.WriteString("No dynamic environment variable added")
	} else {
		sb.WriteString("Dynamic environment variables added:\n")

		vars, err := parseMultienvLine(res)
		if err != nil {
			return "", fmt.Errorf("Invalid environment variable definition: %s (%w)", res, err)
		}

		for i := 0; i < len(vars); i += 2 {
			key := vars[i]
			envs[key] = vars[i+1]
			sb.WriteString(key)
			sb.WriteRune('\n')
		}
	}

	switch postProcessOutput {
	case valid.PostProcessRunOutputHide:
		return "", nil
	case valid.PostProcessRunOutputShow:
		return sb.String(), nil
	default:
		return sb.String(), nil
	}
}

func parseMultienvLine(in string) ([]string, error) {
	in = strings.TrimSpace(in)
	if in == "" {
		return nil, nil
	}
	if len(in) < 3 {
		return nil, errors.New("invalid syntax") // TODO
	}

	var res []string
	var inValue, dquoted, squoted, escaped bool
	var i int

	for j, r := range in {
		if !inValue {
			if r == '=' {
				inValue = true
				res = append(res, in[i:j])
				i = j + 1
			}
			if r == ' ' || r == '\t' {
				return nil, errInvalidKeySyntax
			}
			if r == ',' && len(res) > 0 {
				i = j + 1
			}
			continue
		}

		if r == '"' && !squoted {
			if j == i && !dquoted { // value is double quoted
				dquoted = true
				i = j + 1
			} else if dquoted && in[j-1] != '\\' {
				res = append(res, unescape(in[i:j], escaped))
				i = j + 1
				dquoted = false
				inValue = false
			} else if in[j-1] != '\\' {
				return nil, errMisquoted
			} else if in[j-1] == '\\' {
				escaped = true
			}
			continue
		}

		if r == '\'' && !dquoted {
			if j == i && !squoted { // value is double quoted
				squoted = true
				i = j + 1
			} else if squoted && in[j-1] != '\\' {
				res = append(res, in[i:j])
				i = j + 1
				squoted = false
				inValue = false
			}
			continue
		}

		if r == ',' && !dquoted && !squoted && inValue {
			res = append(res, in[i:j])
			i = j + 1
			inValue = false
		}
	}

	if i < len(in) {
		if !inValue {
			return nil, errRemaining
		}
		res = append(res, unescape(in[i:], escaped))
		inValue = false
	}
	if dquoted || squoted {
		return nil, errMisquoted
	}
	if inValue {
		return nil, errRemaining
	}

	return res, nil
}

func unescape(s string, escaped bool) string {
	if escaped {
		return strings.ReplaceAll(strings.ReplaceAll(s, `\\`, `\`), `\"`, `"`)
	}
	return s
}

var (
	errInvalidKeySyntax = errors.New("invalid key syntax")
	errMisquoted        = errors.New("misquoted")
	errRemaining        = errors.New("remaining unparsed data")
)
