import sys
import os
import difflib
import warnings
import traceback
import pkg_resources
import datetime
import re
import subprocess
import locale
import codecs
from functools import wraps

import click

from cloudify._compat import StringIO, urlquote
from cloudify_rest_client.constants import VisibilityState
from cloudify_rest_client.exceptions import NotModifiedError
from cloudify_rest_client.exceptions import CloudifyClientError
from cloudify_rest_client.exceptions import MaintenanceModeActiveError
from cloudify_rest_client.exceptions import MaintenanceModeActivatingError

from .. import env, logger
from ..cli import helptexts
from ..inputs import inputs_to_dict
from ..utils import generate_random_string
from ..constants import DEFAULT_BLUEPRINT_PATH
from ..exceptions import SuppressedCloudifyCliError
from ..exceptions import CloudifyBootstrapError, CloudifyValidationError
from ..logger import (
    get_logger,
    set_global_verbosity_level,
    DEFAULT_LOG_FILE,
    set_global_json_output)


CLICK_CONTEXT_SETTINGS = dict(
    help_option_names=['-h', '--help'],
    token_normalize_func=lambda param: param.lower())

AGENT_FILTER_NODE_IDS = 'node_ids'
AGENT_FILTER_NODE_INSTANCE_IDS = 'node_instance_ids'
AGENT_FILTER_DEPLOYMENT_ID = 'deployment_id'
AGENT_FILTER_INSTALL_METHODS = 'install_methods'


class MutuallyExclusiveOption(click.Option):
    """Makes options mutually exclusive. The option must pass a `cls` argument
    with this class name and a `mutually_exclusive` argument with a list of
    argument names it is mutually exclusive with.

    NOTE: All mutually exclusive options must use this. It's not enough to
    use it in just one of the options.
    """

    def __init__(self, *args, **kwargs):
        self.mutually_exclusive = set(kwargs.pop('mutually_exclusive', []))
        self.mutuality_string = ', '.join(self.mutually_exclusive)
        if self.mutually_exclusive:
            help = kwargs.get('help', '')
            kwargs['help'] = (
                '{0}. You cannot use this argument with arguments: [{1}]'
                .format(help, self.mutuality_string)
            )
        super(MutuallyExclusiveOption, self).__init__(*args, **kwargs)

    def handle_parse_result(self, ctx, opts, args):
        if self.mutually_exclusive.intersection(opts) and self.name in opts:
            raise click.UsageError(
                'Illegal usage: `{0}` is mutually exclusive with '
                'arguments: [{1}]'.format(self.name, self.mutuality_string)
            )
        return super(MutuallyExclusiveOption, self).handle_parse_result(
            ctx, opts, args)


def _parse_relative_datetime(ctx, param, rel_datetime):
    """Change relative time (ago) to a valid timestamp"""
    if not rel_datetime:
        return None
    parsed = re.findall(r"(\d+) (seconds?|minutes?|hours?|days?|weeks?"
                        "|months?|years?) ?(ago)?",
                        rel_datetime)
    if not parsed or len(parsed[0]) < 2:
        return None
    number = int(parsed[0][0])
    period = parsed[0][1]
    if period[-1] != u's':
        period += u's'
    now = datetime.datetime.utcnow()
    if period == u'years':
        result = now.replace(year=now.year - number)
    elif period == u'months':
        if now.month > number:
            result = now.replace(month=now.month - number)
        else:
            result = now.replace(month=now.month - number + 12,
                                 year=now.year - 1)
    else:
        delta = datetime.timedelta(**{period: number})
        result = now - delta
    return result


def _parse_unix_timestamp(unix_time):
    parsed = re.findall(r"^(\d+)(\.(\d{1,6}))?$", unix_time)
    if not parsed or len(parsed[0]) < 1:
        return None
    return datetime.datetime.utcfromtimestamp(float(unix_time))


class Timestamp(click.DateTime):
    """Timestamp is DateTime enhanced by the ability to parse Unix time"""

    def convert(self, value, param, ctx):
        parsed_unix_time = _parse_unix_timestamp(value)
        if parsed_unix_time:
            return parsed_unix_time
        return super(Timestamp, self).convert(value, param, ctx)

    def get_metavar(self, param):
        return '[{0}|UNIX TIME FORMAT]'.format('|'.join(self.formats))

    def __repr__(self):
        return "Timestamp"


def _format_version_data(version_data,
                         prefix=None,
                         suffix=None,
                         infix=None):
    all_data = version_data.copy()
    all_data['prefix'] = prefix or ''
    all_data['suffix'] = suffix or ''
    all_data['infix'] = infix or ''
    output = StringIO()
    output.write('{prefix}{version}'.format(**all_data))
    output.write('{suffix}'.format(**all_data))
    return output.getvalue()


def _tenant_help_message(message, message_template, resource_name):
    if message is not None:
        return message
    if resource_name is not None:
        return message_template.format(resource_name)
    return helptexts.TENANT


def _get_validate_callback(validate):
    if validate:
        return validate_name
    return None


def show_version(ctx, param, value):
    if not value or ctx.resilient_parsing:
        return

    cli_version_data = {
        'version': pkg_resources.require('cloudify')[0].version
    }
    rest_version_data = env.get_manager_version_data() \
        if env.is_manager_active() else None
    output = ''
    if rest_version_data:
        edition = rest_version_data['edition'].title()
        output += '{0} edition\n\n'.format(edition)

    output += _format_version_data(
        cli_version_data,
        prefix='Cloudify CLI ',
        infix=' ' * 5,
        suffix='\n')
    if rest_version_data:
        output += _format_version_data(
            rest_version_data,
            prefix='Cloudify Manager ',
            infix=' ',
            suffix=' [ip={ip}]\n'.format(**rest_version_data))

    get_logger().info(output)
    ctx.exit()


def inputs_callback(ctx, param, value):
    """Allow to pass any inputs we provide to a command as
    processed inputs instead of having to call `inputs_to_dict`
    inside the command.

    `@cfy.options.inputs` already calls this callback so that
    every time you use the option it returns the inputs as a
    dictionary.
    """
    if not value or ctx.resilient_parsing:
        return {}

    return inputs_to_dict(value)


def properties_callback(ctx, param, value):
    """Same as inputs_callback above,
    But also allows the user to pass inputs of the format key=value where
    key has a dot hierarchy - e.g. 'a.b.c=d', and parses such inputs
    into correct dict format: {a: {b: {c: d}}}.
    """
    if not value or ctx.resilient_parsing:
        return {}
    deleting = ctx.info_name == 'delete-runtime'
    return inputs_to_dict(value, dot_hierarchy=True, deleting=deleting)


def parse_on_off(ctx, param, value):
    if value is None or ctx.resilient_parsing:
        return
    if value.lower() == 'off':
        return False
    elif value.lower() == 'on':
        return True
    else:
        raise CloudifyValidationError(
            'Value must be on/off, but got: {0}'.format(value))


def parse_and_validate_labels(ctx, param, value):
    if value is None or ctx.resilient_parsing:
        return

    if not value:
        raise CloudifyValidationError(
            'ERROR: The `{0}` argument is empty'.format(param.name))

    labels_list = []
    raw_labels_list = value.split(',')
    for label in raw_labels_list:
        if label.count(':') != 1:
            raise CloudifyValidationError('ERROR: Labels should be of the '
                                          'form <key>:<value>,<key>:<value>')

        label_key, label_value = label.split(':')
        validate_param_value('The key of one or more labels', label_key)
        validate_param_value('The value of one or more labels', label_value)
        labels_list.append({label_key: label_value})

    return labels_list


def parse_and_validate_label_to_delete(ctx, param, value):
    if value is None or ctx.resilient_parsing:
        return

    if not value:
        raise CloudifyValidationError(
            'ERROR: The `{0}` argument is empty'.format(param.name))

    if ',' in value or value.count(':') > 1:
        raise CloudifyValidationError(
            'LABEL can be either <key>:<value> or <key>')

    label = value.split(':')
    if len(label) == 1:
        validate_param_value('The provided key', label[0])
        return label[0]
    else:
        label_key, label_value = label
        validate_param_value('The key of the provided label', label_key)
        validate_param_value('The value of the provided label', label_value)
        return {label_key: label_value}


def parse_and_validate_filter_rules(ctx, param, value):
    if value is None or ctx.resilient_parsing:
        return

    if not value:
        raise CloudifyValidationError(
            'ERROR: The `{0}` argument is empty'.format(param.name))

    if any(pattern in value for pattern in
           ['and', '!=', '=', 'is null', 'is not null']):
        filter_rules_list = parse_filter_rules_list(value)
        return {'_filter_rules': filter_rules_list}

    else:
        validate_param_value('The filter ID', value)
        return {'_filter_id': value}


def parse_and_validate_filter_rules_list(ctx, param, value):
    if value is None or ctx.resilient_parsing:
        return

    if not value:
        raise CloudifyValidationError(
            'ERROR: The `{0}` argument is empty'.format(param.name))

    return parse_filter_rules_list(value)


def parse_filter_rules_list(raw_filter_rules):
    filter_rules = []
    for filter_rule in raw_filter_rules.split('and'):
        if all(pattern not in filter_rule for pattern in
               ['=', 'is null', 'is not null']):
            raise CloudifyValidationError(
                'ERROR: Filter rules must be one of: <key>=<value>, '
                '<key>=[<value1>,<value2>,...], <key>!=<value>, '
                '<key>!=[<value1>,<value2>,...], <key> is null, '
                '<key> is not null')
        filter_rules.append(filter_rule.strip().lower())
        # The rest of the validation is done in the rest-service

    return filter_rules


def validate_name(ctx, param, value):
    if value is None or ctx.resilient_parsing:
        return

    return validate_param_value('The `{0}` argument'.format(param.name), value)


def validate_param_value(err_prefix, value):
    if not value:
        raise CloudifyValidationError('ERROR: {0} is empty'.format(err_prefix))

    quoted_value = urlquote(value, safe='')
    if value != quoted_value:
        raise CloudifyValidationError(
            'ERROR: {0} contains illegal characters. Only letters, digits and '
            'the characters "-", "." and "_" are allowed'.format(err_prefix))

    return value


def validate_password(ctx, param, value):
    if value is None or ctx.resilient_parsing:
        return

    if not value:
        raise CloudifyValidationError('ERROR: The password is empty')

    return value


def validate_encryption_passphrase(ctx, param, value):
    value = validate_password(ctx, param, value)
    if value and len(value) < 8:
        raise CloudifyValidationError('ERROR: Passphrase must contain at '
                                      'least 8 characters.')
    return value


def validate_nonnegative_integer(ctx, param, value):
    if ctx.resilient_parsing:
        return

    try:
        value = int(value)
        if value < 0:
            raise ValueError()
    except ValueError:
        raise CloudifyValidationError('ERROR: {0} is expected to be a '
                                      'nonnegative integer'.format(param.name))
    return value


def set_json(ctx, param, value):
    if value is not None:
        set_global_json_output(value)
    return value


def set_format(ctx, param, value):
    if value == 'json':
        set_global_json_output(True)
    return value


def set_manager(ctx, param, value):
    if value is None:
        return
    if env.is_cluster():
        env.set_target_manager(value)
    else:
        get_logger().warning(
            '--manager can only be used in a cluster topology and the '
            'current profile is an all-in-one Cloudify Manager'
        )


def json_output_deprecate(ctx, param, value):
    if value:
        warnings.warn("Instead of --json-output, use the global "
                      "`cfy --json` flag")
    return value


def set_verbosity_level(ctx, param, value):
    if not value or ctx.resilient_parsing:
        return
    if param.name == 'verbose':
        set_global_verbosity_level(value)
    elif value and param.name == 'quiet':
        set_global_verbosity_level(logger.QUIET)
    return value


def set_cli_except_hook(global_verbosity_level):

    def recommend(possible_solutions):
        logger = get_logger()

        logger.info('Possible solutions:')
        for solution in possible_solutions:
            logger.info('  - {0}'.format(solution))

    def new_excepthook(tpe, value, tb):
        with open(DEFAULT_LOG_FILE, 'a') as log_file:
            traceback.print_exception(
                etype=tpe,
                value=value,
                tb=tb,
                file=log_file)

        logger = get_logger()

        prefix = None
        server_traceback = None
        output_message = True

        if issubclass(tpe, CloudifyClientError):
            server_traceback = value.server_traceback
            if not issubclass(
                    tpe,
                    (MaintenanceModeActiveError,
                     MaintenanceModeActivatingError,
                     NotModifiedError)):
                # this means we made a server call and it failed.
                # we should include this information in the error
                prefix = 'An error occurred on the server'
        if issubclass(tpe, SuppressedCloudifyCliError):
            output_message = False
        if issubclass(tpe, CloudifyBootstrapError):
            output_message = False
        if global_verbosity_level:
            # print traceback if verbose
            s_traceback = StringIO()
            traceback.print_exception(
                etype=tpe,
                value=value,
                tb=tb,
                file=s_traceback)
            logger.error(s_traceback.getvalue())
            if server_traceback:
                logger.error('Server Traceback (most recent call last):')

                # No need for print_tb since this exception
                # is already formatted by the server
                logger.error(server_traceback)
        if output_message and not global_verbosity_level:

            # If we output the traceback
            # we output the message too.
            # print_exception does that.
            # here we just want the message (non verbose)
            if prefix:
                logger.error('{0}: {1}'.format(prefix, value))
            else:
                logger.error(value)
        if hasattr(value, 'possible_solutions'):
            recommend(getattr(value, 'possible_solutions'))

    sys.excepthook = new_excepthook


def assert_manager_active(require_creds=True):
    """
    Wrap the command so that it can only run when a manager is active

    :param require_creds: If set to True, the wrapped method will fail if no
    admin password was set either in the profile, or in the env variable
    """
    def decorator(func):
        # Wraps here makes sure the original docstring propagates to click
        @wraps(func)
        def wrapper(*args, **kwargs):
            env.assert_manager_active()
            if require_creds:
                env.assert_credentials_set()
            return func(*args, **kwargs)

        return wrapper
    return decorator


def assert_local_active(func):
    """
    Wrap the command so that it can only run when in local context
    """
    @wraps(func)
    def wrapper(*args, **kwargs):
        env.assert_local_active()
        return func(*args, **kwargs)

    return wrapper


def pass_logger(func):
    """Simply passes the logger to a command.
    """
    # Wraps here makes sure the original docstring propagates to click
    @wraps(func)
    def wrapper(*args, **kwargs):
        new_logger = get_logger()
        return func(logger=new_logger, *args, **kwargs)

    return wrapper


def pass_client(use_tenant_in_header=True, *args, **kwargs):
    """Simply passes the rest client to a command.
    """
    def add_client_inner(func):
        # Wraps here makes sure the original docstring propagates to click
        @wraps(func)
        def wrapper(*wrapper_args, **wrapper_kwargs):
            tenant = wrapper_kwargs.get('tenant_name') \
                if use_tenant_in_header else None
            client = env.get_rest_client(tenant_name=tenant, *args, **kwargs)
            return func(client=client, *wrapper_args, **wrapper_kwargs)

        return wrapper

    return add_client_inner


def pass_context(func):
    """Make click context Cloudify specific

    This exists purely for aesthetic reasons, otherwise
    Some decorators are called `@click.something` instead of
    `@cfy.something`
    """
    return click.pass_context(func)


class CommandMixin(object):
    """
    This class mixin helps to set the right locale for system required
    by python 3 for click library where "LC_ALL" & "LANG" are not set and
    in order to avoid the RuntimeError raised by click library which
    prevents invoking cfy commands
    """
    def main(
        self,
        args=None,
        prog_name=None,
        complete_var=None,
        standalone_mode=True,
        **extra
    ):
        # Make sure to set the locale before calling the main method of
        # click command/group that validate if the environment is
        # good for unicode on Python 3 or not.
        self.set_locale_env()
        super(CommandMixin, self).main(
            args=args,
            prog_name=prog_name,
            complete_var=complete_var,
            standalone_mode=standalone_mode,
            **extra
        )

    @staticmethod
    def set_locale_env():
        # inspired by how click library handle unicode for python 3 environment
        # https://github.com/pallets/click/blob/7.1.2/src/click/_unicodefun.py
        try:
            encoding = codecs.lookup(locale.getpreferredencoding()).name
        except Exception:
            encoding = 'ascii'
        if encoding == 'ascii':
            if os.name == "posix":
                try:
                    locales = subprocess.Popen(
                        ["locale", "-a"], stdout=subprocess.PIPE,
                        stderr=subprocess.PIPE
                    ).communicate()[0]
                except OSError:
                    locales = b""

                if isinstance(locales, bytes):
                    locales = locales.decode("ascii", "replace")

                local_to_set = None
                for line in locales.splitlines():
                    locale_env = line.strip()
                    if locale_env.lower() in (
                            "en_us.utf8",
                            "en_us.utf-8",
                            "c.utf8",
                            "c.utf-8"
                    ):
                        local_to_set = locale_env
                    if local_to_set:
                        os.environ['LC_ALL'] = local_to_set
                        os.environ['LANG'] = local_to_set
                        break


class AliasedGroup(CommandMixin, click.Group):
    def __init__(self, *args, **kwargs):
        self.max_suggestions = kwargs.pop("max_suggestions", 3)
        self.cutoff = kwargs.pop("cutoff", 0.5)
        super(AliasedGroup, self).__init__(*args, **kwargs)

    def get_command(self, ctx, cmd_name):
        rv = click.Group.get_command(self, ctx, cmd_name)
        if rv is not None:
            return rv
        matches = \
            [x for x in self.list_commands(ctx) if x.startswith(cmd_name)]
        if not matches:
            return None
        elif len(matches) == 1:
            return click.Group.get_command(self, ctx, matches[0])
        ctx.fail('Too many matches: {0}'.format(', '.join(sorted(matches))))

    def resolve_command(self, ctx, args):
        """Override clicks ``resolve_command`` method
        and appends *Did you mean ...* suggestions
        to the raised exception message.
        """
        try:
            return super(AliasedGroup, self).resolve_command(ctx, args)
        except click.exceptions.UsageError as error:
            error_msg = str(error)
            original_cmd_name = click.utils.make_str(args[0])
            matches = difflib.get_close_matches(
                original_cmd_name,
                self.list_commands(ctx),
                self.max_suggestions,
                self.cutoff)
            if matches:
                error_msg += '\n\nDid you mean one of these?\n    {0}'.format(
                    '\n    '.join(matches))
            raise click.exceptions.UsageError(error_msg, error.ctx)

    def command(self, *a, **kw):
        kw.setdefault('cls', CommandWithLoggers)
        return super(AliasedGroup, self).command(*a, **kw)


def group(name):
    """Allow to create a group with a default click context
    and a cls for click's `didyoueamn` without having to repeat
    it for every group.
    """
    return click.group(
        name=name,
        context_settings=CLICK_CONTEXT_SETTINGS,
        cls=AliasedGroup)


class CommandWithLoggers(CommandMixin, click.Command):
    """Like a click Command, but configure loggers first.

    We want loggers to be configured after argument parsing has been
    performed (ie. verbose/quiet callbacks have fired), but before the
    command was actually run.
    """

    def invoke(self, *a, **kw):
        logger.configure_loggers()
        return super(CommandWithLoggers, self).invoke(*a, **kw)


def command(*args, **kwargs):
    """Make Click commands Cloudify specific

    This exists purely for aesthetical reasons, otherwise
    Some decorators are called `@click.something` instead of
    `@cfy.something`
    """
    kwargs.setdefault('cls', CommandWithLoggers)
    return click.command(*args, **kwargs)


def argument(*args, **kwargs):
    """Make Click arguments Cloudify specific

    This exists purely for aesthetic reasons, otherwise
    Some decorators are called `@click.something` instead of
    `@cfy.something`
    """
    return click.argument(*args, **kwargs)


class Options(object):
    def __init__(self):
        """The options api is nicer when you use each option by calling
        `@cfy.options.some_option` instead of `@cfy.some_option`.

        Note that some options are attributes and some are static methods.
        The reason for that is that we want to be explicit regarding how
        a developer sees an option. It it can receive arguments, it's a
        method - if not, it's an attribute.
        """
        self.version = click.option(
            '--version',
            is_flag=True,
            callback=show_version,
            expose_value=False,
            is_eager=True,
            help=helptexts.VERSION)

        self.format = click.option(
            '--format',
            type=click.Choice(['plain', 'json']),
            expose_value=False,
            callback=set_format
        )

        self.json = click.option(
            '--json',
            is_flag=True,
            expose_value=False,
            default=None,
            callback=set_json)

        self.inputs = click.option(
            '-i',
            '--inputs',
            multiple=True,
            callback=inputs_callback,
            help=helptexts.INPUTS)

        self.runtime_properties = click.option(
            '-p',
            '--properties',
            required=True,
            multiple=True,
            callback=properties_callback,
            help=helptexts.RUNTIME_PROPERTIES)

        self.reinstall_list = click.option(
            '-r',
            '--reinstall-list',
            multiple=True,
            help=helptexts.REINSTALL_LIST)

        self.parameters = click.option(
            '-p',
            '--parameters',
            multiple=True,
            callback=inputs_callback,
            help=helptexts.PARAMETERS)

        self.output_path = click.option(
            '-o',
            '--output-path',
            help=helptexts.OUTPUT_PATH)

        self.override_collisions = click.option(
            '--override-collisions',
            is_flag=True,
            help=helptexts.OVERRIDE_COLLISIONS
        )

        self.tenant_map = click.option(
            '-m',
            '--tenant-map',
            type=click.Path(exists=True),
            help=helptexts.TENANT_MAP
        )

        self.all_nodes = click.option(
            '--all-nodes',
            is_flag=True,
            help=helptexts.ALL_NODES
        )

        self.optional_output_path = click.option(
            '-o',
            '--output-path',
            help=helptexts.OUTPUT_PATH)

        self.allow_custom_parameters = click.option(
            '--allow-custom-parameters',
            is_flag=True,
            help=helptexts.ALLOW_CUSTOM_PARAMETERS)

        self.install_plugins = click.option(
            '--install-plugins',
            is_flag=True,
            help=helptexts.INSTALL_PLUGINS)

        self.all_tenants = click.option(
            '-a',
            '--all-tenants',
            is_flag=True,
            default=False,
            help=helptexts.ALL_TENANTS,
        )

        self.all_executions = click.option(
            '--all-executions',
            is_flag=True,
            default=False,
            help=helptexts.ALL_EXECUTIONS,
        )

        self.search = click.option(
            '--search',
            default=None,
            required=False,
            help=helptexts.SEARCH,
        )

        self.include_logs = click.option(
            '--include-logs/--no-logs',
            default=True,
            help=helptexts.INCLUDE_LOGS)

        self.dry_run = click.option(
            '--dry-run',
            is_flag=True,
            help=helptexts.DRY_RUN
        )

        self.json_output = click.option(
            '--json-output',
            is_flag=True,
            callback=json_output_deprecate,
            help=helptexts.JSON_OUTPUT)

        self.tail = click.option(
            '--tail',
            is_flag=True,
            cls=MutuallyExclusiveOption,
            mutually_exclusive=['pagination_offset', 'pagination_size'],
            help=helptexts.TAIL_OUTPUT)

        self.validate = click.option(
            '--validate',
            is_flag=True,
            help=helptexts.VALIDATE_BLUEPRINT)

        self.skip_install = click.option(
            '--skip-install',
            is_flag=True,
            help=helptexts.SKIP_INSTALL)

        self.skip_uninstall = click.option(
            '--skip-uninstall',
            is_flag=True,
            help=helptexts.SKIP_UNINSTALL)

        self.skip_reinstall = click.option(
            '--skip-reinstall',
            is_flag=True,
            help=helptexts.SKIP_REINSTALL)

        self.ignore_failure = click.option(
            '--ignore-failure',
            is_flag=True,
            help=helptexts.IGNORE_FAILURE)

        self.install_first = click.option(
            '--install-first',
            is_flag=True,
            help=helptexts.INSTALL_FIRST)

        self.preview = click.option(
            '--preview',
            is_flag=True,
            help=helptexts.PREVIEW)

        self.dont_update_plugins = click.option(
            '--dont-update-plugins',
            is_flag=True,
            help=helptexts.DONT_UPDATE_PLUGINS)

        self.backup_first = click.option(
            '--backup-first',
            is_flag=True,
            help=helptexts.BACKUP_LOGS_FIRST)

        self.ssh_user = click.option(
            '-s',
            '--ssh-user',
            required=False,
            help=helptexts.SSH_USER)

        self.ssh_user_flag = click.option(
            '-s',
            '--ssh-user',
            required=False,
            is_flag=True,
            default=False,
            help=helptexts.SSH_USER)

        self.ssh_key = click.option(
            '-k',
            '--ssh-key',
            required=False,
            cls=MutuallyExclusiveOption,
            help=helptexts.SSH_KEY)

        self.ssh_key_flag = click.option(
            '-k',
            '--ssh-key',
            required=False,
            is_flag=True,
            default=False,
            help=helptexts.SSH_KEY)

        self.profile_name = click.option(
            '--profile-name',
            required=False,
            help=helptexts.PROFILE_NAME)

        self.profile_manager_ip = click.option(
            '-m', '--manager-ip',
            help=helptexts.PROFILE_MANAGER_IP,
        )

        self.manager_username = click.option(
            '-u',
            '--manager-username',
            required=False,
            help=helptexts.MANAGER_USERNAME,
            callback=validate_name
        )

        self.manager_username_flag = click.option(
            '-u',
            '--manager-username',
            required=False,
            is_flag=True,
            default=False,
            help=helptexts.MANAGER_USERNAME)

        self.manager_password = click.option(
            '-p',
            '--manager-password',
            required=False,
            help=helptexts.MANAGER_PASSWORD,
            callback=validate_password)

        self.manager_password_flag = click.option(
            '-p',
            '--manager-password',
            required=False,
            is_flag=True,
            default=False,
            help=helptexts.MANAGER_PASSWORD)

        self.manager_tenant_flag = click.option(
            '-t',
            '--manager-tenant',
            required=False,
            is_flag=True,
            default=False,
            help=helptexts.MANAGER_TENANT)

        self.rest_certificate = click.option(
            '-c',
            '--rest-certificate',
            required=False,
            help=helptexts.REST_CERT
        )

        self.rest_certificate_flag = click.option(
            '-c',
            '--rest-certificate',
            required=False,
            is_flag=True,
            default=False,
            help=helptexts.REST_CERT)

        self.kerberos_env = click.option(
            '--kerberos-env',
            required=False,
            help=helptexts.KERBEROS_ENV
        )

        self.kerberos_env_flag = click.option(
            '--kerberos-env',
            required=False,
            is_flag=True,
            default=False,
            help=helptexts.KERBEROS_ENV)

        self.ssl_state = click.option(
            '--ssl',
            required=False,
            help=helptexts.SSL_STATE,
            callback=parse_on_off,
        )

        self.ssh_port = click.option(
            '--ssh-port',
            required=False,
            help=helptexts.SSH_PORT)

        self.rest_port = click.option(
            '--rest-port',
            required=False,
            help=helptexts.REST_PORT,
            type=int)

        self.ssl_rest = click.option(
            '--ssl',
            is_flag=True,
            required=False,
            default=False,
            help=helptexts.SSL_REST)

        self.init_hard_reset = click.option(
            '--hard',
            is_flag=True,
            help=helptexts.HARD_RESET)

        self.reset_context = click.option(
            '-r',
            '--reset-context',
            is_flag=True,
            help=helptexts.RESET_CONTEXT)

        self.enable_colors = click.option(
            '--enable-colors',
            is_flag=True,
            default=False,
            help=helptexts.ENABLE_COLORS)

        self.wait = click.option(
            '--wait',
            is_flag=True,
            help=helptexts.MAINTENANCE_MODE_WAIT)

        self.node_name = click.option(
            '-n',
            '--node-name',
            required=False,
            help=helptexts.NODE_NAME)

        self.without_deployment_envs = click.option(
            '--without-deployment-envs',
            is_flag=True,
            help=helptexts.RESTORE_SNAPSHOT_EXCLUDE_EXISTING_DEPLOYMENTS)

        self.exclude_credentials = click.option(
            '--exclude-credentials',
            is_flag=True,
            help=helptexts.EXCLUDE_CREDENTIALS_IN_SNAPSHOT)

        self.exclude_logs = click.option(
            '--exclude-logs',
            is_flag=True,
            help=helptexts.EXCLUDE_LOGS_IN_SNAPSHOT)

        self.exclude_events = click.option(
            '--exclude-events',
            is_flag=True,
            help=helptexts.EXCLUDE_EVENTS_IN_SNAPSHOT)

        self.ssh_command = click.option(
            '-c',
            '--command',
            help=helptexts.SSH_COMMAND)

        self.group_name = click.option(
            '-g',
            '--group-name',
            required=True,
            help=helptexts.GROUP,
            callback=validate_name
        )

        self.ldap_distinguished_name = click.option(
            '-l',
            '--ldap-distinguished-name',
            required=False,
            help=helptexts.GROUP_DN)

        self.host_session = click.option(
            '--host',
            is_flag=True,
            help=helptexts.SSH_HOST_SESSION)

        self.session_id = click.option(
            '--sid',
            help=helptexts.SSH_CONNECT_TO_SESSION)

        self.list_sessions = click.option(
            '-l',
            '--list-sessions',
            is_flag=True,
            help=helptexts.SSH_LIST_SESSIONS)

        self.ignore_deployments = click.option(
            '--ignore-deployments',
            is_flag=True,
            help=helptexts.IGNORE_DEPLOYMENTS)

        self.include_system_workflows = click.option(
            '--include-system-workflows',
            required=False,
            is_flag=True,
            help=helptexts.INCLUDE_SYSTEM_WORKFLOWS)

        self.descending = click.option(
            '--descending',
            required=False,
            is_flag=True,
            default=False,
            help=helptexts.DESCENDING)

        self.restore_certificates = click.option(
            '--restore-certificates',
            required=False,
            is_flag=True,
            default=False,
            help=helptexts.RESTORE_CERTIFICATES)

        self.no_reboot = click.option(
            '--no-reboot',
            required=False,
            is_flag=True,
            default=False,
            help=helptexts.NO_REBOOT)

        self.security_role = click.option(
            '-r',
            '--security-role',
            required=False,
            default='default',
            help=helptexts.SECURITY_ROLE)

        self.password = click.option(
            '-p',
            '--password',
            required=True,
            help=helptexts.PASSWORD,
            callback=validate_password)

        self.encryption_passphrase = click.option(
            '-p',
            '--passphrase',
            required=False,
            help=helptexts.ENCRYPTION_PASSPHRASE,
            callback=validate_encryption_passphrase
        )

        self.visibility_filter = click.option(
            '-l',
            '--visibility',
            required=False,
            help=helptexts.VISIBILITY_FILTER.format(VisibilityState.STATES)
        )

        self.filter_by = click.option(
            '--filter-by',
            required=False,
            help=helptexts.FILTER_BY_KEYWORD
        )

        self.skip_credentials_validation = click.option(
            '--skip-credentials-validation',
            is_flag=True,
            default=False,
            help=helptexts.SKIP_CREDENTIALS_VALIDATION
        )

        self.cluster_host_ip = click.option(
            '--cluster-host-ip',
            default=lambda: env.profile.manager_ip,
            help=helptexts.CLUSTER_HOST_IP)

        self.cluster_node_name = click.option(
            '--cluster-node-name',
            default=lambda: 'cloudify_manager_' + generate_random_string(),
            help=helptexts.CLUSTER_NODE_NAME)

        self.private_resource = click.option(
            '--private-resource',
            is_flag=True,
            default=False,
            help=helptexts.PRIVATE_RESOURCE
        )

        self.skip_plugins_validation = click.option(
            '--skip-plugins-validation',
            is_flag=True,
            default=False,
            help=helptexts.SKIP_PLUGINS_VALIDATION
        )

        self.users = click.option(
            '-u',
            '--users',
            required=True,
            multiple=True,
            help=helptexts.USER
        )

        self.get_data = click.option(
            '--get-data',
            is_flag=True,
            default=False,
            help=helptexts.GET_DATA
        )

        self.secret_string = click.option(
            '-s',
            '--secret-string',
            required=False,
            help=helptexts.SECRET_STRING)

        self.secret_update_if_exists = click.option(
            '-u',
            '--update-if-exists',
            is_flag=True,
            cls=MutuallyExclusiveOption,
            mutually_exclusive=['hidden_value', 'visibility'],
            help=helptexts.SECRET_UPDATE_IF_EXISTS,
        )

        self.hidden_value = click.option(
            '--hidden-value',
            is_flag=True,
            default=False,
            help=helptexts.HIDDEN_VALUE,
        )

        self.update_hidden_value = click.option(
            '--hidden-value/--not-hidden-value',
            default=None,
            help=helptexts.HIDDEN_VALUE)

        self.update_visibility = click.option(
            '-l',
            '--visibility',
            help=helptexts.VISIBILITY.format(VisibilityState.STATES)
        )

        self.plugins_bundle_path = click.option(
            '-p',
            '--path',
            required=False,
            help=helptexts.PLUGINS_BUNDLE_PATH
        )

        # same as --inputs, name changed for consistency
        self.cluster_node_options = click.option(
            '-o',
            '--options',
            multiple=True,
            callback=inputs_callback,
            help=helptexts.CLUSTER_NODE_OPTIONS)

        self.pagination_offset = click.option(
            '-o',
            '--pagination-offset',
            required=False,
            default=0,
            callback=validate_nonnegative_integer,
            help=helptexts.PAGINATION_OFFSET)

        self.pagination_size = click.option(
            '-s',
            '--pagination-size',
            required=False,
            default=1000,
            callback=validate_nonnegative_integer,
            help=helptexts.PAGINATION_SIZE)

        self.manager_ip = click.option(
            '--manager-ip',
            required=False,
            help=helptexts.MANAGER_IP
        )

        self.manager_certificate = click.option(
            '--manager_certificate',
            required=False,
            help=helptexts.MANAGER_CERTIFICATE_PATH
        )

        self.stop_old_agent = click.option(
            '--stop-old-agent',
            is_flag=True,
            default=False,
            help=helptexts.STOP_OLD_AGENT
        )

        self.ignore_plugin_failure = click.option(
            '-i',
            '--ignore-plugin-failure',
            is_flag=True,
            default=False,
            help=helptexts.IGNORE_PLUGIN_FAILURE
        )

        self.queue = click.option(
            '--queue',
            is_flag=True,
            default=False,
            cls=MutuallyExclusiveOption,
            mutually_exclusive=['dry_run', 'force'],
            help=helptexts.QUEUE_EXECUTIONS
        )

        self.reset_operations = click.option(
            '--reset-operations',
            is_flag=True,
            default=False,
            help=helptexts.RESET_OPERATIONS
        )

        self.schedule = click.option(
            '--schedule',
            cls=MutuallyExclusiveOption,
            mutually_exclusive=['queue'],
            help=helptexts.SCHEDULE_EXECUTIONS
        )

        self.queue_snapshot = click.option(
            '--queue',
            is_flag=True,
            default=False,
            help=helptexts.QUEUE_SNAPSHOTS
        )

        self.wait_after_fail = click.option(
            '--wait-after-fail',
            default=600,
            type=int,
            help=helptexts.WAIT_AFTER_FAIL
        )

        self.agents_wait = click.option(
            '--wait/--no-wait',
            is_flag=True,
            default=True,
            help=helptexts.AGENTS_WAIT
        )

        self.install_agent_timeout = click.option(
            '--install-agent-timeout',
            default=300,
            type=int,
            help=helptexts.INSTALL_AGENT_TIMEOUT
        )

        self.location = click.option(
            '--location',
            required=False,
            help=helptexts.LOCATION
        )

        self.site_name = click.option(
            '-s',
            '--site-name',
            required=False,
            callback=validate_name,
            help=helptexts.SITE_NAME
        )

        self.detach_site = click.option(
            '-d',
            '--detach-site',
            required=False,
            is_flag=True,
            default=False,
            cls=MutuallyExclusiveOption,
            mutually_exclusive=['site_name'],
            help=helptexts.DETACH_SITE
        )

        self.with_logs = click.option(
            '-l',
            '--with-logs',
            required=False,
            is_flag=True,
            default=False,
            help=helptexts.WITH_LOGS
        )

        self.port = click.option(
            '--port',
            required=False,
            type=click.IntRange(1, 65535),
            help=helptexts.PORT,
        )

        # Args for configuring ldap
        self.ldap_server = click.option(
            '-s',
            '--ldap-server',
            required=True,
            help=helptexts.LDAP_SERVER,
        )
        self.ldap_username = click.option(
            '-u',
            '--ldap-username',
            required=False,
            default=None,
            help=helptexts.LDAP_USERNAME,
        )
        self.ldap_password = click.option(
            '-p',
            '--ldap-password',
            required=False,
            default=None,
            help=helptexts.LDAP_PASSWORD,
        )
        self.ldap_domain = click.option(
            '-d',
            '--ldap-domain',
            required=False,
            help=helptexts.LDAP_DOMAIN,
        )
        self.ldap_is_active_directory = click.option(
            '-a',
            '--ldap-is-active-directory',
            required=False,
            is_flag=True,
            default=False,
            help=helptexts.LDAP_IS_ACTIVE_DIRECTORY,
        )
        self.ldap_dn_extra = click.option(
            '-e',
            '--ldap-dn-extra',
            required=False,
            help=helptexts.LDAP_DN_EXTRA,
        )
        self.ldap_ca_path = click.option(
            '-c',
            '--ldap-ca-path',
            required=False,
            help=helptexts.LDAP_CA_PATH,
        )
        self.ldap_base_dn = click.option(
            '--ldap-base-dn',
            required=False,
            help=helptexts.LDAP_BASE_DN,
        )
        self.ldap_group_dn = click.option(
            '--ldap-group-dn',
            required=False,
            help=helptexts.LDAP_GROUP_DN,
        )
        self.ldap_bind_format = click.option(
            '--ldap-bind-format',
            required=False,
            help=helptexts.LDAP_BIND_FORMAT,
        )
        self.ldap_user_filter = click.option(
            '--ldap-user-filter',
            required=False,
            help=helptexts.LDAP_USER_FILTER,
        )
        self.ldap_group_member_filter = click.option(
            '--ldap-group-member-filter',
            required=False,
            help=helptexts.LDAP_GROUP_MEMBER_FILTER,
        )
        self.ldap_attribute_email = click.option(
            '--ldap-attribute-email',
            required=False,
            help=helptexts.LDAP_ATTRIBUTE_EMAIL,
        )
        self.ldap_attribute_first_name = click.option(
            '--ldap-attribute-first-name',
            required=False,
            help=helptexts.LDAP_ATTRIBUTE_FIRST_NAME,
        )
        self.ldap_attribute_last_name = click.option(
            '--ldap-attribute-last-name',
            required=False,
            help=helptexts.LDAP_ATTRIBUTE_LAST_NAME,
        )
        self.ldap_attribute_uid = click.option(
            '--ldap-attribute-uid',
            required=False,
            help=helptexts.LDAP_ATTRIBUTE_UID,
        )
        self.ldap_attribute_group_membership = click.option(
            '--ldap-attribute-group-membership',
            required=False,
            help=helptexts.LDAP_ATTRIBUTE_GROUP_MEMBERSHIP,
        )
        self.ldap_nested_levels = click.option(
            '--ldap-nested-levels',
            required=False,
            help=helptexts.LDAP_NESTED_LEVELS,
        )

        self.node_id = click.option(
            '--node-id',
            required=False,
            help=helptexts.NODE_ID,
        )

        self.runtime_only_evaluation = click.option(
            '--runtime-only-evaluation',
            is_flag=True,
            default=False,
            required=False,
            help=helptexts.RUNTIME_ONLY_EVALUATION)

        self.auto_correct_types = click.option(
            '--auto-correct-types',
            is_flag=True,
            default=False,
            required=False,
            help=helptexts.AUTO_CORRECT_TYPES)

        self.manager = click.option(
            '--manager',
            required=False,
            expose_value=False,
            help=helptexts.MANAGER,
            callback=set_manager)

        self.all_blueprints = click.option(
            '-a',
            '--all',
            'all_blueprints',
            is_flag=True,
            default=False,
            help=helptexts.PLUGINS_UPDATE_ALL)

        self.except_blueprints = click.option(
            '--except-blueprint',
            'except_blueprints',
            multiple=True,
            required=False,
            help=helptexts.PLUGINS_UPDATE_EXCEPT_BLUEPRINT,
            callback=self.parse_comma_separated)

        self.plugin_names = click.option(
            '--plugin-name',
            'plugin_names',
            multiple=True,
            required=False,
            help=helptexts.PLUGINS_UPDATE_NAME,
            callback=self.parse_comma_separated)

        self.plugins_to_latest = click.option(
            '--to-latest',
            multiple=True,
            required=False,
            help=helptexts.PLUGINS_UPDATE_TO_LATEST,
            callback=self.parse_comma_separated)

        self.plugins_all_to_latest = click.option(
            '--all-to-latest',
            is_flag=True,
            default=None,
            help=helptexts.PLUGINS_UPDATE_ALL_TO_LATEST)

        self.plugins_to_minor = click.option(
            '--to-minor',
            multiple=True,
            required=False,
            help=helptexts.PLUGINS_UPDATE__TO_MINOR,
            callback=self.parse_comma_separated)

        self.plugins_all_to_minor = click.option(
            '--all-to-minor',
            is_flag=True,
            default=None,
            help=helptexts.PLUGINS_UPDATE_ALL_TO_MINOR)

        self.labels = click.option(
            '--labels',
            required=False,
            callback=parse_and_validate_labels,
            help=helptexts.LABELS
        )

        self.async_upload = click.option(
            '-a',
            '--async-upload',
            is_flag=True,
            default=False,
            help=helptexts.ASYNC_UPLOAD,
        )

        self.update_filter_rules = click.option(
            '--filter-rules',
            callback=parse_and_validate_filter_rules_list,
            help=helptexts.FILTER_RULES
        )

        self.filter_rules = click.option(
            '--filter',
            callback=parse_and_validate_filter_rules,
            help=helptexts.FILTER_RULES_OR_ID
        )

        self.frequency = click.option(
            '--freq',
            cls=MutuallyExclusiveOption,
            mutually_exclusive=['rrule'],
            required=False,
            help=helptexts.SCHEDULE_FREQUENCY
        )

        self.count = click.option(
            '-c',
            '--count',
            cls=MutuallyExclusiveOption,
            mutually_exclusive=['rrule'],
            required=False,
            type=int,
            help=helptexts.SCHEDULE_COUNT
        )

        self.weekdays = click.option(
            '--weekdays',
            cls=MutuallyExclusiveOption,
            mutually_exclusive=['rrule'],
            required=False,
            help=helptexts.SCHEDULE_WEEKDAYS
        )

        self.rrule = click.option(
            '--rrule',
            cls=MutuallyExclusiveOption,
            mutually_exclusive=['frequency', 'count', 'weekdays'],
            required=False,
            help=helptexts.SCHEDULE_RRULE
        )

        self.slip = click.option(
            '--slip',
            required=False,
            default=0,
            type=int,
            help=helptexts.SCHEDULE_SLIP
        )

        self.stop_on_fail = click.option(
            '--stop-on-fail',
            required=False,
            is_flag=True,
            default=False,
            help=helptexts.SCHEDULE_STOP_ON_FAIL
        )

        self.group_default_blueprint = click.option(
            '--default-blueprint', '-b',
            help=helptexts.DEP_GROUP_BLUEPRINT
        )

        self.group_description = click.option(
            '--description',
            help=helptexts.DEP_GROUP_DESCRIPTION
        )

        self.group_deployment_id = click.option(
            '--deployment-id', '-d',
            help=helptexts.DEP_GROUP_DEP_ID,
            multiple=True
        )

        self.group_count = click.option(
            '--count',
            help=helptexts.DEP_GROUP_COUNT,
            type=int
        )

    def common_options(self, f):
        """A shorthand for applying commonly used arguments.

        To be used for arguments that are going to be applied for all or
        almost all commands.
        """
        for arg in [self.json, self.verbose(), self.format, self.quiet(),
                    self.manager]:
            f = arg(f)
        return f

    def parse_comma_separated(self, ctx, param, value):
        """Callback for parsing multiple comma-separated arguments.

        This is for use with `--opt a --opt b,c` -> ['a', 'b', 'c']
        """
        if not value:
            return []
        return sum((part.split(',') for part in value), [])

    def agent_filters(self, f):
        """Set of filter arguments for commands working with a list of agents

        Applies deployment id, node id and node instance id filters.
        """
        node_instance_id = click.option('--node-instance-id', multiple=True,
                                        help=helptexts.AGENT_NODE_INSTANCE_ID,
                                        callback=self.parse_comma_separated)
        node_id = click.option('--node-id', multiple=True,
                               help=helptexts.AGENT_NODE_ID,
                               callback=self.parse_comma_separated)
        install_method = click.option('--install-method', multiple=True,
                                      help=helptexts.AGENT_INSTALL_METHOD,
                                      callback=self.parse_comma_separated)
        deployment_id = click.option('--deployment-id', multiple=True,
                                     help=helptexts.AGENT_DEPLOYMENT_ID,
                                     callback=self.parse_comma_separated)

        # we add separate --node-instance-id, --node-id and --deployment-id
        # arguments, but only expose a agents_filter = {'node_id': ..} dict
        # to the decorated function
        def _filters_deco(f):
            @wraps(f)
            def _inner(*args, **kwargs):
                filters = {}
                for arg_name, filter_name in [
                        ('node_id', AGENT_FILTER_NODE_IDS),
                        ('node_instance_id', AGENT_FILTER_NODE_INSTANCE_IDS),
                        ('deployment_id', AGENT_FILTER_DEPLOYMENT_ID),
                        ('install_method', AGENT_FILTER_INSTALL_METHODS)]:
                    filters[filter_name] = kwargs.pop(arg_name, None)

                kwargs['agent_filters'] = filters
                return f(*args, **kwargs)
            return _inner

        for arg in [install_method, node_instance_id, node_id,
                    deployment_id, _filters_deco]:
            f = arg(f)
        return f

    @staticmethod
    def secret_file():
        return click.option(
            '-f',
            '--secret-file',
            required=False,
            cls=MutuallyExclusiveOption,
            mutually_exclusive=['secret_string'],
            help=helptexts.SECRET_FILE
        )

    @staticmethod
    def include_keys(help):
        return click.option(
            '--include-keys',
            is_flag=True,
            help=help)

    @staticmethod
    def verbose(expose_value=False):
        return click.option(
            '-v',
            '--verbose',
            count=True,
            callback=set_verbosity_level,
            expose_value=expose_value,
            is_eager=True,
            help=helptexts.VERBOSE)

    @staticmethod
    def quiet(expose_value=False):
        return click.option(
            '-q',
            '--quiet',
            is_flag=True,
            callback=set_verbosity_level,
            expose_value=expose_value,
            is_eager=True,
            help=helptexts.QUIET)

    @staticmethod
    def tenant_name(required=True,
                    mutually_exclusive_with=None,
                    help=None,
                    resource_name_for_help=None,
                    show_default_in_help=True):
        args = ['-t', '--tenant-name']
        kwargs = {
            'required': required,
            'multiple': False,
            'help': _tenant_help_message(
                help, helptexts.TENANT_TEMPLATE, resource_name_for_help),
            'callback': validate_name
        }
        if show_default_in_help:
            kwargs['help'] += \
                '. If not specified, the current tenant will be used'
        if mutually_exclusive_with:
            kwargs['cls'] = MutuallyExclusiveOption
            kwargs['mutually_exclusive'] = mutually_exclusive_with
        return click.option(*args, **kwargs)

    @staticmethod
    def tenant_name_for_list(*args, **kwargs):
        if 'mutually_exclusive_with' not in kwargs:
            kwargs['mutually_exclusive_with'] = ['all_tenants']
        kwargs['help'] = _tenant_help_message(
            kwargs.get('help'),
            helptexts.TENANT_LIST_TEMPLATE,
            kwargs.get('resource_name_for_help'))
        return Options.tenant_name(
            *args, **kwargs)

    @staticmethod
    def force(help):
        return click.option(
            '-f',
            '--force',
            is_flag=True,
            help=help)

    @staticmethod
    def kill():
        return click.option(
            '-k',
            '--kill',
            is_flag=True,
            help=helptexts.KILL_EXECUTION)

    @staticmethod
    def blueprint_filename(extra_message=''):
        return click.option(
            '-n',
            '--blueprint-filename',
            default=DEFAULT_BLUEPRINT_PATH,
            help=helptexts.BLUEPRINT_FILENAME + extra_message)

    @staticmethod
    def workflow_id(default=None, required=False):
        return click.option(
            '-w',
            '--workflow-id',
            default=default,
            required=required,
            help=helptexts.WORKFLOW_TO_EXECUTE.format(default))

    @staticmethod
    def task_thread_pool_size(default=1):
        return click.option(
            '--task-thread-pool-size',
            type=int,
            default=default,
            help=helptexts.TASK_THREAD_POOL_SIZE.format(default))

    @staticmethod
    def task_retries(default=0):
        return click.option(
            '--task-retries',
            type=int,
            default=default,
            help=helptexts.TASK_RETRIES.format(default))

    @staticmethod
    def sort_by(default='created_at'):
        return click.option(
            '--sort-by',
            required=False,
            default=default,
            help=helptexts.SORT_BY)

    @staticmethod
    def task_retry_interval(default=1):
        return click.option(
            '--task-retry-interval',
            type=int,
            default=default,
            help=helptexts.TASK_RETRIES.format(default))

    @staticmethod
    def timeout(default=900):
        return click.option(
            '--timeout',
            type=int,
            default=default,
            help=helptexts.OPERATION_TIMEOUT.format(default))

    @staticmethod
    def deployment_id(required=False, validate=False):
        return click.option(
            '-d',
            '--deployment-id',
            required=required,
            help=helptexts.DEPLOYMENT_ID,
            callback=_get_validate_callback(validate))

    @staticmethod
    def snapshot_id(required=False, validate=False):
        return click.option(
            '-s',
            '--snapshot-id',
            required=required,
            help=helptexts.SNAPSHOT_ID,
            callback=_get_validate_callback(validate))

    @staticmethod
    def execution_id(required=False, dest=None):
        return click.option(
            '-e',
            '--execution-id',
            dest,
            required=required,
            help=helptexts.EXECUTION_ID)

    @staticmethod
    def blueprint_id(required=False, validate=False):
        return click.option(
            '-b',
            '--blueprint-id',
            required=required,
            help=helptexts.BLUEPRINT_ID,
            callback=_get_validate_callback(validate))

    @staticmethod
    def blueprint_path(required=False):
        return click.option(
            '-p',
            '--blueprint-path',
            required=required,
            type=click.Path(exists=True),
            help=helptexts.BLUEPRINT_PATH)

    @staticmethod
    def tenant_role(help_text, required, options_flags=None):
        args = options_flags or ['-r', '--role']

        kwargs = {
            'required': required,
            'help': help_text
        }
        return click.option(*args, **kwargs)

    @staticmethod
    def user_tenant_role(required=True, options_flags=None):
        return Options.tenant_role(
            helptexts.USER_TENANT_ROLE, required=required,
            options_flags=options_flags)

    @staticmethod
    def group_tenant_role():
        return Options.tenant_role(
            helptexts.GROUP_TENANT_ROLE, required=True)

    @staticmethod
    def visibility(required=False,
                   valid_values=VisibilityState.STATES,
                   mutually_exclusive_required=True):
        args = ['-l', '--visibility']
        kwargs = {
            'required': required,
            'help': helptexts.VISIBILITY.format(valid_values)
        }
        if not required:
            kwargs['default'] = VisibilityState.TENANT
            kwargs['help'] += ' [default: tenant]'
            if mutually_exclusive_required:
                kwargs['cls'] = MutuallyExclusiveOption
                kwargs['mutually_exclusive'] = ['private_resource']
        return click.option(*args, **kwargs)

    @staticmethod
    def plugin_yaml_path():
        return click.option(
            '-y',
            '--yaml-path',
            required=True,
            help=helptexts.PLUGIN_YAML_PATH)

    @staticmethod
    def plugin_icon_path():
        return click.option(
            '-i',
            '--icon-path',
            required=False,
            help=helptexts.PLUGIN_ICON_PATH)

    @staticmethod
    def plugin_title():
        return click.option(
            '--title',
            required=False,
            help=helptexts.PLUGIN_TITLE)

    @staticmethod
    def new_name(resource_name_for_help=None):
        return click.option(
            '-n',
            '--new-name',
            required=False,
            help=helptexts.NEW_NAME.format(resource_name_for_help),
            callback=validate_name
        )

    def networks(self, required=True):
        return click.option(
            '-n',
            '--networks',
            required=required,
            multiple=True,
            callback=inputs_callback,
            help=helptexts.NETWORKS
        )

    @staticmethod
    def input_path(required=False, help=helptexts.INPUT_PATH):
        return click.option(
            '-i',
            '--input-path',
            required=required,
            help=help
        )

    @staticmethod
    def import_input_path():
        return click.option(
            '-i',
            '--input-path',
            required=True,
            type=click.Path(exists=True),
            help=helptexts.IMPORT_SECRETS
        )

    @staticmethod
    def non_encrypted():
        return click.option(
            '--non-encrypted',
            cls=MutuallyExclusiveOption,
            mutually_exclusive=['passphrase'],
            is_flag=True,
            default=False,
            help=helptexts.NON_ENCRYPTED
        )

    @staticmethod
    def from_datetime(required=False, mutually_exclusive_with=None,
                      help=helptexts.FROM_DATETIME):
        kwargs = {
            'required': required,
            'type': Timestamp((
                '%Y-%m-%d', '%Y-%m-%dT%H:%M:%S', '%Y-%m-%d %H:%M:%S',
                '%Y-%m-%d %H:%M:%S.%f',)),
            'help': help,
        }
        if mutually_exclusive_with:
            kwargs['cls'] = MutuallyExclusiveOption
            kwargs['mutually_exclusive'] = mutually_exclusive_with
        return click.option('from_datetime', '--from', **kwargs)

    @staticmethod
    def to_datetime(required=False, mutually_exclusive_with=None,
                    help=helptexts.TO_DATETIME):
        kwargs = {
            'required': required,
            'type': Timestamp((
                '%Y-%m-%d', '%Y-%m-%dT%H:%M:%S', '%Y-%m-%d %H:%M:%S',
                '%Y-%m-%d %H:%M:%S.%f',)),
            'help': help,
        }
        if mutually_exclusive_with:
            kwargs['cls'] = MutuallyExclusiveOption
            kwargs['mutually_exclusive'] = mutually_exclusive_with
        return click.option('to_datetime', '--to', **kwargs)

    @staticmethod
    def before(required=False,
               mutually_exclusive_with=None,
               help=helptexts.BEFORE):
        kwargs = {
            'required': required,
            'type': str,
            'callback': _parse_relative_datetime,
            'expose_value': True,
            'help': help,
        }
        if mutually_exclusive_with:
            kwargs['cls'] = MutuallyExclusiveOption
            kwargs['mutually_exclusive'] = mutually_exclusive_with
        return click.option('--before', **kwargs)

    @staticmethod
    def keep_last(resource_name,
                  required=False,
                  mutually_exclusive_with=None):
        kwargs = {
            'required': required,
            'type': click.IntRange(min=1),
            'help': helptexts.KEEP_LAST.format(resource_name),
        }
        if mutually_exclusive_with:
            kwargs['cls'] = MutuallyExclusiveOption
            kwargs['mutually_exclusive'] = mutually_exclusive_with
        return click.option('--keep-last', **kwargs)

    @staticmethod
    def store_before(default=False):
        return click.option(
            '--store-before',
            is_flag=True,
            default=default,
            help=helptexts.STORE_BEFORE_DELETION
        )

    @staticmethod
    def store_output_path():
        return click.option(
            '-o',
            '--output-path',
            required=False,
            type=click.Path(file_okay=True, dir_okay=False),
            help=helptexts.STORE_OUTPUT_PATH
        )

    @staticmethod
    def manager_tenant(default=None):
        return click.option(
            '-t',
            '--manager-tenant',
            required=False,
            help=helptexts.MANAGER_TENANT,
            callback=validate_name,
            default=default
        )

    @staticmethod
    def reevaluate_active_statuses(
            help=helptexts.REEVALUATE_ACTIVE_STATUSES):
        return click.option(
            '--reevaluate-active-statuses',
            is_flag=True,
            default=False,
            required=False,
            help=help)


options = Options()
