# -*- coding: utf-8 -*-
"""TcEx testing profile Class."""
import base64
import json
import logging
import math
import os
import pickle
import re
import sys
import zlib
from random import randint

import colorama as c
from requests.sessions import Session

from ..__metadata__ import __version__ as tcex_version
from ..env_store import EnvStore
from ..utils import Utils
from .install_json import InstallJson
from .layout_json import LayoutJson
from .permutations import Permutations

try:
    import jmespath
except ImportError:
    # jmespath is only required for local development
    pass


# autoreset colorama
c.init(autoreset=True, strip=False)


class Profile:
    """Testing Profile Class.

    Args:
        default_args (dict, optional): The default Args for the profile.
        feature (str, optional): The feature name.
        name (str, optional): The filename of the profile in the profile.d director.
        redis_client (redis.client.Redis, optional): An instance of Redis client.
        pytestconfig (?, optional): Pytest config object.
        monkeypatch (?, optional): Pytest monkeypatch object.
        tcex_testing_context (str, optional): The current context for this profile.
        logger (logging.Logger, optional): An logging instance.
        options (dict, optional): ?
    """

    def __init__(
        self,
        default_args=None,
        feature=None,
        name=None,
        redis_client=None,
        pytestconfig=None,
        monkeypatch=None,
        tcex_testing_context=None,
        logger=None,
        options=None,
    ):
        """Initialize Class properties."""
        self._default_args = default_args or {}
        self._feature = feature
        self._name = name
        self.log = logger or logging.getLogger('profile').addHandler(logging.NullHandler())
        self.redis_client = redis_client
        self.pytestconfig = pytestconfig
        self.monkeypatch = monkeypatch
        self.tcex_testing_context = tcex_testing_context
        self.test_options = options

        # properties
        self._app_path = os.getcwd()
        self._data = None
        self._output_variables = None
        self._context_tracker = []
        self._pytest_args = None
        self.env_store = EnvStore(logger=self.log)
        self.ij = InstallJson(logger=self.log)
        self.lj = LayoutJson(logger=self.log)
        self.permutations = Permutations(logger=self.log)
        self.tc_staged_data = {}

    @property
    def _test_case_data(self):
        """Return partially parsed test case data."""
        return os.getenv('PYTEST_CURRENT_TEST').split(' ')[0].split('::')

    @property
    def _test_case_name(self):
        """Return partially parsed test case data."""
        return self._test_case_data[-1].replace('/', '-').replace('[', '-').replace(']', '')

    def _write_file(self, json_data):
        """Write updated profile file.

        Args:
            json_data (dict): The profile data.
        """
        # Permuted test cases set options to a true value, so disable writeback
        if self.test_options:
            return

        with open(self.filename, 'w') as fh:
            fh.write(f'{json.dumps(json_data, indent=2, sort_keys=True)}\n')

    def add(self, profile_data=None, profile_name=None, sort_keys=True, permutation_id=None):
        """Add a profile.

        Args:
            profile_data (dict, optional): The profile data.
            profile_name (str, optional): The name of the profile.
            sort_keys (bool, optional): If True the keys will be sorted. Defaults to True.
            permutation_id (int, optional): The index of the permutation id. Defaults to None.
        """
        profile_data = profile_data or {}
        if profile_name is not None:
            # profile_name is only used for profile migrations
            self.name = profile_name

        # get input permutations when a permutation_id is passed
        input_permutations = None
        if permutation_id is not None:
            try:
                input_permutations = self.permutations.input_dict(permutation_id)
            except Exception:
                # catch any error
                print(f'{c.Fore.RED}Invalid permutation id provided.')
                sys.exit(1)

        # this should not hit since tctest also check for duplicates
        if os.path.isfile(self.filename):
            print(f'{c.Fore.RED}A profile with the name already exists.')
            sys.exit(1)

        profile = {
            'outputs': profile_data.get('outputs'),
            'stage': profile_data.get('stage', {'kvstore': {}}),
            'options': profile_data.get(
                'options',
                {
                    'autostage': {'enabled': False, 'only_inputs': None},
                    'session': {'enabled': False, 'blur': []},
                },
            ),
        }
        if self.ij.runtime_level.lower() in ['triggerservice', 'webhooktriggerservice']:
            profile['configs'] = [
                {
                    'trigger_id': str(randint(1000, 9999)),
                    'config': profile_data.get(
                        'inputs',
                        {
                            'optional': self.ij.params_to_args(
                                input_permutations=input_permutations,
                                required=False,
                                service_config=False,
                            ),
                            'required': self.ij.params_to_args(
                                input_permutations=input_permutations,
                                required=True,
                                service_config=False,
                            ),
                        },
                    ),
                }
            ]
        elif self.ij.runtime_level.lower() in ['organization', 'playbook']:
            profile['exit_codes'] = profile_data.get('exit_codes', [0])
            profile['exit_message'] = None
            profile['inputs'] = profile_data.get(
                'inputs',
                {
                    'optional': self.ij.params_to_args(
                        required=False, input_permutations=input_permutations
                    ),
                    'required': self.ij.params_to_args(
                        required=True, input_permutations=input_permutations
                    ),
                },
            )

        if self.ij.runtime_level.lower() == 'organization':
            profile['stage']['threatconnect'] = {}
            profile['validation_criteria'] = profile_data.get('validation_criteria', {'percent': 5})
            del profile['outputs']
        elif self.ij.runtime_level.lower() == 'triggerservice':
            profile['trigger'] = {}
        elif self.ij.runtime_level.lower() == 'webhooktriggerservice':
            profile['webhook_event'] = {
                'body': '',
                'headers': [],
                'method': 'GET',
                'query_params': [],
                'trigger_id': '',
            }

        with open(self.filename, 'w') as fh:
            json.dump(profile, fh, indent=2, sort_keys=sort_keys)

    def add_context(self, context):
        """Add a context to the context tracker for this profile.

        Args:
            context (str): The context (session_id) for this profile.
        """
        self._context_tracker.append(context)

    def clear_context(self, context):
        """Delete all context data in redis.

        Args:
            context (str): The context (session_id) to clear in KV store.
        """
        keys = self.redis_client.hkeys(context)
        if keys:
            return self.redis_client.hdel(context, *keys)
        return 0

    @property
    def context_tracker(self):
        """Return the current context trackers for Service Apps."""
        if not self._context_tracker:
            if self.tcex_testing_context:
                self._context_tracker = json.loads(
                    self.redis_client.hget(self.tcex_testing_context, '_context_tracker') or '[]'
                )
        return self._context_tracker

    @property
    def data(self):
        """Return the Data (dict) from the current profile."""
        if self._data is None:
            try:
                with open(self.filename, 'r') as fh:
                    self._data = json.load(fh)
            except OSError:
                print(f'{c.Fore.RED}Could not open profile {self.filename}.')

        if self._data:
            self._data['name'] = self.name
        return self._data

    @data.setter
    def data(self, profile_data):
        """Set profile_data dict."""
        self._data = profile_data

    def delete(self):
        """Delete an existing profile."""
        raise NotImplementedError('The delete method is not currently implemented.')

    @property
    def directory(self):
        """Return fully qualified profile directory."""
        return os.path.join(self._app_path, 'tests', self.feature, 'profiles.d')

    @property
    def feature(self):
        """Return the current feature."""
        if self._feature is None:
            # when called in testing framework will get the feature from pytest env var.
            self._feature = self._test_case_data[0].split('/')[1].replace('/', '-')
        return self._feature

    @property
    def feature_directory(self):
        """Return fully qualified feature directory."""
        return os.path.join(self._app_path, 'tests', self.feature)

    @property
    def filename(self):
        """Return profile fully qualified filename."""
        return os.path.join(self.directory, f'{self.name}.json')

    def init(self):
        """Return the Data (dict) from the current profile."""

        if self.data is None:
            self.log.error('Profile init failed; loaded profile data is None')

        # Now can initialize anything that needs initializing

        self.session_init()  # initialize session recording/playback

        if self.test_options:
            if self.test_options.get('autostage', False):
                self.init_autostage()

    def init_autostage(self):
        """Convert input arguments to staged data to automatically test playbook data propagation.

        The profile_data is checked for the key "auto_stage."

        auto_stage, if set, constrains the list of inputs that
        are converted to staged redis variables.  It will be the
        list of input names that will be staged.

        """
        profile_data = self.data
        auto_stage = profile_data.get('options', {}).get('autostage', {}).get('only_inputs')
        install_params = self.ij.contents

        # Scan for what inputs allow playbook data
        playbook_variables = {}
        for param in install_params.get('params', []):
            name = param.get('name')
            playbook_type = param.get('playbookDataType', None)
            playbook_variables[name] = playbook_type

        # make sure the staging area exists
        if 'stage' not in profile_data:
            profile_data['stage'] = {}
        if 'kvstore' not in profile_data['stage']:
            profile_data['stage']['kvstore'] = {}

        # First optional inputs
        inputs = profile_data.get('inputs', {})
        optionals = inputs.get('optional', {})
        requireds = inputs.get('required', {})

        for input_name, input_value in optionals.items():
            if input_name == 'tc_action':
                continue
            if isinstance(auto_stage, list) and input_name not in auto_stage:
                continue
            playbook_type = playbook_variables.get(input_name, None)
            if not playbook_type:
                continue
            if isinstance(input_value, str) and input_value in profile_data['stage']['kvstore']:
                continue  # this is already staged
            if isinstance(input_value, list):
                if 'StringArray' not in playbook_type:
                    continue
                type_name = 'StringArray'
            else:
                type_name = 'String'

            key = '#App:123:{}!{}'.format(input_name, type_name)

            profile_data['stage']['kvstore'][key] = input_value
            profile_data['inputs']['optional'][input_name] = key

        for input_name, input_value in requireds.items():
            if input_name == 'tc_action':
                continue
            if isinstance(auto_stage, list) and input_name not in auto_stage:
                continue
            playbook_type = playbook_variables.get(input_name, None)
            if not playbook_type:
                continue
            if isinstance(input_value, str) and input_value in profile_data['stage']['kvstore']:
                continue  # this is already staged
            if isinstance(input_value, list):
                if 'StringArray' not in playbook_type:
                    continue
                type_name = 'StringArray'
            else:
                type_name = 'String'

            key = '#App:123:{}!{}'.format(input_name, type_name)

            profile_data['stage']['kvstore'][key] = input_value
            profile_data['inputs']['required'][input_name] = key

    def migrate(self):
        """Migrate profile to latest schema and rewrite data."""

        # Short circuit migrations if the profile is newer than this code
        # Ideally, we'd put a migration stamp in the profile instead
        migration_mtime = os.stat(__file__).st_mtime
        migration_target = f'{tcex_version}.{migration_mtime}'

        with open(os.path.join(self.filename), 'r+') as fh:
            profile_data = json.load(fh)

            profile_version = profile_data.get('version', None)
            if not profile_version or profile_version < migration_target:
                profile_data['version'] = migration_target
            else:
                return profile_data  # profile is already migrated

            # migrate test options
            self.migrate_options(profile_data)

            # update all env variables to match latest pattern
            self.migrate_permutation_output_variables(profile_data)

            # update config section of profile for service Apps
            self.migrate_service_config_inputs(profile_data)

            # change for threatconnect staged data
            profile_data = self.migrate_stage_redis_name(profile_data)

            # change for threatconnect staged data
            profile_data = self.migrate_stage_threatconnect_data(profile_data)

            # update all version 1 env variables to match latest pattern
            profile_data = self.migrate_variable_pattern_env_v1(profile_data)

            # update all version 2 env variables to match latest pattern
            profile_data = self.migrate_variable_pattern_env_v2(profile_data)

            # update all tcenv variables to match latest pattern
            profile_data = self.migrate_variable_pattern_tcenv(profile_data)

            # write updated profile
            fh.seek(0)
            json.dump(profile_data, fh, indent=2, sort_keys=True)
            fh.write('\n')  # add required newline
            fh.truncate()

            # re-replace environment variables
            profile_data = self.replace_env_variables(profile_data)

            # replace all staged variable
            profile_data = self.replace_tc_variables(profile_data)

        return profile_data

    @staticmethod
    def migrate_permutation_output_variables(profile_data):
        """Remove permutation_output_variables field.

        Args:
            profile_data (dict): The profile data dict.

        Returns:
            dict: The updated dict.
        """
        try:
            del profile_data['permutation_output_variables']
        except KeyError:
            pass
        return profile_data

    def migrate_service_config_inputs(self, profile_data):
        """Change flat config inputs to include required/options.

        Args:
            profile_data (dict): The profile data dict.

        Returns:
            dict: The updated dict.
        """
        for configs in profile_data.get('configs', []):
            config = configs.get('config', {})

            # handle updated configs
            if config.get('optional') or config.get('required'):
                continue

            # new config schema
            config_inputs = {'optional': {}, 'required': {}}

            # iterate over defined inputs
            for k, v in config.items():
                input_data = self.ij.params_dict.get(k)
                if input_data is not None:
                    input_type = 'optional'
                    if input_data.get('required') is True:
                        input_type = 'required'

                # add value back with appropriate input type
                config_inputs[input_type][k] = v

            # overwrite flattened config
            configs['config'] = config_inputs
        return profile_data

    @staticmethod
    def migrate_options(profile_data):
        """Migrate profile to use options for tests"""

        # N.B. Profile data is passed by reference, so we can
        # modify it in place

        options = profile_data.get('options', {})
        autostage = options.get('autostage', {'enabled': False})
        autostage['only_inputs'] = autostage.get('only_inputs', None)

        session = options.get('session', {'enabled': False})
        session['blur'] = session.get('blur', [])

        options['autostage'] = autostage
        options['session'] = session

        profile_data['options'] = options

    @staticmethod
    def migrate_stage_redis_name(profile_data):
        """Update staged redis to kvstore

        This change updates the previous value of redis with a
        more generic value of kvstore for staged data.

        Args:
            profile_data (dict): The current profile data dict.

        Returns:
            dict: The update profile dict.
        """
        if profile_data.get('stage') is None:
            return profile_data

        kvstore_data = profile_data['stage'].get('redis', None)
        if kvstore_data is not None:
            del profile_data['stage']['redis']
            profile_data['stage']['kvstore'] = kvstore_data

        return profile_data

    @staticmethod
    def migrate_stage_threatconnect_data(profile_data):
        """Update for staged threatconnect data section of profile

        This change updates the previous list to a dict with a key that
        can be reference as a variable in other sections of the profile.

        Args:
            profile_data (dict): The current profile data dict.

        Returns:
            dict: The update profile dict.
        """
        if 'stage' not in profile_data:
            return profile_data

        stage_tc = profile_data.get('stage').get('threatconnect')

        # check if profile is using old list type
        if isinstance(stage_tc, list):
            profile_data['stage']['threatconnect'] = {}

            counter = 0
            for item in stage_tc:
                profile_data['stage']['threatconnect'][f'item_{counter}'] = item
                counter += 1

        return profile_data

    @staticmethod
    def migrate_variable_pattern_env_v1(profile_data):
        """Update the profile variable to latest pattern

        Args:
            profile_data (dict): The profile data dict.

        Returns:
            dict: The updated dict.
        """
        profile = json.dumps(profile_data)

        for m in re.finditer(r'\"\$(env|envs)\.(\w+)\"', profile):
            try:
                full_match = m.group(0)
                env_type = m.group(1)
                env_key = m.group(2)

                new_variable = f'"${{{env_type}:{env_key}}}"'
                profile = profile.replace(full_match, new_variable)
            except IndexError:
                print(f'{c.Fore.YELLOW}Invalid variable found {full_match}.')
        return json.loads(profile)

    @staticmethod
    def migrate_variable_pattern_env_v2(profile_data):
        """Update the profile variable to latest pattern

        Args:
            profile_data (dict): The profile data dict.

        Returns:
            dict: The updated dict.
        """
        profile = json.dumps(profile_data)

        for m in re.finditer(r'\${(env|envs|local|remote)\.(.*?)}', profile):
            try:
                full_match = m.group(0)
                env_type = m.group(1)  # currently env, envs, local, remote
                env_key = m.group(2)

                new_variable = f'${{{env_type}:{env_key}}}'
                profile = profile.replace(full_match, new_variable)
            except IndexError:
                print(f'{c.Fore.YELLOW}Invalid variable found {full_match}.')
        return json.loads(profile)

    def migrate_variable_pattern_tcenv(self, profile_data):
        """Update the profile variable to latest pattern

        Args:
            profile_data (dict): The profile data dict.

        Returns:
            dict: The updated dict.
        """
        if 'jmespath' not in sys.modules:
            print(
                f'{c.Fore.RED}Missing jmespath module. Try '
                f'installing "pip install tcex[development]"'
            )
            sys.exit(1)

        profile = json.dumps(profile_data)

        for data in self.tc_staged_data:
            key = data.get('key')
            for m in re.finditer(r'\${tcenv\.' + str(key) + r'\.(.*?)}', profile):
                try:
                    full_match = m.group(0)
                    jmespath_expression = m.group(1)

                    new_variable = f'${{tcenv:{key}:{jmespath_expression}}}'
                    profile = profile.replace(full_match, new_variable)
                except IndexError:
                    print(f'{c.Fore.YELLOW}Invalid variable found {full_match}.')
        return json.loads(profile)

    @property
    def name(self):
        """Return partially parsed test case data."""
        if self._name is None:
            name_pattern = r'^test_[a-zA-Z0-9_]+\[(.+)\]$'
            self._name = re.search(name_pattern, self._test_case_data[-1]).group(1)
        return self._name

    @name.setter
    def name(self, name):
        """Set the profile name"""
        self._name = name

    @staticmethod
    def output_data_rule(variable, data):
        """Return the default output data for a given variable"""
        output_data = {'expected_output': data, 'op': 'eq'}
        if variable.endswith('json.raw!String'):
            output_data['exclude'] = []
            output_data['op'] = 'jeq'
            output_data['ignore_order'] = False
        elif variable.endswith('web_link!String') or variable.endswith('web_link!StringArray'):
            output_data['op'] = 'is_url'
        elif variable.endswith('.id!String') or variable.endswith('.id!StringArray'):
            output_data['op'] = 'is_number'
        elif (
            variable.endswith('date_added!String')
            or variable.endswith('date_added!StringArray')
            or variable.endswith('last_modified!String')
            or variable.endswith('last_modified!StringArray')
        ):
            output_data['op'] = 'is_date'
        elif variable.endswith('StringArray'):
            output_data['op'] = 'dd'
            output_data['ignore_order'] = False
        elif variable.endswith('TCEntity') or variable.endswith('TCEntityArray'):
            output_data['exclude'] = ['id']
            output_data['op'] = 'jeq'
            output_data['ignore_order'] = False
        return output_data

    def profile_inputs(self):
        """Return the appropriate inputs (config) for the current App type.

        Service App use config and others use inputs.

        "inputs": {
            "optional": {}
            "required": {}
        }
        """
        if self.ij.runtime_level.lower() in ['triggerservice', 'webhooktriggerservice']:
            for config_data in self.configs:
                yield config_data.get('config')
        else:
            yield self.inputs

    @property
    def pytest_args(self):
        """Return dict of pytest config args."""
        if self._pytest_args is None:
            self._pytest_args = {}
            if self.pytestconfig:
                args = self.pytestconfig.option  # argparse.Namespace
                self._pytest_args = {
                    'merge_inputs': args.merge_inputs or False,
                    'merge_outputs': args.merge_outputs or False,
                    'replace_exit_message': args.replace_exit_message or False,
                    'replace_outputs': args.replace_outputs or False,
                    'record_session': args.record_session or False,
                    'ignore_session': args.ignore_session or False,
                    'enable_autostage': args.enable_autostage or False,
                    'disable_autostage': args.disable_autostage or False,
                }

        return self._pytest_args

    def replace_env_variables(self, profile_data):
        """Replace any env vars.

        Environment variables replaced follow the pattern ${type:name[=default]}
        e.g. ${env:FOO} or ${env:FOO=bla} to have a default value of "bla" if FOO
        is unset.

        A warning is raised when a substitution is called for, but the source
        does not contain a matching key.  A default value of '' will be used
        when this happens.

        An error is raised if the substitution would result in a new key
        pattern being formed, i.e. if the substitution text contains '${',
        which may or may not be expanded, or break profile expansion.

        Args:
            profile_data (dict): The profile data dict.

        Returns:
            dict: The updated dict.
        """
        profile = json.dumps(profile_data)

        for m in re.finditer(r'\${(env|envs|local|remote):(.*?)}', profile):
            try:
                full_match = m.group(0)
                env_type = m.group(1)  # currently env, envs, local, or remote
                env_key = m.group(2)
                if '=' in env_key:
                    env_key, default_value = env_key.split('=', 1)
                else:
                    default_value = None

                env_value = self.env_store.getenv(env_key, env_type, default_value)
                if env_value is not None:
                    if '${' in env_value:
                        self.log.error(
                            f'Profile replacement value for {full_match} includes '
                            f'recursive expansion {env_value}'
                        )
                    profile = profile.replace(full_match, env_value)
            except IndexError:
                print(f'{c.Fore.YELLOW}Could not replace variable {full_match}).')
        return json.loads(profile)

    def replace_tc_variables(self, profile_data):
        """Replace all of the TC output variables in the profile with their correct value.

        Args:
            profile_data (dict): The profile data dict.

        Returns:
            dict: The updated dict.
        """
        if 'jmespath' not in sys.modules:
            print(
                f'{c.Fore.RED}Missing jmespath module. Try '
                f'installing "pip install tcex[development]"'
            )
            sys.exit(1)

        profile = json.dumps(profile_data)

        for data in self.tc_staged_data:
            key = data.get('key')
            data_value = data.get('data')

            for m in re.finditer(r'\${tcenv:' + str(key) + r':(.*?)}', profile):
                try:
                    full_match = m.group(0)
                    jmespath_expression = m.group(1)

                    if jmespath_expression:
                        value = jmespath.search(jmespath_expression, data_value)
                        profile = profile.replace(full_match, str(value))
                except IndexError:
                    print(f'{c.Fore.YELLOW}Invalid variable found {full_match}.')
        return json.loads(profile)

    def session_init(self):
        """Initialize session recording/playback.

        Configured ON with the --record_session test flag, forcibly disabled with the
        --ignore_session test flag. The profile field options.session.enabled can be
        true to enable session recording/playback.

        The profile field options.session.blur may be a list of fields to blur to force
        matching (ie, date/times, passwords, etc)
        """
        ignore_session = self.pytest_args.get('ignore_session')
        record_session = self.pytest_args.get('record_session')

        if ignore_session:
            return

        session_options = self.options.get('session', {})
        session_enabled = session_options.get('enabled', False)

        if 'session' in self.stage and not session_enabled:
            session_enabled = True
            session_options['enabled'] = True
            self.options['session'] = session_options
            self.session_update_profile(force=True)  # add option to profile

        if record_session:
            session_enabled = True
            session_options['enabled'] = session_enabled
            self.options['session'] = session_options

            # save session data in stage.session
            self.stage['session'] = {'_record': True}

        if not session_enabled:
            return

        # if stage.session doesn't exist, but session_enabled is true, implicitly turn
        # on session recording (someone zapped the data out of the profile)
        if 'session' not in self.stage:
            session_data = {'_record': True}
            self.stage['session'] = session_data
        else:
            session_data = self.stage.get('session')

        blur = ['password']
        blur_options = session_options.get('blur', [])
        # if options.session.blur is not a list, make it a tuple
        if not isinstance(blur_options, list):
            blur_options = (blur_options,)
        blur.extend(blur_options)

        self.stage['session'] = session_data

        _request = getattr(Session, 'request')

        session_profile = self

        # Monkeypatch method for requests.sessions.Session.request
        def request(self, method, url, *args, **kwargs):
            """Intercept method for Session.request."""
            params = kwargs.get('params', {})
            parmlist = []
            params_keys = sorted(params.keys())
            for key in params_keys:
                if key in blur:
                    value = '***'
                else:
                    value = params.get(key)
                parmlist.append((key, value))

            # The key for this request e.g. GET https://... ('foo':'bla')
            request_key = f'{method} {url} {parmlist}'

            # if not recording, we must be playing back
            if not session_data.get('_record', False):
                result_data = session_data.get(request_key, None)
                if result_data is None:
                    raise KeyError('No stage.session value found for key {}'.format(request_key))
                return session_profile.session_unpickle_result(result_data)

            result = _request(self, method, url, *args, **kwargs)
            pickled_result = session_profile.session_pickle_result(result)
            session_data[request_key] = pickled_result
            return result

        # Add the intercept
        self.monkeypatch.setattr(Session, 'request', request)

    def session_pickle_result(self, result):  # pylint: disable=no-self-use
        """Pickled the result object so we can reconstruct it later"""
        return base64.b64encode(zlib.compress(pickle.dumps(result))).decode('utf-8')

    def session_unpickle_result(self, result):  # pylint: disable=no-self-use
        """Reverse the pickle operation"""
        return pickle.loads(zlib.decompress(base64.b64decode(result.encode('utf-8'))))

    def session_update_profile(self, force=False):
        """Write back the profile *if* we recorded session data"""
        stage = self.data.get('stage', {})
        session = stage.get('session', {})
        _record = session.get('_record', False)

        if not _record and not force:
            return

        if '_record' in session:
            del session['_record']  # don't record _record!

        with open(self.filename, 'r+') as fh:
            json_data = json.load(fh)

        json_data['stage']['session'] = session
        options = json_data.get('options', {})
        json_data['options'] = options
        options['session'] = self.data.get('options').get('session')

        self._write_file(json_data)

    @property
    def test_directory(self):
        """Return fully qualified test directory."""
        return os.path.join(self._app_path, 'tests')

    def test_permutations(self):
        """Return a list of (id, profile_name, test_options) for test permutations.

        Warning: the profile at this point is being called by conftest during test
        discovery. Most of the profile will NOT be set properly.
        """
        # Base response is an unadorned test
        response = [(self.name, self.name, {})]

        with open(self.filename, 'r') as fh:
            profile_data = json.load(fh)

        enable_autostage = self.pytest_args.get('enable_autostage')
        disable_autostage = self.pytest_args.get('disable_autostage')

        options = profile_data.get('options', {})
        if 'options' not in profile_data:
            profile_data['options'] = options
        autostage_options = options.get('autostage', {})
        if 'autostage' not in options:
            options['autostage'] = autostage_options

        # do a little dance to see if we should write back the profile
        autostage_enabled = autostage_options.get('enabled')

        if disable_autostage:
            autostage_options['enabled'] = False
        elif enable_autostage:
            autostage_options['enabled'] = True

        if autostage_enabled != autostage_options.get('enabled'):
            self._write_file(profile_data)

        # now just read the option, after possibly having
        # rewritten it
        autostage_enabled = autostage_options.get('enabled')

        # N.B. autostage could potentially add more runs, with additional
        # options, e.g. migrating '' to empty or Null or whatever
        if autostage_enabled:
            response.append([self.name + ':autostage', self.name, {'autostage': True}])

        return response

    def update_exit_message(self):
        """Update validation rules from exit_message section of profile."""
        message_tc = ''
        if os.path.isfile(self.message_tc_filename):
            with open(self.message_tc_filename, 'r') as mh:
                message_tc = mh.read()

        with open(self.filename, 'r+') as fh:
            profile_data = json.load(fh)

            if (
                profile_data.get('exit_message') is None
                or isinstance(profile_data.get('exit_message'), str)
                or self.pytest_args.get('replace_exit_message')
            ):
                # update the profile
                profile_data['exit_message'] = {'expected_output': message_tc, 'op': 'eq'}

                # write updated profile
                fh.seek(0)
                json.dump(profile_data, fh, indent=2, sort_keys=True)
                fh.write('\n')  # add required newline
                fh.truncate()

    def update_outputs(self):
        """Update the validation rules for outputs section of a profile.

        By default this method will only update if the current value is null. If the
        flag --replace_outputs is passed to pytest (e.g., pytest --replace_outputs)
        the outputs will replaced regardless of their current value. If the flag
        --merge_outputs is passed to pytest (e.g., pytest --merge_outputs) any new
        outputs will be added and any outputs that are not longer valid will be
        removed.
        """
        if self.redis_client is None:
            # redis_client is only available for children of TestCasePlaybookCommon
            print(f'{c.Fore.RED}An instance of redis_client is not set.')
            sys.exit(1)

        outputs = {}
        trigger_id = None
        for context in self.context_tracker:
            # get all current keys in current context
            redis_data = self.redis_client.hgetall(context)
            trigger_id = self.redis_client.hget(context, '_trigger_id')

            # updated outputs with validation data
            self.update_outputs_variables(outputs, redis_data, trigger_id)

            # cleanup redis
            self.clear_context(context)

        # TODO: move to teardown
        # Update any profile outputs
        self.session_update_profile()

        if self.outputs is None or self.pytest_args.get('replace_outputs'):
            # update profile if current profile is not or user specifies --replace_outputs
            with open(self.filename, 'r+') as fh:
                profile_data = json.load(fh)
                profile_data['outputs'] = outputs

                # write updated profile
                fh.seek(0)
                json.dump(profile_data, fh, indent=2, sort_keys=True)
                fh.write('\n')  # add required newline
                fh.truncate()
        elif self.pytest_args.get('merge_outputs'):
            if trigger_id is not None:
                # service Apps have a different structure with id: data
                merged_outputs = {}
                for id_, data in outputs.items():
                    merged_outputs[id_] = {}
                    for key in list(data):
                        if key in self.outputs.get(id_, {}):
                            # use current profile output value if exists
                            merged_outputs[id_][key] = self.outputs[id_][key]
                        else:
                            merged_outputs[id_][key] = outputs[id_][key]
            else:
                # update playbook App profile outputs
                merged_outputs = {}
                for key in list(outputs):
                    if key in self.outputs:
                        # use current profile output value if exists
                        merged_outputs[key] = self.outputs[key]
                    else:
                        merged_outputs[key] = outputs[key]

            # update profile outputs
            with open(self.filename, 'r+') as fh:
                profile_data = json.load(fh)
                profile_data['outputs'] = merged_outputs

                # write updated profile
                fh.seek(0)
                json.dump(profile_data, fh, indent=2, sort_keys=True)
                fh.write('\n')  # add required newline
                fh.truncate()

    def update_outputs_variables(self, outputs, redis_data, trigger_id):
        """Return the outputs section of a profile.

        Args:
            outputs (dict): The dict to add outputs.
            redis_data (dict): The data from KV store for this profile.
            trigger_id (str): The current trigger_id (service Apps).
        """
        for variable in self.tc_playbook_out_variables:
            # get data from redis for current context
            data = redis_data.get(variable.encode('utf-8'))

            # validate redis variables
            if data is None:
                if 1 not in self.exit_codes:
                    # TODO: add feature in testing framework to allow writing null and
                    #       then check if variables exist instead of null value.
                    # log error for missing output data if not a fail test case (exit code of 1)
                    self.log.debug(f'[{self.name}] Missing KV store output for variable {variable}')
            else:
                data = json.loads(data.decode('utf-8'))

            # validate validation variables
            validation_data = (self.outputs or {}).get(variable)
            if trigger_id is None and validation_data is None and self.outputs:
                self.log.error(f'[{self.name}] Missing validations rule: {variable}')

            # make business rules based on data type or content
            output_data = {'expected_output': data, 'op': 'eq'}
            if 1 not in self.exit_codes:
                output_data = self.output_data_rule(variable, data)

            # get trigger id for service Apps
            if trigger_id is not None:
                if isinstance(trigger_id, bytes):
                    trigger_id = trigger_id.decode('utf-8')
                outputs.setdefault(trigger_id, {})
                outputs[trigger_id][variable] = output_data
            else:
                outputs[variable] = output_data

    def validate_required_inputs(self):
        """Update interactive menu to build profile.

        This method will also merge input is --merge_inputs is passed to pytest.
        """

        errors = []
        status = True
        updated_params = []

        # handle non-layout and layout based App appropriately
        for profile_inputs in self.profile_inputs():  # dict with optional, required nested dicts
            profile_inputs_flattened = profile_inputs.get('optional', {})
            profile_inputs_flattened.update(profile_inputs.get('required', {}))

            params = self.ij.params_dict
            if self.lj.has_layout:
                # using inputs from layout.json since they are required to be in order
                # (display field can only use inputs previously defined)
                params = {}
                for name in self.lj.params_dict:
                    # get data from install.json based on name
                    params[name] = self.ij.params_dict.get(name)

                # hidden fields will not be in layout.json so they need to be include manually
                params.update(self.ij.filter_params_dict(hidden=True))

            inputs = {}
            merged_inputs = {
                'optional': {},
                'required': {},
            }
            for name, data, in params.items():
                if data.get('serviceConfig'):
                    # inputs that are serviceConfig are not applicable for profiles
                    continue

                if not data.get('hidden'):
                    # each non hidden input will be checked for permutations if the App has layout
                    if not self.permutations.validate_input_variable(name, inputs):
                        continue

                # get the value from the current profile
                value = profile_inputs_flattened.get(name)

                input_type = 'optional'
                if data.get('required'):
                    input_type = 'required'
                    if value in [None, '']:  # accept value of 0
                        # validation step
                        errors.append(f'- Missing/Invalid value for required arg ({name})')
                        status = False

                # update inputs
                inputs[name] = value
                merged_inputs[input_type][name] = value

            updated_params.append(merged_inputs)

        if self.pytest_args.get('merge_inputs'):
            # update profile outputs
            with open(self.filename, 'r+') as fh:
                profile_data = json.load(fh)
                if self.ij.runtime_level.lower() in ['triggerservice', 'webhooktriggerservice']:
                    for index, config_item in enumerate(profile_data.get('configs', [])):
                        config_item['config'] = updated_params[index]
                else:
                    profile_data['inputs'] = updated_params[0]

                # write updated profile
                fh.seek(0)
                json.dump(profile_data, fh, indent=2, sort_keys=True)
                fh.write('\n')  # add required newline
                fh.truncate()

        errors = '\n'.join(errors)  # convert error to string for assert message
        return status, f'\n{errors}'

    #
    # Properties
    #

    @property
    def args(self):
        """Return combined optional and required args."""
        args = self.inputs_optional
        args.update(self.inputs_required)
        return dict(args)

    @property
    def configs(self):
        """Return environments."""
        return list(self.data.get('configs', []))

    @property
    def environments(self):
        """Return environments."""
        return self.data.get('environments', ['build'])

    @property
    def exit_codes(self):
        """Return exit codes."""
        return self.data.get('exit_codes', [])

    @property
    def exit_message(self):
        """Return exit message dict."""
        return self.data.get('exit_message', {})

    @property
    def inputs(self):
        """Return inputs dict."""
        return self.data.get('inputs', {})

    @property
    def inputs_optional(self):
        """Return required inputs dict."""
        return self.inputs.get('optional', {})

    @property
    def inputs_required(self):
        """Return required inputs dict."""
        return self.inputs.get('required', {})

    @property
    def message_tc_filename(self):
        """Return the fqpn for message_tc file relative to profile."""
        return os.path.join(
            self._default_args.get('tc_out_path'), self.feature, self._test_case_name, 'message.tc'
        )

    @property
    def options(self):
        """Return options dict."""
        if self.data.get('options') is None:
            self.data['options'] = {}
        return self.data.get('options')

    @property
    def owner(self):
        """Return the owner value."""
        return (
            self.data.get('required', {}).get('owner')
            or self.data.get('optional', {}).get('owner')
            or self.data.get('owner')
        )

    @property
    def outputs(self):
        """Return outputs dict."""
        return self.data.get('outputs')

    @property
    def stage(self):
        """Return stage dict."""
        if self.data.get('stage') is None:
            self.data['stage'] = {}
        return self.data.get('stage', {})

    @property
    def stage_kvstore(self):
        """Return stage kv store dict."""
        return self.stage.get('kvstore', {})

    @property
    def stage_threatconnect(self):
        """Return stage threatconnect dict."""
        return self.stage.get('threatconnect', {})

    @property
    def tc_in_path(self):
        """Return fqpn tc_in_path arg relative to profile."""
        if self.ij.runtime_level.lower() in ['triggerservice', 'webhooktriggerservice']:
            tc_in_path = os.path.join(self._default_args.get('tc_in_path'), self.feature)
        else:
            tc_in_path = os.path.join(
                self._default_args.get('tc_in_path'), self.feature, self._test_case_name
            )
        return tc_in_path

    @property
    def tc_log_path(self):
        """Return fqpn tc_log_path arg relative to profile."""
        if self.ij.runtime_level.lower() in ['triggerservice', 'webhooktriggerservice']:
            tc_log_path = os.path.join(self._default_args.get('tc_log_path'), self.feature)
        else:
            tc_log_path = os.path.join(
                self._default_args.get('tc_log_path'), self.feature, self._test_case_name
            )
        return tc_log_path

    @property
    def tc_out_path(self):
        """Return fqpn tc_out_path arg relative to profile."""
        if self.ij.runtime_level.lower() in ['triggerservice', 'webhooktriggerservice']:
            tc_out_path = os.path.join(self._default_args.get('tc_out_path'), self.feature)
        else:
            tc_out_path = os.path.join(
                self._default_args.get('tc_out_path'), self.feature, self._test_case_name
            )
        return tc_out_path

    @property
    def tc_playbook_out_variables(self):
        """Return calculated output variables.

        * iterate over all inputs:
          * if input key has exposePlaybookKeyAs defined
          * if value a variable
            * lookup value in stage.kvstore data
            * for each key add to output variables
        """
        output_variables = self.ij.tc_playbook_out_variables
        if self.lj.has_layout:
            # if layout based App get valid outputs
            output_variables = self.ij.create_output_variables(
                self.permutations.outputs_by_inputs(self.args)
            )

        for arg, value in self.args.items():
            # get full input data from install.json
            input_data = self.ij.params_dict.get(arg, {})

            # check to see if it support dynamic output variables
            if 'exposePlaybookKeyAs' not in input_data:
                continue

            # get the output variable type from install.json input data
            variable_type = input_data.get('exposePlaybookKeyAs')

            # staged data for this dynamic input must be a KeyValueArray
            for data in self.stage_kvstore.get(value, []):
                # create a variable using key value
                variable = self.ij.create_variable(data.get('key'), variable_type, job_id=9876)
                output_variables.append(variable)

        return output_variables

    @property
    def tc_temp_path(self):
        """Return fqpn tc_temp_path arg relative to profile."""
        if self.ij.runtime_level.lower() in ['triggerservice', 'webhooktriggerservice']:
            tc_temp_path = os.path.join(self._default_args.get('tc_temp_path'), self.feature)
        else:
            tc_temp_path = os.path.join(
                self._default_args.get('tc_temp_path'), self.feature, self._test_case_name
            )
        return tc_temp_path

    @property
    def validation_criteria(self):
        """Return the validation_criteria value."""
        return self.data.get('validation_criteria', {})

    @property
    def webhook_event(self):
        """Return webhook event dict."""
        return self.data.get('webhook_event', {})


class ProfileInteractive:
    """Testing Profile Interactive Class.

    Args:
        profile (Profile): The profile object to build interactive inputs.
    """

    def __init__(self, profile):
        """Initialize Class properties."""
        self.profile = profile

        # properties
        self._inputs = {
            'optional': {},
            'required': {},
        }
        self._staging_data = {'kvstore': {}}
        self._user_defaults = None
        self.exit_codes = []
        self.input_type_map = {
            'boolean': self.present_boolean,
            'choice': self.present_choice,
            'keyvaluelist': self.present_key_value_list,
            'multichoice': self.present_multichoice,
            'string': self.present_string,
        }
        self.utils = Utils()
        self.user_defaults_filename = os.path.join('tests', '.user_defaults')

    def _default(self, data):
        """Return the best option for default."""
        if data.get('type').lower() == 'boolean':
            default = str(data.get('default', 'false')).lower()
        elif data.get('type').lower() == 'choice':
            default = 0
            valid_values = self.profile.ij.expand_valid_values(data.get('validValues', []))
            if data.get('name') == 'tc_action':
                for vv in valid_values:
                    if self.profile.feature.lower() == vv.replace(' ', '_').lower():
                        default = vv
                        break
            else:
                default = data.get('default')
        elif data.get('type').lower() == 'multichoice':
            default = data.get('default')
            if default is not None and isinstance(default, str):
                default = default.split('|')
        else:
            default = data.get('default')
            if default is None:
                default = self.user_defaults.get(data.get('name'))
        return default

    @staticmethod
    def _split_list(data):
        """Split a list in two "equal" parts."""
        half = math.ceil(len(data) / 2)
        return data[:half], data[half:]

    def add_input(self, name, data, value):
        """Add an input to inputs."""
        if data.get('required', False):
            self._inputs['required'].setdefault(name, value)
        else:
            self._inputs['optional'].setdefault(name, value)

    @staticmethod
    def choice(option_text):
        """Return the input choice string."""
        return f'{c.Fore.MAGENTA}Choice{c.Fore.RESET}{c.Style.BRIGHT}{option_text}: '

    @property
    def inputs(self):
        """Return inputs dict."""
        return self._inputs

    def present(self):
        """Present interactive menu to build profile."""

        def params_data():
            # handle non-layout and layout based App appropriately
            if self.profile.lj.has_layout:
                # using inputs from layout.json since they are required to be in order
                # (display field can only use inputs previously defined)
                for name in self.profile.lj.params_dict:
                    # get data from install.json based on name
                    data = self.profile.ij.params_dict.get(name)
                    yield name, data

                # hidden fields will not be in layout.json so they need to be include manually
                for name, data in self.profile.ij.filter_params_dict(hidden=True).items():
                    yield name, data
            else:
                for name, data in self.profile.ij.params_dict.items():
                    yield name, data

        inputs = {}
        for name, data in params_data():
            if data.get('serviceConfig'):
                # inputs that are serviceConfig are not applicable for profiles
                continue

            if not data.get('hidden'):
                # each input will be checked for permutations if the App has layout and not hidden
                if not self.profile.permutations.validate_input_variable(name, inputs):
                    continue

            # present the input
            value = self.input_type_map.get(data.get('type').lower())(name, data)

            # update inputs
            inputs[name] = value

        self.present_exit_code()

    def present_exit_code(self):
        """Provide user input for exit code."""

        self.print_header({'label': 'Exit Codes'})
        values = input(self.choice(' [0]')).strip().split(',')

        # add input
        for e in values:
            e = e or 0
            try:
                self.exit_codes.append(int(e))
            except ValueError:
                print(f'{c.Fore.RED}Please provide a integer between 0-3.')
                sys.exit(1)

        # user feedback
        self.print_feedback(self.exit_codes)

    def present_boolean(self, name, data):
        """Build a question for boolean input."""
        default = self._default(data)
        valid_values = ['true', 'false']

        option_default = 'false'
        option_text = ''
        options = []
        for v in valid_values:
            if v.lower() == default.lower():
                option_default = v
                v = f'[{v}]'
            options.append(v)
        option_text = f" {'/'.join(options)}"

        self.print_header(data)
        value = input(self.choice(option_text)).strip()
        if not value:
            value = option_default
        value = self.utils.to_bool(value)

        # user feedback
        self.print_feedback(value)

        # add input
        self.add_input(name, data, value)

        return value

    def present_choice(self, name, data):
        """Build a question for choice input."""
        default = self._default(data)
        valid_values = self.profile.ij.expand_valid_values(data.get('validValues', []))

        # default value needs to be converted to index
        option_index = 0
        if default:
            try:
                option_index = valid_values.index(default)
            except ValueError:
                # if "magic" variable (e.g., ${GROUP_TYPES}) was not expanded then use index 0.
                # there is no way to tell if the default value is be part of the expansion.
                if any([re.match(r'^\${.*}$', v) for v in valid_values]):
                    option_index = 0
                else:
                    print(
                        f'''{c.Fore.RED}Invalid value of ({default}) for {data.get('name')}, '''
                        'check that default value and validValues match in install.json.'
                    )
                    sys.exit()
        option_text = f' [{option_index}]'

        # build options list to display to the user in two columns
        options = []
        for i, v in enumerate(valid_values):
            options.append(f'{i}. {v}')

        # print header information
        self.print_header(data)

        # display options list into two columns
        left, right = self._split_list(options)
        for i, _ in enumerate(left):
            ld = left[i]
            try:
                rd = right[i]
            except IndexError:
                rd = ''
            print(f'{ld:40} {rd:40}')

        # collect user input an process accordingly
        value = input(self.choice(option_text)).strip()
        if not value:
            value = option_index
        else:
            try:
                value = int(value)
            except ValueError:
                print(f'{c.Fore.RED}Please provide a integer between 0-{len(valid_values) - 1}.')
                sys.exit(1)

        # get value from valid value index
        if valid_values:
            try:
                value = valid_values[value]
            except IndexError:
                print(
                    f'{c.Fore.RED}Invalid value of {value} provided. '
                    f'Please provide a integer between 0-{len(valid_values) - 1}'
                )
                sys.exit(1)

        # user feedback
        self.print_feedback(value)

        # add input
        self.add_input(name, data, value)

        return value

    def present_key_value_list(self, name, data):
        """Build a question for key value list input."""
        # add input
        variable = self.profile.ij.create_variable(data.get('name'), 'KeyValueArray')
        self.add_input(name, data, variable)
        self._staging_data['kvstore'].setdefault(
            variable, [{'key': 'placeholder', 'value': 'placeholder'}]
        )

        return variable

    def present_multichoice(self, name, data):
        """Build a question for choice input."""
        default = self._default(data)  # array of default values
        valid_values = self.profile.ij.expand_valid_values(data.get('validValues', []))

        # default values will be return as an array (e.g., one|two -> ['one'. 'two']).
        # using the valid values array we can look up these values to show as default in input.
        option_indexes = [0]
        if default:
            option_indexes = []
            for d in default:
                try:
                    option_indexes.append(valid_values.index(d))
                except ValueError:
                    # if "magic" variable (e.g., ${GROUP_TYPES}) was not expanded then skip value.
                    # there is no way to tell if the default value is be part of the expansion.
                    if any([re.match(r'^\${.*}$', v) for v in valid_values]):
                        continue

                    print(
                        f'''{c.Fore.RED}Invalid value of ({d}) for {data.get('name')}, check '''
                        'that default value(s) and validValues match in install.json.'
                    )
                    sys.exit()
        option_text = f''' [{','.join([str(v) for v in option_indexes])}]'''

        # build options list to display to the user in two columns
        options = []
        for i, v in enumerate(valid_values):
            options.append(f'{i}. {v}')

        # print header information
        self.print_header(data)

        # display options list into two columns
        left, right = self._split_list(options)
        for i, _ in enumerate(left):
            ld = left[i]
            try:
                rd = right[i]
            except IndexError:
                rd = ''
            print(f'{ld:40} {rd:40}')

        # collect user input an process accordingly
        value = input(self.choice(option_text)).strip()
        if not value:
            # use default values from option_index if no value is provided
            value = option_indexes
        else:
            # parse values into index position based on valid value presented to the user
            try:
                value = [int(i) for i in value.strip().split(',')]
            except ValueError:
                print(
                    f'{c.Fore.RED}Please provide one or more integers between '
                    f'0-{len(valid_values) - 1} separated by commas.'
                )
                sys.exit(1)

        # get value from valid value index
        values = []
        for index in value:
            try:
                values.append(valid_values[index])
            except IndexError:
                print(
                    f'{c.Fore.RED}Invalid value of {index} provided. '
                    f'Please provide one or more integers between 0-{len(valid_values) - 1} '
                    'separated by commas.'
                )
                sys.exit(1)

        # values stored in profile are pipe delimeted for multichoice
        delimited_values = '|'.join(values)

        # user feedback
        self.print_feedback(delimited_values)

        # add input
        self.add_input(name, data, delimited_values)

        return delimited_values

    def present_string(self, name, data):
        """Build a question for boolean input."""
        default = self._default(data)  # the default value from install.json or other

        option_text = ''
        if default is not None:
            option_text = f' [{default}]'

        self.print_header(data)
        value = input(self.choice(option_text)).strip()
        if not value:
            value = default

        feedback_value = value
        input_value = value

        # allow a null input
        if input_value == 'null':
            input_value = None
        elif input_value in ['"null"', "'null'"]:
            input_value = 'null'

        # for non-service Apps replace user input with a variable and add to staging data
        if (
            self.profile.ij.runtime_level.lower() not in ['triggerservice', 'webhooktriggerservice']
            and 'String' in data.get('playbookDataType', [])
            and not os.getenv('TCEX_NO_PROFILE_VARIABLE')
        ):  # only stage String Type
            # create variable and staging data
            variable = self.profile.ij.create_variable(data.get('name'), data.get('type'))
            self._staging_data['kvstore'].setdefault(variable, input_value)
            feedback_value = f'"{value}" - ({variable})'
            input_value = variable

        # user feedback
        self.print_feedback(feedback_value)

        # add input
        self.add_input(name, data, input_value)

        # update default
        if default is None:
            self.user_defaults[name] = value

        return value

    @staticmethod
    def print_feedback(feedback_value):
        """Print the value used."""
        print(f'Using value: {c.Fore.GREEN}{feedback_value}\n')

    @staticmethod
    def print_header(data):
        """Enrich the header with metadata."""

        def _print_metadata(title, value):
            """Print the title and value"""
            print(f'{c.Fore.CYAN}{title!s:<22}: {c.Fore.RESET}{c.Style.BRIGHT}{value}')

        label = data.get('label', 'NO LABEL')
        print(f'\n{c.Fore.GREEN}{label}')

        # type
        _print_metadata('Type', data.get('type'))

        # note
        note = data.get('note', '')[:200]
        if note:
            _print_metadata('Note', note)

        # required
        _print_metadata('Required', str(data.get('required', False)).lower())

        # hidden
        if data.get('hidden'):
            _print_metadata('Hidden', 'true')

        # Input Types
        pbt = ','.join(data.get('playbookDataType', []))
        if pbt:
            _print_metadata('Playbook Data Types', pbt)

        vv = ','.join(data.get('validValues', []))
        if vv:
            _print_metadata('Valid Values', vv)

        print('-' * 50)

    @property
    def staging_data(self):
        """Return staging data dict."""
        return self._staging_data

    @property
    def user_defaults(self):
        """Return user defaults"""
        if self._user_defaults is None:
            self._user_defaults = {}
            if os.path.isfile(self.user_defaults_filename):
                with open(self.user_defaults_filename, 'r') as fh:
                    self._user_defaults = json.load(fh)
        return self._user_defaults