/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package commands

import (
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"math/rand"
	"os"
	"path/filepath"
	"reflect"
	"strings"
	"time"

	"github.com/apache/openwhisk-cli/wski18n"
	"github.com/apache/openwhisk-client-go/whisk"

	"github.com/fatih/color"
	"github.com/mattn/go-colorable"
	"github.com/spf13/cobra"
)

const (
	MEMORY_LIMIT      = 256
	TIMEOUT_LIMIT     = 60000
	LOGSIZE_LIMIT     = 10
	CONCURRENCY_LIMIT = 1
	ACTIVATION_ID     = "activationId"
	WEB_EXPORT_ANNOT  = "web-export"
	RAW_HTTP_ANNOT    = "raw-http"
	FINAL_ANNOT       = "final"
	WEB_SECURE_ANNOT  = "require-whisk-auth"
	NODE_JS_EXT       = ".js"
	PYTHON_EXT        = ".py"
	JAVA_EXT          = ".jar"
	SWIFT_EXT         = ".swift"
	ZIP_EXT           = ".zip"
	PHP_EXT           = ".php"
	BAL_BIN_EXT       = ".balx"
	RUBY_EXT          = ".rb"
	RUST_EXT          = ".rs"
	GO_EXT            = ".go"
	NODE_JS           = "nodejs"
	PYTHON            = "python"
	JAVA              = "java"
	SWIFT             = "swift"
	PHP               = "php"
	RUBY              = "ruby"
	RUST              = "rust"
	GO                = "go"
	BAL               = "ballerina"
	DEFAULT           = "default"
	BLACKBOX          = "blackbox"
	SEQUENCE          = "sequence"
	FETCH_CODE        = true
	DO_NOT_FETCH_CODE = false
	ACTION_UPDATE     = true
	ACTION_CREATE     = false
	MAX_JS_INT        = 1<<53 - 1
)

var actionCmd = &cobra.Command{
	Use:   "action",
	Short: wski18n.T("work with actions"),
}

var actionCreateCmd = &cobra.Command{
	Use:           "create ACTION_NAME ACTION",
	Short:         wski18n.T("create a new action"),
	SilenceUsage:  true,
	SilenceErrors: true,
	PreRunE:       SetupClientConfig,
	RunE: func(cmd *cobra.Command, args []string) error {
		var action *whisk.Action
		var err error

		if whiskErr := CheckArgs(
			args,
			1,
			2,
			"Action create",
			wski18n.T("An action name and code artifact are required.")); whiskErr != nil {
			return whiskErr
		}

		if action, err = parseAction(cmd, args, false); err != nil {
			return actionParseError(cmd, args, err)
		}

		if action, err = augmentAction(cmd, args, action, ACTION_CREATE); err != nil {
			return actionParseError(cmd, args, err)
		}

		if _, _, err = Client.Actions.Insert(action, false); err != nil {
			return actionInsertError(action, err)
		}

		printActionCreated(action.Name)

		return nil
	},
}

var actionUpdateCmd = &cobra.Command{
	Use:           "update ACTION_NAME [ACTION]",
	Short:         wski18n.T("update an existing action, or create an action if it does not exist"),
	SilenceUsage:  true,
	SilenceErrors: true,
	PreRunE:       SetupClientConfig,
	RunE: func(cmd *cobra.Command, args []string) error {
		var action *whisk.Action
		var err error

		if whiskErr := CheckArgs(
			args,
			1,
			2,
			"Action update",
			wski18n.T("An action name is required. A code artifact is optional.")); whiskErr != nil {
			return whiskErr
		}

		if action, err = parseAction(cmd, args, true); err != nil {
			return actionParseError(cmd, args, err)
		}

		if action, err = augmentAction(cmd, args, action, ACTION_UPDATE); err != nil {
			return actionParseError(cmd, args, err)
		}

		if _, _, err = Client.Actions.Insert(action, true); err != nil {
			return actionInsertError(action, err)
		}

		printActionUpdated(action.Name)

		return nil
	},
}

var actionInvokeCmd = &cobra.Command{
	Use:           "invoke ACTION_NAME",
	Short:         wski18n.T("invoke action"),
	SilenceUsage:  true,
	SilenceErrors: true,
	PreRunE:       SetupClientConfig,
	RunE: func(cmd *cobra.Command, args []string) error {
		var err error
		var qualifiedName = new(QualifiedName)
		var parameters interface{}

		if whiskErr := CheckArgs(
			args,
			1,
			1,
			"Action invoke",
			wski18n.T("An action name is required.")); whiskErr != nil {
			return whiskErr
		}

		if qualifiedName, err = NewQualifiedName(args[0]); err != nil {
			return NewQualifiedNameError(args[0], err)
		}

		parameters = getParameters(Flags.common.param, false, false)
		blocking := Flags.common.blocking || Flags.action.result
		resultOnly := Flags.action.result
		header := !resultOnly

		res, err := invokeAction(
			*qualifiedName,
			parameters,
			blocking,
			resultOnly)

		return printInvocationResponse(*qualifiedName, blocking, header, res, err)
	},
}

func invokeAction(
	qualifiedName QualifiedName,
	parameters interface{},
	blocking bool,
	result bool) (interface{}, error) {
	// TODO remove all global modifiers
	Client.Namespace = qualifiedName.GetNamespace()
	res, _, err := Client.Actions.Invoke(
		qualifiedName.GetEntityName(),
		parameters,
		blocking,
		result)
	return res, err
}

func printInvocationResponse(
	qualifiedName QualifiedName,
	blocking bool,
	header bool,
	result interface{},
	err error) error {
	if err == nil {
		printInvocationMsg(qualifiedName, blocking, header, result, color.Output)
	} else {
		if !blocking {
			return handleInvocationError(err, qualifiedName.GetEntityName())
		} else {
			return printFailedBlockingInvocationResponse(qualifiedName, header, result, err)
		}
	}

	return err
}

func printFailedBlockingInvocationResponse(
	qualifiedName QualifiedName,
	header bool,
	result interface{},
	err error) error {
	if isBlockingTimeout(err) {
		printBlockingTimeoutMsg(
			qualifiedName.GetNamespace(),
			qualifiedName.GetEntityName(),
			getValueFromResponse(ACTIVATION_ID, result))
		return err
	} else if isApplicationError(err) {
		printInvocationMsg(
			qualifiedName,
			true,
			header,
			result,
			colorable.NewColorableStderr())
		return err
	} else {
		return handleInvocationError(err, qualifiedName.GetEntityName())
	}
}

var actionGetCmd = &cobra.Command{
	Use:           "get ACTION_NAME [FIELD_FILTER | --summary | --url]",
	Short:         wski18n.T("get action"),
	SilenceUsage:  true,
	SilenceErrors: true,
	PreRunE:       SetupClientConfig,
	RunE: func(cmd *cobra.Command, args []string) error {
		var err error
		var field string
		var action *whisk.Action
		var qualifiedName = new(QualifiedName)
		var fetchCode bool

		if whiskErr := CheckArgs(args, 1, 2, "Action get", wski18n.T("An action name is required.")); whiskErr != nil {
			return whiskErr
		}

		if !Flags.action.url && !Flags.common.summary && len(args) > 1 {
			field = args[1]

			if !fieldExists(&whisk.Action{}, field) {
				return invalidFieldFilterError(field)
			}
		}

		if qualifiedName, err = NewQualifiedName(args[0]); err != nil {
			return NewQualifiedNameError(args[0], err)
		}

		Client.Namespace = qualifiedName.GetNamespace()
		fetchCode = cmd.LocalFlags().Changed(SAVE_AS_FLAG) || cmd.LocalFlags().Changed(SAVE_FLAG)

		if action, _, err = Client.Actions.Get(qualifiedName.GetEntityName(), fetchCode); err != nil {
			return actionGetError(qualifiedName.GetEntityName(), fetchCode, err)
		}

		if Flags.action.url {
			actionURL, err := action.ActionURL(Properties.APIHost,
				DefaultOpenWhiskApiPath,
				Properties.APIVersion,
				qualifiedName.GetPackageName())
			if err != nil {
				errStr := wski18n.T("Invalid host address '{{.host}}': {{.err}}",
					map[string]interface{}{"host": Properties.APIHost, "err": err})
				werr := whisk.MakeWskError(errors.New(errStr), whisk.EXIT_CODE_ERR_GENERAL, whisk.DISPLAY_MSG, whisk.NO_DISPLAY_USAGE)
				return werr
			}
			printActionGetWithURL(qualifiedName.GetEntity(), actionURL)
		} else if Flags.common.summary {
			printSummary(action)
		} else if cmd.LocalFlags().Changed(SAVE_AS_FLAG) || cmd.LocalFlags().Changed(SAVE_FLAG) {
			return saveCode(*action, Flags.action.saveAs)
		} else {
			if len(field) > 0 {
				printActionGetWithField(qualifiedName.GetEntityName(), field, action)
			} else {
				printActionGet(qualifiedName.GetEntityName(), action)
			}
		}

		return nil
	},
}

var actionDeleteCmd = &cobra.Command{
	Use:           "delete ACTION_NAME",
	Short:         wski18n.T("delete action"),
	SilenceUsage:  true,
	SilenceErrors: true,
	PreRunE:       SetupClientConfig,
	RunE: func(cmd *cobra.Command, args []string) error {
		var qualifiedName = new(QualifiedName)
		var err error

		if whiskErr := CheckArgs(
			args,
			1,
			1,
			"Action delete",
			wski18n.T("An action name is required.")); whiskErr != nil {
			return whiskErr
		}

		if qualifiedName, err = NewQualifiedName(args[0]); err != nil {
			return NewQualifiedNameError(args[0], err)
		}

		Client.Namespace = qualifiedName.GetNamespace()

		if _, err = Client.Actions.Delete(qualifiedName.GetEntityName()); err != nil {
			return actionDeleteError(qualifiedName.GetEntityName(), err)
		}

		printActionDeleted(qualifiedName.GetEntityName())

		return nil
	},
}

var actionListCmd = &cobra.Command{
	Use:           "list [ NAMESPACE | PACKAGE_NAME ]",
	Short:         wski18n.T("list all actions in a namespace or actions contained in a package"),
	SilenceUsage:  true,
	SilenceErrors: true,
	PreRunE:       SetupClientConfig,
	RunE: func(cmd *cobra.Command, args []string) error {
		var qualifiedName = new(QualifiedName)
		var actions []whisk.Action
		var err error

		if whiskErr := CheckArgs(
			args,
			0,
			1,
			"Action list",
			wski18n.T("An optional namespace is the only valid argument.")); whiskErr != nil {
			return whiskErr
		}

		if len(args) == 1 {
			if qualifiedName, err = NewQualifiedName(args[0]); err != nil {
				return NewQualifiedNameError(args[0], err)
			}

			Client.Namespace = qualifiedName.GetNamespace()
		}

		options := &whisk.ActionListOptions{
			Skip:  Flags.common.skip,
			Limit: Flags.common.limit,
		}

		if actions, _, err = Client.Actions.List(qualifiedName.GetEntityName(), options); err != nil {
			return actionListError(qualifiedName.GetEntityName(), options, err)
		}

		sortByName := Flags.common.nameSort
		printList(actions, sortByName)

		return nil
	},
}

func parseAction(cmd *cobra.Command, args []string, update bool) (*whisk.Action, error) {
	var err error
	var existingAction *whisk.Action
	var paramArgs []string
	var annotArgs []string
	var parameters interface{}
	var annotations interface{}

	var qualifiedName = new(QualifiedName)

	if qualifiedName, err = NewQualifiedName(args[0]); err != nil {
		return nil, NewQualifiedNameError(args[0], err)
	}

	Client.Namespace = qualifiedName.GetNamespace()
	action := new(whisk.Action)
	action.Name = qualifiedName.GetEntityName()
	action.Namespace = qualifiedName.GetNamespace()
	action.Limits = getLimits(
		cmd.LocalFlags().Changed(MEMORY_FLAG),
		cmd.LocalFlags().Changed(LOG_SIZE_FLAG),
		cmd.LocalFlags().Changed(TIMEOUT_FLAG),
		cmd.LocalFlags().Changed(CONCURRENCY_FLAG),
		Flags.action.memory,
		Flags.action.logsize,
		Flags.action.timeout,
		Flags.action.concurrency)

	paramArgs = Flags.common.param
	annotArgs = Flags.common.annotation

	if len(paramArgs) > 0 {
		if parameters, err = getJSONFromStrings(paramArgs, true); err != nil {
			return nil, getJSONFromStringsParamError(paramArgs, true, err)
		}

		action.Parameters = parameters.(whisk.KeyValueArr)
	}

	if len(annotArgs) > 0 {
		if annotations, err = getJSONFromStrings(annotArgs, true); err != nil {
			return nil, getJSONFromStringsAnnotError(annotArgs, true, err)
		}

		action.Annotations = annotations.(whisk.KeyValueArr)
	}

	if len(Flags.action.kind) > 0 && len(Flags.action.docker) > 0 {
		errStr := wski18n.T("Cannot specify both --kind and --docker at the same time.")
		return nil, whisk.MakeWskError(errors.New(errStr), whisk.NOT_ALLOWED, whisk.DISPLAY_MSG, whisk.NO_DISPLAY_USAGE)
	}

	if Flags.action.copy {
		var copiedQualifiedName = new(QualifiedName)

		if copiedQualifiedName, err = NewQualifiedName(args[1]); err != nil {
			return nil, NewQualifiedNameError(args[1], err)
		}

		Client.Namespace = copiedQualifiedName.GetNamespace()

		if existingAction, _, err = Client.Actions.Get(copiedQualifiedName.GetEntityName(), FETCH_CODE); err != nil {
			return nil, actionGetError(copiedQualifiedName.GetEntityName(), FETCH_CODE, err)
		}

		Client.Namespace = qualifiedName.GetNamespace()
		action.Exec = existingAction.Exec
		action.Parameters = append(action.Parameters, existingAction.Parameters...)
		action.Annotations = append(action.Annotations, existingAction.Annotations...)
	} else if Flags.action.sequence {
		if len(args) == 2 {
			action.Exec = new(whisk.Exec)
			action.Exec.Kind = SEQUENCE
			action.Exec.Components = csvToQualifiedActions(args[1])
		} else {
			return nil, noArtifactError()
		}
	} else if len(args) > 1 || len(Flags.action.docker) > 0 {
		action.Exec, err = getExec(args, Flags.action)
		if err != nil {
			return nil, err
		}
	} else if !update {
		return nil, noArtifactError()
	}

	if update {
		action.DelAnnotations = Flags.action.delAnnotation
	}
	whisk.Debug(whisk.DbgInfo, "Parsed action struct: %#v\n", action)
	return action, err
}

func augmentAction(cmd *cobra.Command, args []string, action *whisk.Action, update bool) (*whisk.Action, error) {
	var err error
	var existingAction *whisk.Action = nil
	var augmentedAction *whisk.Action = new(whisk.Action)
	*augmentedAction = *action

	if update {
		if existingAction, _, err = Client.Actions.Get(action.Name, DO_NOT_FETCH_CODE); err != nil {
			whiskErr, isWhiskError := err.(*whisk.WskError)

			if (isWhiskError && whiskErr.ExitCode != whisk.EXIT_CODE_NOT_FOUND) || !isWhiskError {
				return nil, actionGetError(action.Name, DO_NOT_FETCH_CODE, err)
			}
		}
	}

	// Augment the action's annotations with the --web related annotations
	if augmentedAction, err = augmentWebArg(cmd, args, action, augmentedAction, existingAction); err != nil {
		return nil, err
	}

	// Augment the action's annotations with the --web-secure related annotations
	if augmentedAction, err = augmentWebSecureArg(cmd, args, action, augmentedAction, existingAction); err != nil {
		return nil, err
	}

	whisk.Debug(whisk.DbgInfo, "Augmented action struct: %#v\n", augmentedAction)
	return augmentedAction, err
}

func augmentWebArg(cmd *cobra.Command, args []string, action *whisk.Action, augmentedAction *whisk.Action, existingAction *whisk.Action) (*whisk.Action, error) {
	var err error
	preserveAnnotations := action.Annotations == nil

	if cmd.LocalFlags().Changed(WEB_FLAG) {
		augmentedAction.Annotations, err = webAction(Flags.action.web, action.Annotations, action.Name, preserveAnnotations, existingAction)
		if existingAction != nil && err == nil {
			// Always carry forward any existing --web-secure annotation value
			// Although it can be overwritten later if --web-secure is set
			webSecureAnnotations := getWebSecureAnnotations(existingAction)
			if len(webSecureAnnotations) > 0 {
				augmentedAction.Annotations = augmentedAction.Annotations.AppendKeyValueArr(webSecureAnnotations)
			}
		}
	}

	if err != nil {
		return nil, err
	}

	whisk.Debug(whisk.DbgInfo, "augmentWebArg: Augmented action struct: %#v\n", augmentedAction)
	return augmentedAction, nil
}

/*
 * Return a whisk.Action augmented with --web-secure annotation updates
 * originalAction:  a action constructed from command line argument
 * action:          an action constructed from command line args + possible other augmentation (i.e. --web annotations)
 * existingAction:  on an action update, this is the existing action
 */
func augmentWebSecureArg(cmd *cobra.Command, args []string, originalAction *whisk.Action, action *whisk.Action, existingAction *whisk.Action) (*whisk.Action, error) {
	preserveAnnotations := action.Annotations == nil
	var augmentedAction *whisk.Action = new(whisk.Action)
	*augmentedAction = *action
	disableWebAction := strings.ToLower(Flags.action.web) == "false" || strings.ToLower(Flags.action.web) == "no"
	isWebSecureFlagValidToUse := action.WebAction() || (existingAction != nil && existingAction.WebAction() && !disableWebAction)

	// Process the --web-secure flag when set
	if cmd.LocalFlags().Changed(WEB_SECURE_FLAG) {
		// The --web-secure option is only valid when:
		//   1. action --web is set to either true or raw (i.e. web-export annotation is true)
		//   -OR-
		//   2. existing action web-export annotation is true && action --web is not false/no
		whisk.Debug(whisk.DbgInfo, "disableWebAction: %v  isWebSecureFlagValidToUse: %v\n", disableWebAction, isWebSecureFlagValidToUse)
		if !isWebSecureFlagValidToUse {
			return nil, webSecureUsageError()
		}

		// Carry forward some or all of the existing action's annotations
		//   all  -> if original command line had at least one annotation specified
		//   some -> if original command line had NO annotations, carry forward web/websecure annotation values
		if existingAction != nil {
			if preserveAnnotations {
				augmentedAction.Annotations = action.Annotations.AppendKeyValueArr(existingAction.Annotations)
			} else {
				augmentedAction.Annotations = action.Annotations.AppendKeyValueArr(getWebActionAnnotations(existingAction))
				augmentedAction.Annotations = augmentedAction.Annotations.AppendKeyValueArr(getWebSecureAnnotations(existingAction))
			}
		}
		// when "--web-secure false", need to delete require-whisk-auth annotation
		secureSecret := webSecureSecret(Flags.action.websecure) // will be false when "--web-secure false"
		existingSecret := augmentedAction.Annotations.GetValue(WEB_SECURE_ANNOT)
		_, disableSecurity := secureSecret.(bool)
		if existingSecret != nil && disableSecurity {
			augmentedAction.DelAnnotations = []string{"require-whisk-auth"}
		}
		augmentedAction.Annotations = updateWebSecureAnnotation(Flags.action.websecure, augmentedAction.Annotations)
	}

	whisk.Debug(whisk.DbgInfo, "augmentWebSecureArg: Augmented action struct: %#v\n", augmentedAction)
	return augmentedAction, nil
}

func getExec(args []string, params ActionFlags) (*whisk.Exec, error) {
	var err error
	var code string
	var exec *whisk.Exec

	exec = new(whisk.Exec)
	kind := params.kind
	isNative := params.native
	docker := params.docker
	mainEntry := params.main
	ext := ""

	if len(args) == 2 {
		artifact := args[1]
		ext = filepath.Ext(artifact)
		code, err = ReadFile(artifact)

		if err != nil {
			whisk.Debug(whisk.DbgError, "ReadFile(%s) error: %s\n", artifact, err)
			return nil, err
		}

		if ext == ZIP_EXT || ext == JAVA_EXT || ext == BAL_BIN_EXT {
			code = base64.StdEncoding.EncodeToString([]byte(code))
		}

		exec.Code = &code
	} else if len(args) == 1 && len(docker) == 0 {
		return nil, noArtifactError()
	} else if len(args) > 1 {
		return nil, noArtifactError()
	}

	if len(kind) > 0 {
		exec.Kind = kind
	} else if len(docker) > 0 || isNative {
		exec.Kind = BLACKBOX
		if isNative {
			exec.Image = "openwhisk/dockerskeleton"
		} else {
			exec.Image = docker
		}
	} else if ext == SWIFT_EXT {
		exec.Kind = fmt.Sprintf("%s:%s", SWIFT, DEFAULT)
	} else if ext == NODE_JS_EXT {
		exec.Kind = fmt.Sprintf("%s:%s", NODE_JS, DEFAULT)
	} else if ext == PYTHON_EXT {
		exec.Kind = fmt.Sprintf("%s:%s", PYTHON, DEFAULT)
	} else if ext == JAVA_EXT {
		exec.Kind = fmt.Sprintf("%s:%s", JAVA, DEFAULT)
	} else if ext == PHP_EXT {
		exec.Kind = fmt.Sprintf("%s:%s", PHP, DEFAULT)
	} else if ext == RUBY_EXT {
		exec.Kind = fmt.Sprintf("%s:%s", RUBY, DEFAULT)
	} else if ext == RUST_EXT {
		exec.Kind = fmt.Sprintf("%s:%s", RUST, DEFAULT)
	} else if ext == GO_EXT {
		exec.Kind = fmt.Sprintf("%s:%s", GO, DEFAULT)
	} else if ext == BAL_BIN_EXT {
		exec.Kind = fmt.Sprintf("%s:%s", BAL, DEFAULT)
	} else {
		if ext == ZIP_EXT {
			return nil, zipKindError()
		} else {
			return nil, extensionError(ext)
		}
	}

	// Error if entry point is not specified for Java
	if len(mainEntry) != 0 {
		exec.Main = mainEntry
	} else {
		if exec.Kind == "java" {
			return nil, javaEntryError()
		}
	}

	return exec, nil
}

func getBinaryKindExtension(runtime string) (extension string) {
	switch strings.ToLower(runtime) {
	case JAVA:
		extension = JAVA_EXT
	case BAL:
		extension = BAL_BIN_EXT
	default:
		extension = ZIP_EXT
	}

	return extension
}

func getKindExtension(runtime string) (extension string) {
	switch strings.ToLower(runtime) {
	case NODE_JS:
		extension = NODE_JS_EXT
	case PYTHON:
		extension = PYTHON_EXT
	case SWIFT:
		fallthrough
	case PHP:
		extension = fmt.Sprintf(".%s", runtime)
	case RUBY:
		extension = RUBY_EXT
	case RUST:
		extension = RUST_EXT
	case GO:
		extension = GO_EXT
	}

	return extension
}

func saveCode(action whisk.Action, filename string) (err error) {
	var code string
	var runtime string
	var exec whisk.Exec

	exec = *action.Exec
	runtime = strings.Split(exec.Kind, ":")[0]

	if strings.ToLower(runtime) == BLACKBOX && exec.Code == nil && *exec.Binary == false {
		return cannotSaveImageError()
	} else if strings.ToLower(runtime) == SEQUENCE {
		return cannotSaveSequenceError()
	}

	if exec.Code != nil {
		code = *exec.Code
	}

	if *exec.Binary {
		decoded, _ := base64.StdEncoding.DecodeString(code)
		code = string(decoded)

		if len(filename) == 0 {
			filename = action.Name + getBinaryKindExtension(runtime)
		}
	} else {
		if len(filename) == 0 {
			filename = action.Name + getKindExtension(runtime)
		}
	}

	if exists, err := FileExists(filename); err != nil {
		return err
	} else if exists {
		return fileExistsError(filename)
	}

	if err := writeFile(filename, code); err != nil {
		return err
	}

	pwd, err := os.Getwd()
	if err != nil {
		whisk.Debug(whisk.DbgError, "os.Getwd() error: %s\n", err)
		return err
	}

	savedPath := fmt.Sprintf("%s%s%s", pwd, string(os.PathSeparator), filename)

	printSavedActionCodeSuccess(savedPath)

	return nil
}

func webAction(webMode string, annotations whisk.KeyValueArr, entityName string, preserveAnnotations bool, existingAction *whisk.Action) (whisk.KeyValueArr, error) {
	switch strings.ToLower(webMode) {
	case "yes":
		fallthrough
	case "true":
		return webActionAnnotations(preserveAnnotations, annotations, entityName, addWebAnnotations, existingAction)
	case "no":
		fallthrough
	case "false":
		return webActionAnnotations(preserveAnnotations, annotations, entityName, deleteWebAnnotations, existingAction)
	case "raw":
		return webActionAnnotations(preserveAnnotations, annotations, entityName, addRawAnnotations, existingAction)
	default:
		return nil, webInputError(webMode)
	}
}

type WebActionAnnotationMethod func(annotations whisk.KeyValueArr) whisk.KeyValueArr

func webActionAnnotations(
	preserveAnnotations bool,
	annotations whisk.KeyValueArr,
	entityName string,
	webActionAnnotationMethod WebActionAnnotationMethod,
	existingAction *whisk.Action) (whisk.KeyValueArr, error) {

	if preserveAnnotations && existingAction != nil {
		annotations = whisk.KeyValueArr.AppendKeyValueArr(annotations, existingAction.Annotations)
	}

	annotations = webActionAnnotationMethod(annotations)

	return annotations, nil
}

func addWebAnnotations(annotations whisk.KeyValueArr) whisk.KeyValueArr {
	annotations = deleteWebAnnotationKeys(annotations)
	annotations = addKeyValue(WEB_EXPORT_ANNOT, true, annotations)
	annotations = addKeyValue(RAW_HTTP_ANNOT, false, annotations)
	annotations = addKeyValue(FINAL_ANNOT, true, annotations)

	return annotations
}

func deleteWebAnnotations(annotations whisk.KeyValueArr) whisk.KeyValueArr {
	annotations = deleteWebAnnotationKeys(annotations)
	annotations = addKeyValue(WEB_EXPORT_ANNOT, false, annotations)
	annotations = addKeyValue(RAW_HTTP_ANNOT, false, annotations)
	annotations = addKeyValue(FINAL_ANNOT, false, annotations)

	return annotations
}

func addRawAnnotations(annotations whisk.KeyValueArr) whisk.KeyValueArr {
	annotations = deleteWebAnnotationKeys(annotations)
	annotations = addKeyValue(WEB_EXPORT_ANNOT, true, annotations)
	annotations = addKeyValue(RAW_HTTP_ANNOT, true, annotations)
	annotations = addKeyValue(FINAL_ANNOT, true, annotations)

	return annotations
}

func deleteWebAnnotationKeys(annotations whisk.KeyValueArr) whisk.KeyValueArr {
	annotations = deleteKey(WEB_EXPORT_ANNOT, annotations)
	annotations = deleteKey(RAW_HTTP_ANNOT, annotations)
	annotations = deleteKey(FINAL_ANNOT, annotations)

	return annotations
}

func getWebActionAnnotations(action *whisk.Action) whisk.KeyValueArr {
	var webKvArr = make(whisk.KeyValueArr, 3, 3)
	var j = 0

	var i = action.Annotations.FindKeyValue(WEB_EXPORT_ANNOT)
	if i > -1 {
		webKvArr[j] = action.Annotations[i]
		j++
	}
	i = action.Annotations.FindKeyValue(RAW_HTTP_ANNOT)
	if i > -1 {
		webKvArr[j] = action.Annotations[i]
		j++
	}
	i = action.Annotations.FindKeyValue(FINAL_ANNOT)
	if i > -1 {
		webKvArr[j] = action.Annotations[i]
		j++
	}
	return webKvArr[0:j]
}

func getWebSecureAnnotations(action *whisk.Action) whisk.KeyValueArr {
	var webKvArr = make(whisk.KeyValueArr, 1, 1)
	var j = 0

	var i = action.Annotations.FindKeyValue(WEB_SECURE_ANNOT)
	if i > -1 {
		webKvArr[j] = action.Annotations[i]
		j++
	}

	return webKvArr[0:j]
}

/*
 * Update the existing annotations with the web security annotation
 * If the current web security setting and existing setting are the "same", keep the existing value
 *   -> checking for the same "--web-secure true" setting means just checking if the two values are integers
 * If the current web security setting is "false", remove any existing setting
 */
func updateWebSecureAnnotation(websecure string, annotations whisk.KeyValueArr) whisk.KeyValueArr {
	secureSecret := webSecureSecret(websecure) // will be false when "--web-secure false"
	existingSecret := annotations.GetValue(WEB_SECURE_ANNOT)
	_, disableSecurity := secureSecret.(bool)
	_, newSecretIsInt := secureSecret.(int64)
	var existingSecretIsInt bool = false
	if existingSecret != nil {
		_, existingSecretIsInt = existingSecret.(json.Number)
	}

	if existingSecretIsInt && newSecretIsInt {
		whisk.Debug(whisk.DbgInfo, "Retaining existing secret number\n")
	} else if existingSecret != nil && disableSecurity {
		whisk.Debug(whisk.DbgInfo, "disabling web-secure; deleting annotation: %v\n", WEB_SECURE_ANNOT)
		annotations = deleteKey(WEB_SECURE_ANNOT, annotations)
	} else {
		whisk.Debug(whisk.DbgInfo, "Setting %v annotation; prior secret %v new secret %v\n",
			WEB_SECURE_ANNOT, reflect.TypeOf(existingSecret), reflect.TypeOf(secureSecret))
		annotations = annotations.AddOrReplace(&whisk.KeyValue{Key: WEB_SECURE_ANNOT, Value: secureSecret})
	}

	return annotations
}

//
// Generate a secret according to the --web-secure setting
//  true:   return a random int64
//  false:  return false, meaning no secret was returned
//  string: return the same string
func webSecureSecret(webSecureMode string) interface{} {
	switch strings.ToLower(webSecureMode) {
	case "true":
		return genWebActionSecureKey()
	case "false":
		return false
	default:
		return webSecureMode
	}
}

func getLimits(memorySet bool, logSizeSet bool, timeoutSet bool, concurrencySet bool, memory int, logSize int, timeout int, concurrency int) *whisk.Limits {
	var limits *whisk.Limits

	if memorySet || logSizeSet || timeoutSet || concurrencySet {
		limits = new(whisk.Limits)

		if memorySet {
			limits.Memory = &memory
		}

		if logSizeSet {
			limits.Logsize = &logSize
		}

		if timeoutSet {
			limits.Timeout = &timeout
		}

		if concurrencySet {
			limits.Concurrency = &concurrency
		}
	}

	return limits
}

func nestedError(errorMessage string, err error) error {
	return whisk.MakeWskErrorFromWskError(
		errors.New(errorMessage),
		err,
		whisk.EXIT_CODE_ERR_GENERAL,
		whisk.DISPLAY_MSG,
		whisk.DISPLAY_USAGE)
}

func nonNestedError(errorMessage string) error {
	return whisk.MakeWskError(
		errors.New(errorMessage),
		whisk.EXIT_CODE_ERR_USAGE,
		whisk.DISPLAY_MSG,
		whisk.DISPLAY_USAGE)
}

func actionParseError(cmd *cobra.Command, args []string, err error) error {
	whisk.Debug(whisk.DbgError, "parseAction(%s, %s) error: %s\n", cmd.Name(), args, err)

	errMsg := wski18n.T(
		"Invalid argument(s). {{.required}}",
		map[string]interface{}{
			"required": err,
		})

	return nestedError(errMsg, err)
}

func actionInsertError(action *whisk.Action, err error) error {
	whisk.Debug(whisk.DbgError, "Client.Actions.Insert(%#v, false) error: %s\n", action, err)

	errMsg := wski18n.T(
		"Unable to create action '{{.name}}': {{.err}}",
		map[string]interface{}{
			"name": action.Name,
			"err":  err,
		})

	return nestedError(errMsg, err)
}

func getJSONFromStringsParamError(params []string, keyValueFormat bool, err error) error {
	whisk.Debug(whisk.DbgError, "getJSONFromStrings(%#v, %t) failed: %s\n", params, keyValueFormat, err)

	errMsg := wski18n.T(
		"Invalid parameter argument '{{.param}}': {{.err}}",
		map[string]interface{}{
			"param": fmt.Sprintf("%#v", params),
			"err":   err,
		})

	return nestedError(errMsg, err)
}

func getJSONFromStringsAnnotError(annots []string, keyValueFormat bool, err error) error {
	whisk.Debug(whisk.DbgError, "getJSONFromStrings(%#v, %t) failed: %s\n", annots, keyValueFormat, err)

	errMsg := wski18n.T(
		"Invalid annotation argument '{{.annotation}}': {{.err}}",
		map[string]interface{}{
			"annotation": fmt.Sprintf("%#v", annots),
			"err":        err,
		})

	return nestedError(errMsg, err)
}

func invalidFieldFilterError(field string) error {
	errMsg := wski18n.T(
		"Invalid field filter '{{.arg}}'.",
		map[string]interface{}{
			"arg": field,
		})

	return nonNestedError(errMsg)
}

func actionDeleteError(entityName string, err error) error {
	whisk.Debug(whisk.DbgError, "Client.Actions.Delete(%s) error: %s\n", entityName, err)

	errMsg := wski18n.T(
		"Unable to delete action '{{.name}}': {{.err}}",
		map[string]interface{}{
			"name": entityName,
			"err":  err,
		})

	return nestedError(errMsg, err)
}

func actionGetError(entityName string, fetchCode bool, err error) error {
	whisk.Debug(whisk.DbgError, "Client.Actions.Get(%s, %t) error: %s\n", entityName, fetchCode, err)

	errMsg := wski18n.T(
		"Unable to get action '{{.name}}': {{.err}}",
		map[string]interface{}{
			"name": entityName,
			"err":  err,
		})

	return nestedError(errMsg, err)
}

func handleInvocationError(err error, entityName string) error {
	whisk.Debug(
		whisk.DbgError,
		"Client.Actions.Invoke(%s) error: %s\n",
		entityName,
		err)

	errMsg := wski18n.T(
		"Unable to invoke action '{{.name}}': {{.err}}",
		map[string]interface{}{
			"name": entityName,
			"err":  err,
		})

	return nestedError(errMsg, err)
}

func actionListError(entityName string, options *whisk.ActionListOptions, err error) error {
	whisk.Debug(whisk.DbgError, "Client.Actions.List(%s, %#v) error: %s\n", entityName, options, err)

	errMsg := wski18n.T(
		"Unable to obtain the list of actions for namespace '{{.name}}': {{.err}}",
		map[string]interface{}{
			"name": getClientNamespace(),
			"err":  err,
		})

	return nestedError(errMsg, err)
}

func webInputError(arg string) error {
	errMsg := wski18n.T(
		"Invalid argument '{{.arg}}' for --web flag. Valid input consist of 'yes', 'true', 'raw', 'false', or 'no'.",
		map[string]interface{}{
			"arg": arg,
		})

	return nonNestedError(errMsg)
}

func webSecureUsageError() error {
	errMsg := wski18n.T("The --web-secure option is only valid when the --web option is enabled.")
	return nonNestedError(errMsg)
}

func zipKindError() error {
	errMsg := wski18n.T("creating an action from a .zip artifact requires specifying the action kind explicitly")

	return nonNestedError(errMsg)
}

func noArtifactError() error {
	errMsg := wski18n.T("An action name and code artifact are required.")

	return nonNestedError(errMsg)
}

func extensionError(extension string) error {
	errMsg := wski18n.T(
		"'{{.name}}' is not a supported action runtime",
		map[string]interface{}{
			"name": extension,
		})

	return nonNestedError(errMsg)
}

func javaEntryError() error {
	errMsg := wski18n.T("Java actions require --main to specify the fully-qualified name of the main class")

	return nonNestedError(errMsg)
}

func cannotSaveImageError() error {
	return nonNestedError(wski18n.T("Cannot save Docker images"))
}

func cannotSaveSequenceError() error {
	return nonNestedError(wski18n.T("Cannot save action sequences"))
}

func fileExistsError(file string) error {
	errMsg := wski18n.T("The file '{{.file}}' already exists", map[string]interface{}{
		"file": file,
	})

	return nonNestedError(errMsg)
}

func printActionCreated(entityName string) {
	fmt.Fprintf(
		color.Output,
		wski18n.T(
			"{{.ok}} created action {{.name}}\n",
			map[string]interface{}{
				"ok":   color.GreenString("ok:"),
				"name": boldString(entityName),
			}))
}

func printActionUpdated(entityName string) {
	fmt.Fprintf(
		color.Output,
		wski18n.T(
			"{{.ok}} updated action {{.name}}\n",
			map[string]interface{}{
				"ok":   color.GreenString("ok:"),
				"name": boldString(entityName),
			}))
}

func printBlockingTimeoutMsg(namespace string, entityName string, activationID interface{}) {
	fmt.Fprintf(
		colorable.NewColorableStderr(),
		wski18n.T(
			"{{.ok}} invoked /{{.namespace}}/{{.name}}, but the request has not yet finished, with id {{.id}}\n",
			map[string]interface{}{
				"ok":        color.GreenString("ok:"),
				"namespace": boldString(namespace),
				"name":      boldString(entityName),
				"id":        boldString(activationID),
			}))
}

func printInvocationMsg(
	qualifiedName QualifiedName,
	blocking bool,
	header bool,
	response interface{},
	outputStream io.Writer) {
	if header {
		fmt.Fprintf(
			outputStream,
			wski18n.T(
				"{{.ok}} invoked /{{.namespace}}/{{.name}} with id {{.id}}\n",
				map[string]interface{}{
					"ok":        color.GreenString("ok:"),
					"namespace": boldString(qualifiedName.GetNamespace()),
					"name":      boldString(qualifiedName.GetEntityName()),
					"id":        boldString(getValueFromResponse(ACTIVATION_ID, response)),
				}))
	}

	if blocking {
		printJSON(response, outputStream)
	}
}

func printActionGetWithField(entityName string, field string, action *whisk.Action) {
	fmt.Fprintf(
		color.Output,
		wski18n.T(
			"{{.ok}} got action {{.name}}, displaying field {{.field}}\n",
			map[string]interface{}{
				"ok":    color.GreenString("ok:"),
				"name":  boldString(entityName),
				"field": boldString(field),
			}))

	printField(action, field)
}

func printActionGetWithURL(entityName string, actionURL string) {
	fmt.Fprintf(
		color.Output,
		wski18n.T("{{.ok}} got action {{.name}}\n",
			map[string]interface{}{
				"ok":   color.GreenString("ok:"),
				"name": boldString(entityName),
			}))
	fmt.Println(actionURL)
}

func printActionGet(entityName string, action *whisk.Action) {
	fmt.Fprintf(
		color.Output,
		wski18n.T("{{.ok}} got action {{.name}}\n",
			map[string]interface{}{
				"ok":   color.GreenString("ok:"),
				"name": boldString(entityName),
			}))

	printJSON(action)
}

func printActionDeleted(entityName string) {
	fmt.Fprintf(
		color.Output,
		wski18n.T(
			"{{.ok}} deleted action {{.name}}\n",
			map[string]interface{}{
				"ok":   color.GreenString("ok:"),
				"name": boldString(entityName),
			}))
}

func printSavedActionCodeSuccess(name string) {
	fmt.Fprintf(
		color.Output,
		wski18n.T(
			"{{.ok}} saved action code to {{.name}}\n",
			map[string]interface{}{
				"ok":   color.GreenString("ok:"),
				"name": boldString(name),
			}))
}

// Generate a random number to be used as a web action's require-whisk-auth secret
func genWebActionSecureKey() int64 {
	r := rand.New(rand.NewSource(time.Now().Unix()))

	// Truncate integer for API GW interoperability
	return r.Int63() & MAX_JS_INT
}

// Check if the specified action is a web-action
func isWebAction(client *whisk.Client, qname QualifiedName) (*whisk.Action, error) {
	var err error = nil

	savedNs := client.Namespace
	client.Namespace = qname.GetNamespace()
	fullActionName := "/" + qname.GetNamespace() + "/" + qname.GetEntityName()

	action, _, err := client.Actions.Get(qname.GetEntityName(), DO_NOT_FETCH_CODE)

	if err != nil {
		whisk.Debug(whisk.DbgError, "client.Actions.Get(%s, %t) error: %s\n", fullActionName, DO_NOT_FETCH_CODE, err)
		whisk.Debug(whisk.DbgError, "Unable to obtain action '%s' for web action validation\n", fullActionName)
		errMsg := wski18n.T("Unable to get action '{{.name}}': {{.err}}",
			map[string]interface{}{"name": fullActionName, "err": err})
		err = whisk.MakeWskErrorFromWskError(errors.New(errMsg), err, whisk.EXIT_CODE_ERR_NETWORK, whisk.DISPLAY_MSG,
			whisk.NO_DISPLAY_USAGE)
	} else {
		err = errors.New(wski18n.T("Action '{{.name}}' is not a web action. Issue 'wsk action update \"{{.name}}\" --web true' to convert the action to a web action.",
			map[string]interface{}{"name": fullActionName}))

		if action.WebAction() {
			err = nil
		}
	}

	client.Namespace = savedNs

	return action, err
}

func init() {
	actionCreateCmd.Flags().BoolVar(&Flags.action.native, "native", false, wski18n.T("treat ACTION as native action (zip file provides a compatible executable to run)"))
	actionCreateCmd.Flags().StringVar(&Flags.action.docker, "docker", "", wski18n.T("use provided docker image (a path on DockerHub) to run the action"))
	actionCreateCmd.Flags().BoolVar(&Flags.action.copy, "copy", false, wski18n.T("treat ACTION as the name of an existing action"))
	actionCreateCmd.Flags().BoolVar(&Flags.action.sequence, "sequence", false, wski18n.T("treat ACTION as comma separated sequence of actions to invoke"))
	actionCreateCmd.Flags().StringVar(&Flags.action.kind, "kind", "", wski18n.T("the `KIND` of the action runtime (example: swift:default, nodejs:default)"))
	actionCreateCmd.Flags().StringVar(&Flags.action.main, "main", "", wski18n.T("the name of the action entry point (function or fully-qualified method name when applicable)"))
	actionCreateCmd.Flags().IntVarP(&Flags.action.timeout, TIMEOUT_FLAG, "t", TIMEOUT_LIMIT, wski18n.T("the timeout `LIMIT` in milliseconds after which the action is terminated"))
	actionCreateCmd.Flags().IntVarP(&Flags.action.memory, MEMORY_FLAG, "m", MEMORY_LIMIT, wski18n.T("the maximum memory `LIMIT` in MB for the action"))
	actionCreateCmd.Flags().IntVarP(&Flags.action.logsize, LOG_SIZE_FLAG, "l", LOGSIZE_LIMIT, wski18n.T("the maximum log size `LIMIT` in MB for the action"))
	actionCreateCmd.Flags().IntVarP(&Flags.action.concurrency, CONCURRENCY_FLAG, "c", CONCURRENCY_LIMIT, wski18n.T("the maximum intra-container concurrent activation `LIMIT` for the action"))
	actionCreateCmd.Flags().StringSliceVarP(&Flags.common.annotation, "annotation", "a", nil, wski18n.T("annotation values in `KEY VALUE` format"))
	actionCreateCmd.Flags().StringVarP(&Flags.common.annotFile, "annotation-file", "A", "", wski18n.T("`FILE` containing annotation values in JSON format"))
	actionCreateCmd.Flags().StringSliceVarP(&Flags.common.param, "param", "p", nil, wski18n.T("parameter values in `KEY VALUE` format"))
	actionCreateCmd.Flags().StringVarP(&Flags.common.paramFile, "param-file", "P", "", wski18n.T("`FILE` containing parameter values in JSON format"))
	actionCreateCmd.Flags().StringVar(&Flags.action.web, WEB_FLAG, "", wski18n.T("treat ACTION as a web action, a raw HTTP web action, or as a standard action; yes | true = web action, raw = raw HTTP web action, no | false = standard action"))
	actionCreateCmd.Flags().StringVar(&Flags.action.websecure, WEB_SECURE_FLAG, "", wski18n.T("secure the web action. where `SECRET` is true, false, or any string. Only valid when the ACTION is a web action"))

	actionUpdateCmd.Flags().BoolVar(&Flags.action.native, "native", false, wski18n.T("treat ACTION as native action (zip file provides a compatible executable to run)"))
	actionUpdateCmd.Flags().StringVar(&Flags.action.docker, "docker", "", wski18n.T("use provided docker image (a path on DockerHub) to run the action"))
	actionUpdateCmd.Flags().BoolVar(&Flags.action.copy, "copy", false, wski18n.T("treat ACTION as the name of an existing action"))
	actionUpdateCmd.Flags().BoolVar(&Flags.action.sequence, "sequence", false, wski18n.T("treat ACTION as comma separated sequence of actions to invoke"))
	actionUpdateCmd.Flags().StringVar(&Flags.action.kind, "kind", "", wski18n.T("the `KIND` of the action runtime (example: swift:default, nodejs:default)"))
	actionUpdateCmd.Flags().StringVar(&Flags.action.main, "main", "", wski18n.T("the name of the action entry point (function or fully-qualified method name when applicable)"))
	actionUpdateCmd.Flags().IntVarP(&Flags.action.timeout, TIMEOUT_FLAG, "t", TIMEOUT_LIMIT, wski18n.T("the timeout `LIMIT` in milliseconds after which the action is terminated"))
	actionUpdateCmd.Flags().IntVarP(&Flags.action.memory, MEMORY_FLAG, "m", MEMORY_LIMIT, wski18n.T("the maximum memory `LIMIT` in MB for the action"))
	actionUpdateCmd.Flags().IntVarP(&Flags.action.logsize, LOG_SIZE_FLAG, "l", LOGSIZE_LIMIT, wski18n.T("the maximum log size `LIMIT` in MB for the action"))
	actionUpdateCmd.Flags().IntVarP(&Flags.action.concurrency, CONCURRENCY_FLAG, "c", CONCURRENCY_LIMIT, wski18n.T("the maximum intra-container concurrent activation `LIMIT` for the action"))
	actionUpdateCmd.Flags().StringSliceVarP(&Flags.common.annotation, "annotation", "a", []string{}, wski18n.T("annotation values in `KEY VALUE` format"))
	actionUpdateCmd.Flags().StringVarP(&Flags.common.annotFile, "annotation-file", "A", "", wski18n.T("`FILE` containing annotation values in JSON format"))
	actionUpdateCmd.Flags().StringSliceVarP(&Flags.common.param, "param", "p", []string{}, wski18n.T("parameter values in `KEY VALUE` format"))
	actionUpdateCmd.Flags().StringVarP(&Flags.common.paramFile, "param-file", "P", "", wski18n.T("`FILE` containing parameter values in JSON format"))
	actionUpdateCmd.Flags().StringVar(&Flags.action.web, WEB_FLAG, "", wski18n.T("treat ACTION as a web action, a raw HTTP web action, or as a standard action; yes | true = web action, raw = raw HTTP web action, no | false = standard action"))
	actionUpdateCmd.Flags().StringVar(&Flags.action.websecure, WEB_SECURE_FLAG, "", wski18n.T("secure the web action. where `SECRET` is true, false, or any string. Only valid when the ACTION is a web action"))
	actionUpdateCmd.Flags().StringArrayVar(&Flags.action.delAnnotation, "del-annotation", []string{}, wski18n.T("the list of annotations to be deleted from the action, e.g. --del-annotation key1 --del-annotation key2"))

	actionInvokeCmd.Flags().StringSliceVarP(&Flags.common.param, "param", "p", []string{}, wski18n.T("parameter values in `KEY VALUE` format"))
	actionInvokeCmd.Flags().StringVarP(&Flags.common.paramFile, "param-file", "P", "", wski18n.T("`FILE` containing parameter values in JSON format"))
	actionInvokeCmd.Flags().BoolVarP(&Flags.common.blocking, "blocking", "b", false, wski18n.T("blocking invoke"))
	actionInvokeCmd.Flags().BoolVarP(&Flags.action.result, "result", "r", false, wski18n.T("blocking invoke; show only activation result (unless there is a failure)"))

	actionGetCmd.Flags().BoolVarP(&Flags.common.summary, "summary", "s", false, wski18n.T("summarize action details; parameters with prefix \"*\" are bound, \"**\" are bound and finalized"))
	actionGetCmd.Flags().BoolVarP(&Flags.action.url, "url", "r", false, wski18n.T("get action url"))
	actionGetCmd.Flags().StringVar(&Flags.action.saveAs, SAVE_AS_FLAG, "", wski18n.T("file to save action code to"))
	actionGetCmd.Flags().BoolVarP(&Flags.action.save, SAVE_FLAG, "", false, wski18n.T("save action code to file corresponding with action name"))

	actionListCmd.Flags().IntVarP(&Flags.common.skip, "skip", "s", 0, wski18n.T("exclude the first `SKIP` number of actions from the result"))
	actionListCmd.Flags().IntVarP(&Flags.common.limit, "limit", "l", 30, wski18n.T("only return `LIMIT` number of actions from the collection"))
	actionListCmd.Flags().BoolVarP(&Flags.common.nameSort, "name-sort", "n", false, wski18n.T("sorts a list alphabetically by entity name; only applicable within the limit/skip returned entity block"))

	actionCmd.AddCommand(
		actionCreateCmd,
		actionUpdateCmd,
		actionInvokeCmd,
		actionGetCmd,
		actionDeleteCmd,
		actionListCmd,
	)
}
