// Package options provides configuration options for the boilerplate tool.
package options

import (
	"fmt"

	"github.com/urfave/cli/v2"

	"github.com/gruntwork-io/boilerplate/errors"
	"github.com/gruntwork-io/boilerplate/getterhelper"
	"github.com/gruntwork-io/boilerplate/variables"
)

const OptTemplateURL = "template-url"
const OptOutputFolder = "output-folder"
const OptNonInteractive = "non-interactive"
const OptVar = "var"
const OptVarFile = "var-file"
const OptMissingKeyAction = "missing-key-action"
const OptMissingConfigAction = "missing-config-action"
const OptNoHooks = "no-hooks"
const OptNoShell = "no-shell"
const OptDisableDependencyPrompt = "disable-dependency-prompt"

// BoilerplateOptions represents the command-line options for the boilerplate app
type BoilerplateOptions struct {
	Vars                    map[string]any
	ShellCommandAnswers     map[string]bool
	TemplateURL             string
	TemplateFolder          string
	OutputFolder            string
	OnMissingKey            MissingKeyAction
	OnMissingConfig         MissingConfigAction
	NonInteractive          bool
	NoHooks                 bool
	NoShell                 bool
	DisableDependencyPrompt bool
	ExecuteAllShellCommands bool
}

// Validate that the options have reasonable values and return an error if they don't
func (options *BoilerplateOptions) Validate() error {
	if options.TemplateURL == "" {
		return errors.WithStackTrace(ErrTemplateURLOptionCannotBeEmpty)
	}

	if err := getterhelper.ValidateTemplateURL(options.TemplateURL); err != nil {
		return err
	}

	if options.OutputFolder == "" {
		return errors.WithStackTrace(ErrOutputFolderOptionCannotBeEmpty)
	}

	return nil
}

// ParseOptions parses the command line options provided by the user
func ParseOptions(cliContext *cli.Context) (*BoilerplateOptions, error) {
	vars, err := variables.ParseVars(cliContext.StringSlice(OptVar), cliContext.StringSlice(OptVarFile))
	if err != nil {
		return nil, err
	}

	missingKeyAction := DefaultMissingKeyAction
	missingKeyActionValue := cliContext.String(OptMissingKeyAction)

	if missingKeyActionValue != "" {
		missingKeyAction, err = ParseMissingKeyAction(missingKeyActionValue)
		if err != nil {
			return nil, err
		}
	}

	missingConfigAction := DefaultMissingConfigAction
	missingConfigActionValue := cliContext.String(OptMissingConfigAction)

	if missingConfigActionValue != "" {
		missingConfigAction, err = ParseMissingConfigAction(missingConfigActionValue)
		if err != nil {
			return nil, err
		}
	}

	templateURL, templateFolder, err := DetermineTemplateConfig(cliContext.String(OptTemplateURL))
	if err != nil {
		return nil, err
	}

	options := &BoilerplateOptions{
		TemplateURL:             templateURL,
		TemplateFolder:          templateFolder,
		OutputFolder:            cliContext.String(OptOutputFolder),
		NonInteractive:          cliContext.Bool(OptNonInteractive),
		OnMissingKey:            missingKeyAction,
		OnMissingConfig:         missingConfigAction,
		Vars:                    vars,
		NoHooks:                 cliContext.Bool(OptNoHooks),
		NoShell:                 cliContext.Bool(OptNoShell),
		DisableDependencyPrompt: cliContext.Bool(OptDisableDependencyPrompt),
		ExecuteAllShellCommands: false,
		ShellCommandAnswers:     make(map[string]bool),
	}

	if err := options.Validate(); err != nil {
		return nil, err
	}

	return options, nil
}

// DetermineTemplateConfig decides what should be passed to TemplateURL and TemplateFolder. This parses the templateUrl
// and determines if it is a local path. If so, use that path directly instead of downloading it to a temp working dir.
// We do this by setting the template folder, which will instruct the process routine to skip downloading the template.
// Returns TemplateURL, TemplateFolder, error
func DetermineTemplateConfig(templateURL string) (string, string, error) {
	url, err := getterhelper.ParseGetterURL(templateURL)
	if err != nil {
		return "", "", err
	}

	if url.Scheme == "file" {
		// Intentionally return as both TemplateURL and TemplateFolder so that validation passes, but still skip
		// download.
		return templateURL, templateURL, nil
	}

	return templateURL, "", nil
}

// MissingKeyAction is an enum that represents what we can do when a template looks up a missing key. This typically happens
// when there is a typo in the variable name in a template.
type MissingKeyAction string

var (
	Invalid       = MissingKeyAction("invalid") // print <no value> for any missing key
	ZeroValue     = MissingKeyAction("zero")    // print the zero value of the missing key
	ExitWithError = MissingKeyAction("error")   // exit with an error when there is a missing key
)

var AllMissingKeyActions = []MissingKeyAction{Invalid, ZeroValue, ExitWithError}
var DefaultMissingKeyAction = ExitWithError

// ParseMissingKeyAction converts the given string to a MissingKeyAction enum, or returns an error if this is not a valid value for the
// MissingKeyAction enum
func ParseMissingKeyAction(str string) (MissingKeyAction, error) {
	for _, missingKeyAction := range AllMissingKeyActions {
		if string(missingKeyAction) == str {
			return missingKeyAction, nil
		}
	}

	return MissingKeyAction(""), errors.WithStackTrace(InvalidMissingKeyAction(str))
}

// MissingConfigAction is an enum that represents what to do when the template folder passed to boilerplate does not contain a
// boilerplate.yml file.
type MissingConfigAction string

var (
	Exit   = MissingConfigAction("exit")
	Ignore = MissingConfigAction("ignore")
)
var AllMissingConfigActions = []MissingConfigAction{Exit, Ignore}
var DefaultMissingConfigAction = Exit

// ParseMissingConfigAction converts the given string to a MissingConfigAction enum, or returns an error if this is not a valid value for the
// MissingConfigAction enum
func ParseMissingConfigAction(str string) (MissingConfigAction, error) {
	for _, missingConfigAction := range AllMissingConfigActions {
		if string(missingConfigAction) == str {
			return missingConfigAction, nil
		}
	}

	return MissingConfigAction(""), errors.WithStackTrace(InvalidMissingConfigAction(str))
}

//nolint:staticcheck
var (
	ErrTemplateURLOptionCannotBeEmpty  = fmt.Errorf("The --%s option cannot be empty", OptTemplateURL)
	ErrOutputFolderOptionCannotBeEmpty = fmt.Errorf("The --%s option cannot be empty", OptOutputFolder)
)

type InvalidMissingKeyAction string

func (err InvalidMissingKeyAction) Error() string {
	return fmt.Sprintf("Invalid MissingKeyAction '%s'. Value must be one of: %s", string(err), AllMissingKeyActions)
}

type InvalidMissingConfigAction string

func (err InvalidMissingConfigAction) Error() string {
	return fmt.Sprintf("Invalid MissingConfigAction '%s'. Value must be one of: %s", string(err), AllMissingConfigActions)
}
