"""TcEx testing profile Class."""
# standard library
import json
import math

# import os
import re
import sys
from base64 import b64encode
from typing import Optional, Union

# third-party
import colorama as c

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


class Interactive:
    """Testing Profile Interactive Class."""

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

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

        # properties
        self._inputs = {
            'optional': {},
            'required': {},
        }
        self._no_selection_text = 'No Selection'
        self._staging_data = {'kvstore': {}}
        # self._user_defaults = None
        self.collect_type_map = {
            'Any': self.collect_string,
            'Binary': self.collect_binary,
            'BinaryArray': self.collect_binary_array,
            'KeyValue': self.collect_key_value,
            'KeyValueArray': self.collect_key_value_array,
            'String': self.collect_string,
            'StringArray': self.collect_string_array,
            'TCEntity': self.collect_tcentity,
            'TCEntityArray': self.collect_tcentity_array,
        }
        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.user_defaults_filename = os.path.join('tests', '.user_defaults')

    def _default(self, data: dict) -> Union[list, str]:  # pylint: disable=unused-argument
        """Return the best option for default.

        Args:
            data: The install.json params object.

        Returns:
            list, str: The default value for the input.
        """
        if data.get('type').lower() == 'boolean':
            default = str(data.get('default', 'false')).lower()
        elif data.get('type').lower() == 'choice':
            default = 0
            valid_values: list = self._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: str = data.get('default')
        elif data.get('type').lower() == 'multichoice':
            default: str = data.get('default')
            if default is not None and isinstance(default, str):
                default: list = default.split('|')
        else:
            default = data.get('default')
            # if default is None:
            #     # set default from user default file
            #     default = self.user_defaults.get(data.get('name'))
        return default

    def _expand_valid_values(self, valid_values: list) -> list:
        """Expand supported playbook variables to their full list.

        Args:
            valid_values (list): The list of valid values for Choice or MultiChoice inputs.

        Returns:
            list: An expanded list of valid values for Choice or MultiChoice inputs.
        """
        valid_values = list(valid_values)
        if '${ARTIFACT_TYPES}' in valid_values:
            valid_values.remove('${ARTIFACT_TYPES}')
            valid_values.extend(
                [
                    'ASN',
                    'Asset Group ID',
                    'Certificate File',
                    'CIDR',
                    'Credential ID',
                    'Document Metadata',
                    'Email Address',
                    'Email Attachment File',
                    'Email Attachment File Name',
                    'Email Body',
                    'Email Message File',
                    'Email Subject',
                    'Event File',
                    'Exploit ID',
                    'File Hash',
                    'Filter ID',
                    'Hashtag',
                    'Host',
                    'Image File',
                    'IP Address',
                    'Log File',
                    'MutEx',
                    'PCAP File',
                    'Policy ID',
                    'Registry Key',
                    'Results ID',
                    'Screenshot File',
                    'Tactic ID',
                    'Technique ID',
                    'Ticket ID',
                    'Timestamp',
                    'URL',
                    'User Agent',
                    'Vulnerability Detection ID',
                    'Vulnerability ID',
                ]
            )
        elif '${GROUP_TYPES}' in valid_values:
            valid_values.remove('${GROUP_TYPES}')
            valid_values.extend(
                [
                    'Adversary',
                    'Campaign',
                    'Document',
                    'Email',
                    'Event',
                    'Incident',
                    'Intrusion Set',
                    'Signature',
                    'Task',
                    'Threat',
                ]
            )
        elif '${INDICATOR_TYPES}' in valid_values:
            valid_values.remove('${INDICATOR_TYPES}')
            r = self.profile.session.get('/v2/types/indicatorTypes')
            if r.ok:
                valid_values.extend(
                    [t.get('name') for t in r.json().get('data', {}).get('indicatorType', {})]
                )
        elif '${OWNERS}' in valid_values:
            valid_values.remove('${OWNERS}')
            r = self.profile.session.get('/v2/owners')
            if r.ok:
                valid_values.extend(
                    [o.get('name') for o in r.json().get('data', {}).get('owner', {})]
                )
        elif '${USERS}' in valid_values:
            valid_values.remove('${USERS}')
            r = self.profile.session.get('/v2/owners/mine/members')
            if r.ok:
                valid_values.extend(
                    [o.get('userName') for o in r.json().get('data', {}).get('user', {})]
                )
        elif '${USER_GROUPS}' in valid_values:
            valid_values.remove('${USER_GROUPS}')
            valid_values.extend(['User Group 1', 'User Group 1'])

        return valid_values

    def _input_value(self, label: str, option_text: Optional[str] = None) -> str:
        """Return user input.

        Args:
            label: The label to display to the user.
            option_text: the Option text to display to the user.

        Returns:
            str: The value selected by the user.
        """
        # update option text to include help message
        option_text = option_text or ''
        if option_text:
            # add space for cleaness in user display
            option_text = f' {option_text}'

        print(f'{c.Fore.WHITE}[? for help]')
        prompt = f'{c.Fore.MAGENTA}{label}{c.Fore.RESET}{c.Style.BRIGHT}{option_text}: '
        input_value = input(prompt).strip()  # nosec

        # handle special user inputs
        if input_value == '?':
            self.present_help()
            return self._input_value(label, option_text)

        return input_value

    @staticmethod
    def _split_list(data: list) -> tuple:
        """Split a list in two "equal" parts.

        Args:
            data: The list of data to split into two equal parts.

        Returns:
            tuple: The two halves of the list.
        """
        half: int = math.ceil(len(data) / 2)
        return data[:half], data[half:]

    def add_input(self, name: str, data: dict, value: str) -> None:
        """Add an input to inputs.

        Args:
            name: The name of the input.
            data: The install.json params object.
            value: The value for the input.
        """
        if data.get('required', False):
            self._inputs['required'].setdefault(name, value)
        else:
            self._inputs['optional'].setdefault(name, value)

    # def add_user_default(self, key, value, data_type=None):
    #     """Add data to user default."""
    #     self.user_defaults.setdefault(self.profile.feature, {})
    #     if data_type is None:
    #         self.user_defaults[self.profile.feature][key] = value
    #     else:
    #         # store the value under the appropriate data type
    #         self.user_defaults[self.profile.feature].setdefault(key, {})
    #         self.user_defaults[self.profile.feature][key].setdefault(data_type, value)

    #     if self.user_defaults.get('base') is None:
    #         self.user_defaults['base'] = self.user_defaults[self.profile.feature]

    def add_staging_data(self, name: str, type_: str, value: str) -> str:
        """Create staging data and return variable value.

        Args:
            name: The name of the input.
            type_: The type of input (Binary, StringArray, etc.)
            value: The value to write in the staging data.

        Returns:
            str: The newly create variable string.
        """
        arg_value = value
        if (
            self.profile.ij.runtime_level.lower() not in ['triggerservice', 'webhooktriggerservice']
            and value is not None
        ):
            arg_value: str = self.profile.ij.create_variable(name, type_)
            self._staging_data['kvstore'].setdefault(arg_value, value)

        return arg_value

    def collect_binary(self, **kwargs) -> str:
        """Collect binary data

        Args:
            default (str, kwargs): The default value if no value provided by user.
            feedback (bool, kwargs): If True user feedback will be printed.
            option_text (str, kwargs): The text shown to the user.
            required (str, kwargs): If True the user cannot continue until they provide a value.

        Returns:
            str: The input str from the user.
        """
        input_value: str = self._input_value('Input', kwargs.get('option_text'))
        if not input_value:
            # if no default value and required force user to input again
            if kwargs.get('default') is None and kwargs.get('required') is True:
                self.print_required()
                return self.collect_binary(**kwargs)

        if input_value not in [None, '']:
            input_data: str = b64encode(input_value.encode()).decode()
            feedback = f'{input_value} -> ({input_data})'
        else:
            input_data: str = kwargs.get('default')
            feedback = input_data

        # print user feedback
        if kwargs.get('feedback', True):
            self.print_feedback(feedback)

        return input_data

    def collect_binary_array(self, **kwargs) -> list:
        """Collect binary array data

        Args:
            required (str, kwargs): If True the user cannot continue until they provide a value.

        Returns:
            list: The input list from the user.
        """
        input_values = []
        required = kwargs.get('required', False)
        while True:
            input_value = self.collect_binary(feedback=False, required=required)
            if not input_value:
                break
            input_values.append(input_value)
            required = False  # only the first input is required

        if not input_values:
            # return None to ensure data doesn't get added to inputs
            input_values = None

        # print user feedback
        self.print_feedback(input_values)

        return input_values

    def collect_boolean(self, **kwargs) -> bool:
        """Collect binary data

        Args:
            default (str, kwargs): The default value if no value provided by user.
            option_text (str, kwargs): The text shown to the user.

        Returns:
            bool: The boolean value select by the user.
        """
        input_value = self._input_value('Input', kwargs.get('option_text'))
        if input_value == '':
            input_value = kwargs.get('default')

        if str(input_value).lower() not in ['0', 'f', 'false', '1', 't', 'true']:
            self.print_invalid_bool()
            return self.collect_boolean(**kwargs)

        # covert input value to a proper boolean
        input_value = self.profile.utils.to_bool(input_value)

        # print user feedback
        self.print_feedback(input_value)

        return input_value

    def collect_choice(self, **kwargs) -> str:
        """Collect choice data

        Args:
            default (str, kwargs): The default value if no value provided by user.
            option_text (str, kwargs): The text shown to the user.
            required (str, kwargs): If True the user cannot continue until they provide a value.
            valid_values (str, kwargs): A list of valid values

        Returns:
            str: The users selected choice.
        """
        # collect input value from user and set default if required
        input_value: str = self._input_value('Choice', kwargs.get('option_text')) or kwargs.get(
            'default'
        )

        # ensure input value is provided when input is required
        if input_value is None and kwargs.get('required') is True:
            self.print_required()
            return self.collect_choice(**kwargs)

        # if input value is None then there is not need to continue
        if input_value is None:
            return input_value

        # set valid values
        valid_values: list = kwargs.get('valid_values', [])

        # convert to int or recollect input
        try:
            input_value = int(input_value)
        except ValueError:
            self.print_invalid_index(f'0-{len(valid_values)}')
            return self.collect_choice(**kwargs)

        # ensure input value is valid
        valid_index_values = [i for i, _ in enumerate(valid_values)]
        # valid_index_values = list(range(0, len(valid_values) - 1))
        if input_value not in valid_index_values:
            self.print_invalid_index(f'0-{len(valid_values)}')
            return self.collect_choice(**kwargs)

        # using index value provided by user, set value to valid value
        input_value = valid_values[input_value]
        if input_value == self._no_selection_text:
            # special case for when user select no selection
            input_value = None

        # print user feedback
        if kwargs.get('feedback', True):
            self.print_feedback(input_value)

        return input_value

    def collect_exit_code(self, **kwargs) -> int:
        """Collect exit codes.

        Args:
            option_text (str, kwargs): The text shown to the user.

        Returns:
            str: The users provided exit code.
        """
        input_value = self._input_value('Code', kwargs.get('option_text'))

        if input_value != '':
            try:
                input_value = int(input_value)
            except ValueError:
                self.print_invalid_exit_code()
                return self.collect_exit_code(**kwargs)

            if input_value not in [0, 1, 3]:
                self.print_invalid_exit_code()
                return self.collect_exit_code(**kwargs)

        return input_value

    def collect_exit_codes(self, **kwargs) -> list:
        """Collect exit codes.

        Returns:
            list: The users provided exit code.
        """
        input_values = []
        while True:
            input_value = self.collect_exit_code(**kwargs)
            if input_value == '':
                break
            input_values.append(input_value)

        if not input_values:
            # return None to ensure data doesn't get added to inputs
            input_values = [0]

        # print user feedback
        self.print_feedback(input_values)

        return input_values

    def collect_key_value(self, **kwargs) -> dict:
        """Collect key value data.

        Args:
            option_text (str, kwargs): The text shown to the user.
            required (str, kwargs): If True the user cannot continue until they provide a value.

        Returns:
            dict: The users provided key value input.
        """
        input_value = None
        key = self._input_value('Key', option_text=kwargs.get('option_text'))

        # ensure input value is provided when input is required
        if key == '' and kwargs.get('required') is True:
            self.print_required()
            return self.collect_key_value(**kwargs)

        if key != '':
            value = self._input_value('Value')
            input_value = {'key': key, 'value': value}
        else:
            input_value = kwargs.get('default')

        # print user feedback
        if kwargs.get('feedback', True):
            self.print_feedback(input_value)

        return input_value

    def collect_key_value_array(self, **kwargs) -> list:
        """Collect key value array data

        Args:
            default (str, kwargs): The default value if no value provided by user.
            option_text (str, kwargs): The text shown to the user.
            required (str, kwargs): If True the user cannot continue until they provide a value.

        Returns:
            list: The users provided list of key value inputs.
        """
        input_values = []
        required: bool = kwargs.get('required')
        while True:
            input_value = self.collect_key_value(
                default=kwargs.get('default'),
                feedback=False,
                option_test=kwargs.get('option_text'),
                required=required,
            )
            if not input_value:
                break
            input_values.append(input_value)
            required = False

        if not input_values:
            # return None to ensure data doesn't get added to inputs
            input_values = None

        # print user feedback
        self.print_feedback(input_values)

        return input_values

    def collect_multichoice(self, **kwargs) -> list:
        """Collect multichoice data

        Args:
            required (str, kwargs): If True the user cannot continue until they provide a value.
            valid_values (str, kwargs): A list of valid values

        Returns:
            list: The users provided list of choice inputs.
        """
        input_values = []
        required = kwargs.get('required', False)
        while True:
            input_value = self.collect_choice(
                feedback=False,
                # option_text=kwargs.get('option_text'),
                required=required,
                valid_values=kwargs.get('valid_values'),
            )
            if not input_value:
                break
            input_values.append(input_value)
            required = False

        input_values = list(set(input_values))
        if input_values:
            # format multichoice value as pipe delimited string
            input_values = '|'.join(input_values)
        else:
            # return None to ensure data doesn't get added to inputs
            input_values = None

        # print user feedback
        self.print_feedback(input_values)

        return input_values

    def collect_string(self, **kwargs) -> str:
        """Collect string data

        Args:
            option_text (str, kwargs): The text shown to the user.
            default (str, kwargs): The default value if no value provided by user.

        Returns:
            str: The user provided input.
        """
        input_value = self._input_value('Input', kwargs.get('option_text', ''))
        if not input_value:
            input_value = kwargs.get('default')

        if input_value is None and kwargs.get('required', False) is True:
            self.print_required()
            return self.collect_string(**kwargs)

        # print user feedback
        if kwargs.get('feedback', True):
            self.print_feedback(input_value)

        # APP-622 - handle null/None values
        if input_value == 'null':
            input_value = None
        elif input_value in ['"null"', "'null'"]:
            input_value = 'null'

        return input_value

    def collect_string_array(self, **kwargs) -> list:
        """Collect string data

        Args:
            required (str, kwargs): If True the user cannot continue until they provide a value.

        Returns:
            str: The user provided input.
        """
        input_values = []
        required = kwargs.get('required', False)
        while True:
            input_value = self.collect_string(feedback=False, required=required)
            if not input_value:
                break
            input_values.append(input_value)
            required = False

        if not input_values:
            # return None to ensure data doesn't get added to inputs
            input_values = None

        # print user feedback
        self.print_feedback(input_values)

        return input_values

    def collect_tcentity(self, **kwargs) -> dict:
        """Collect tcentity data

        Args:
            required (str, kwargs): If True the user cannot continue until they provide a value.

        Returns:
            str: The user provided input.
        """
        input_value = None
        id_ = self._input_value('ID')
        if id_:
            value = self._input_value('Value')
            type_ = self._input_value('Type')
            input_value = {'id': id_, 'value': value, 'type': type_}

        if input_value is None and kwargs.get('required', False) is True:
            self.print_required()
            return self.collect_tcentity(**kwargs)

        # print user feedback
        if kwargs.get('feedback', True):
            self.print_feedback(input_value)

        return input_value

    def collect_tcentity_array(self, **kwargs) -> list:
        """Collect tcentity array data

        Args:
            required (str, kwargs): If True the user cannot continue until they provide a value.

        Returns:
            list: The user provided inputs.
        """
        input_values = []
        required = kwargs.get('required', False)
        while True:
            input_value = self.collect_tcentity(feedback=False, required=required)
            if not input_value:
                break
            input_values.append(input_value)
            required = False

        if not input_values:
            # return None to ensure data doesn't get added to inputs
            input_values = None

        # print user feedback
        self.print_feedback(input_values)

        return input_values

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

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

        def params_data() -> tuple:
            # 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: str = self.input_type_map.get(data.get('type').lower())(name, data)

            # update inputs
            inputs[name] = value

        self.present_exit_code()

    def present_boolean(self, name: str, data) -> bool:
        """Build a question for boolean input.

        Args:
            name: The name of the input field.
            data: The install.json input param object.

        Returns:
            bool: The user provided input.
        """
        # print header information
        self.print_header(data)

        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)})'''

        value = self.collect_boolean(default=option_default, option_text=option_text)

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

        return value

    def present_choice(self, name: str, data: dict) -> str:
        """Build a question for choice input.

        Args:
            name: The name of the input field.
            data: The install.json input param object.

        Returns:
            str: The user provided input.
        """
        # print header information
        self.print_header(data)

        default = self._default(data)
        option_index = 0
        valid_values = self._expand_valid_values(data.get('validValues', []))
        if data.get('required', False) is False:
            # add option to invalidate defaults
            valid_values.insert(0, self._no_selection_text)

        # default value needs to be converted to index
        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}')

        # 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
        value = self.collect_choice(
            default=option_index, option_text=option_text, valid_values=valid_values
        )

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

        return value

    def present_data_types(self, data_types: list, required: Optional[bool] = False) -> str:
        """Present data types options.

        Args:
            data_types: A list of optional data types.
            required: If False the no selection option will be added.

        Returns:
            str: The user provided input.
        """
        if 'Any' in data_types:
            data_types = [
                'Binary',
                'BinaryArray',
                'KeyValue',
                'KeyValueArray',
                'String',
                'StringArray',
                'TCEntity',
                'TCEntityArray',
            ]

        # add option to not select an index value if input is not required
        if required is False:
            data_types.insert(0, self._no_selection_text)

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

        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}')

        index = self._input_value('Type', '[0]') or 0

        try:
            data_type = data_types[int(index)]
        except (IndexError, TypeError, ValueError):
            print(
                f'{c.Fore.RED}Invalid index of {index} provided. '
                f'Please provide a integer between 0-{len(data_types) - 1}'
            )
            sys.exit(1)

        return data_type

    def present_exit_code(self) -> None:
        """Provide user input for exit code."""
        self.print_header({'label': 'Exit Codes'})
        self.exit_codes = list(set(self.collect_exit_codes(default=[0], option_text='[0]')))

    @staticmethod
    def present_help() -> None:
        """Provide user help information."""
        print(
            f'{c.Fore.CYAN}For String type inputs: \n'
            ' * A value of null will be treated as an actual null value.\n'
            ' * Using "null" or \'null\' to insert a string of null.\n'
        )
        print(f'{c.Fore.CYAN}When done entering array data press enter to continue.')

    def present_key_value_list(self, name: str, data: dict) -> None:
        """Build a question for key value list input.

        Args:
            name: The name of the input field.
            data: The install.json input param object.

        Returns:
            str: The user provided input.
        """
        # print header information
        self.print_header(data)

        # the default value from install.json or user_data
        default = self._default(data)  # array of default values

        # collect input
        input_data = self.collect_key_value_array(default=default, required=data.get('required'))

        # create variable
        variable = self.add_staging_data(name, 'KeyValueArray', input_data)

        # add input to args
        self.add_input(name, data, variable)

        # user feedback
        feedback_data = input_data
        if input_data is not None:
            feedback_data = json.dumps(feedback_data)

        # # update default
        # if default is None:
        #     self.add_user_default(name, input_data)

        return variable

    def present_multichoice(self, name: str, data: dict) -> list:
        """Build a question for multichoice input.

        Args:
            name: The name of the input field.
            data: The install.json input param object.

        Returns:
            list: The user provided inputs.
        """
        # print header information
        self.print_header(data)

        default = self._default(data)  # array of default values
        option_indexes = [0]
        valid_values = self._expand_valid_values(data.get('validValues', []))
        if data.get('required', False) is False:
            # add option to invalidate defaults
            valid_values.insert(0, self._no_selection_text)

        # 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.
        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}')

        # 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
        values = self.collect_multichoice(
            default=option_indexes,
            option_text=option_text,
            required=data.get('required'),
            valid_values=valid_values,
        )

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

        return values

    def present_string(self, name: str, data: dict) -> str:
        """Build a question for string input.

        Args:
            name: The name of the input field.
            data: The install.json input param object.

        Returns:
            str: The user provided input.
        """
        # display header information
        self.print_header(data)

        # use playbook data types to determine what input to provide (default to String)
        data_type = data.get('playbookDataType', ['String'])[0]
        if len(data.get('playbookDataType', [])) > 1 or data_type.lower() == 'any':
            data_type = self.present_data_types(
                data.get('playbookDataType'), required=data.get('required', False)
            )

        # no need to proceed if there is not valid data type selected.
        if data_type == self._no_selection_text:
            self.add_input(name, data, None)
            self.print_feedback('null')
            return None

        # the default value from install.json or user_data
        default = self._default(data)

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

        # use data_type to properly format collection input
        input_value = self.collect_type_map[data_type](
            default=default, option_text=option_text, required=data.get('required', False)
        )

        # add staging data and get variable name
        variable = self.add_staging_data(name, data_type, input_value)

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

        # # update default
        # if default is None:
        #     if len(data.get('playbookDataType', [])) > 1 or data_type.lower() == 'any':
        #         # for inputs that take multiple types we need to store user default with the type
        #         self.add_user_default(name, input_value, data_type)
        #     else:
        #         self.add_user_default(name, input_value)

        return variable

    @staticmethod
    def print_feedback(feedback_value: Union[list, str]) -> None:
        """Print the value used."""
        print(f'Using value: {c.Fore.GREEN}{feedback_value}\n')

    @staticmethod
    def print_header(data: dict) -> None:
        """Enrich the header with metadata.

        Args:
            data: The install.json input param object.
        """

        def _print_metadata(title: str, value: str) -> None:
            """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'))

        # default
        default = data.get('default')
        if default:
            _print_metadata('Default', default)

        # 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)

    @staticmethod
    def print_invalid_bool() -> None:
        """Print a invalid bool error."""
        print(f'{c.Fore.RED}The provided value is not a boolean value (true/false).\n')

    @staticmethod
    def print_invalid_exit_code() -> None:
        """Print a invalid exit code error."""
        print(f'{c.Fore.RED}The provided value is not a valid exit code (0, 1).\n')

    @staticmethod
    def print_invalid_index(range_: str) -> None:
        """Print a invalid index error.

        Args:
            range_: The range of possible value for choice or multichoice selections.
        """
        print(
            f'{c.Fore.RED}The provided index value is not '
            f'valid, please select a valid value between {range_}.\n'
        )

    @staticmethod
    def print_required() -> None:
        """Print a required error."""
        print(f'{c.Fore.RED}This input is required, please enter an appropriate value.\n')

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

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

    #         # use feature defaults
    #         self._user_defaults = user_defaults.get(self.profile.feature)
    #         if self._user_defaults is None:
    #             # use base defaults if not feature defaults found
    #             self._user_defaults = user_defaults.get('base', {})

    #     return self._user_defaults
