#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import annotations

import json
import re
from collections import UserDict
from collections import UserList
from typing import Dict
from typing import List
from typing import Tuple

import pandas
from aws_xray_sdk.core import xray_recorder as xray

from iobot.exceptions import IOBotModelKeySortException
from iobot.utils import pad_if_blank_or_none

from psi_logger import logger

@xray.capture()
@logger.log
def sort_object_keys(objects: dict,
                     key_sort_order: list,
                     drop_invalid_keys: bool = False) -> dict:
    """
    >>> data: Dict = {'hello':'world','foo_bar':'bar_foo','this_field_will_be_split':'some value','account_id':1,'contact_id':1,'tower_id':1, 'this_key_will_drop_or_error': True }  # noqa: E501

    >>> sort_object_keys(data, key_sort_order=['this_field_will_be_split','foo_bar','hello'])
    Traceback (most recent call last):
    ...
    iobot.exceptions.IOBotModelKeySortException: "account_id" is not in key_sort_order list, use "drop_invalid_keys=True" to drop unknown keys while sorting

    >>> sort_object_keys(data, key_sort_order=['this_field_will_be_split','foo_bar','hello'], drop_invalid_keys=True)
    {'this_field_will_be_split': 'some value', 'foo_bar': 'bar_foo', 'hello': 'world'}
    """
    try:
        return dict(sorted(objects.items(),
                           key=lambda i: key_sort_order.index(i[0])))

    except ValueError as err:
        bogus_key = str(err).split(' ')[0].split("'")[1]

        if drop_invalid_keys:
            del objects[bogus_key]
            return sort_object_keys(objects,
                                    key_sort_order,
                                    drop_invalid_keys=drop_invalid_keys)
        else:
            raise IOBotModelKeySortException(
                    f'"{bogus_key}" is not in key_sort_order list, use "drop_invalid_keys=True" to drop unknown keys while sorting')  # noqa: E501


@xray.capture()
class IOBotObject(UserDict):
    @logger.log
    def __init__(self, *args, **kwargs) -> None:
        self.data = {}
        self._creds = {'access_token': ''}
        self._log = {}
        self._secrets = {}
        self.client = {}
        self.slater = {}
        self.slack_element_type_map = {}

        # unfurl kwargs into state
        for key, value in kwargs.items():
            self.__setitem__(key, value)

    def __delitem__(self, key):
        return self.data.pop(key)

    def __getitem__(self, key):
        return self.data.get(key)

    def __setitem__(self, key, value):
        if key in self:
            del self[key]
        self.data[key] = value

    @xray.capture()
    @logger.log
    def __to_dict__(self):
        return self.data.__dict__()

    @xray.capture()
    @logger.log
    def get(self, key, default=None):
        return self.data.get(key, default)

    @xray.capture()
    @logger.log
    def set_slack_element_type_map(self,
                                   slack_element_type_map: Dict[str, str]):

        self.slack_element_type_map = slack_element_type_map

    @xray.capture()
    @logger.log
    def set_slack_element_type_map_item(self,
                                        key: str, value: str):

        self.slack_element_type_map[key] = value

    @xray.capture()
    @logger.log
    def set_active(self, value: str) -> None:
        self.__setitem__('active', value)

    @xray.capture()
    @logger.log
    def set_created(self, value: Dict) -> None:
        self.__setitem__('created', value)

    @xray.capture()
    @logger.log
    def set_creds(self, value: Dict) -> None:
        self.__setitem__('_creds', value)

    @xray.capture()
    @logger.log
    def set_description(self, value: str) -> None:
        self.__setitem__('description', value)

    @xray.capture()
    @logger.log
    def set_id(self, value: str) -> None:
        self.__setitem__('id', value)

    @xray.capture()
    @logger.log
    def set_name(self, value: str) -> None:
        self.__setitem__('name', value)

    @xray.capture()
    @logger.log
    def set_ref(self, value: str) -> None:
        self.__setitem__('ref', value)

    @xray.capture()
    @logger.log
    def set_row_num(self, value: str) -> None:
        self.__setitem__('row_num', value)

    @xray.capture()
    @logger.log
    def set_updated(self, value: Dict) -> None:
        self.__setitem__('updated', value)

    @xray.capture()
    @logger.log
    def to_json(self):
        return json.dumps(self.data, default=lambda o: o.__dict__,
                          sort_keys=True, indent=2)

    @xray.capture()
    @logger.log
    def get_element_type_by_key(self, key):
        if key in self.slack_element_type_map:
            return self.slack_element_type_map[key]

    @xray.capture()
    @logger.log
    def get_blank_form_values(self):
        values: dict = {}
        for key in self.slack_element_type_map.keys():
            values[key] = ''

        return values

    @xray.capture()
    @logger.log
    def to_external_select_option(self):
        return {
            'text': {
                'text': f'{self.data.get("name").title()} ({self.data.get("id")})',
                'type': 'plain_text'
            },
            'value': f'{self.data.get("id")}'
        }

    # TODO: deprecate and migrate to slater downstream...
    @xray.capture()
    @logger.log
    def initialize_clients(self, creds, log,  secrets_manager, slater_client) -> None:
        self._creds = creds
        self._log = log
        self._secrets = secrets_manager
        self.slater = slater_client

    @xray.capture()
    @logger.log
    def GetSLaterClient(self):
        return self.slater

    @xray.capture()
    @logger.log
    @staticmethod
    def format_keys(data: Dict,
                    regex_key_filters: List[str] = None,
                    key_names_remap: Dict[str, str] = None,
                    regex_key_remap: Dict[str, str] = None,
                    key_sort_order: List[str] = None) -> IOBotObject:
        """ Format internal state based on options and return a copy.

        Parameters
        ----------

        regex_key_filters: List[str]
          Use this option to pass multiple regular expressions to supress keys

        key_names_remap: Dict[str, str]
          Use this option to rename fields for display

        regex_key_remap: Dict[str, str]
          Use this option to rename fields for display with a regex
          not currently implemented

          Design thought is either perform a
          split() and a sub() with the regex and return both in a dict since
          there is no way to no caller intent without additional switches which
          would then make presumptions

        key_sort_order: List[str]
          Use this to specify the sort order of fields for data

        Returns
        -------
        data : IOBotObject
          A copy of the original object with request mutations

        >>> data: Dict = {'hello':'world','foo_bar':'bar_foo','this_field_will_be_split':'some value','account_id':1,'contact_id':1,'tower_id':1 }  # noqa: E501

        >>> IOBotObject.format_keys(data)
        {'hello': 'world', 'foo_bar': 'bar_foo', 'this_field_will_be_split': 'some value', 'account_id': 1, 'contact_id': 1, 'tower_id': 1}

        >>> IOBotObject.format_keys(data, key_names_remap={'foo_bar':'bar_foo'})
        {'hello': 'world', 'bar_foo': 'bar_foo', 'this_field_will_be_split': 'some value', 'account_id': 1, 'contact_id': 1, 'tower_id': 1}

        >>> IOBotObject.format_keys(data, regex_key_filters=[r'_id$',r'hello'])
        {'foo_bar': 'bar_foo', 'this_field_will_be_split': 'some value'}

        >>> IOBotObject.format_keys(data, regex_key_filters=[r'_id$'], key_sort_order=['this_field_will_be_split','foo_bar','hello'])
        {'this_field_will_be_split': 'some value', 'foo_bar': 'bar_foo', 'hello': 'world'}

        """
        if not regex_key_filters:
            regex_key_filters = []

        if not key_names_remap:
            key_names_remap = {}

        new_state = IOBotObject(**{})
        for key, value in data.items():
            if any(list(re.search(pattern,
                                  key) for pattern in regex_key_filters)):
                continue  # we matched so this is a field we skip

            key = key_names_remap.get(key, key)
            new_state[key] = value

        if key_sort_order:
            if key_names_remap:
                for index, key in enumerate(key_sort_order):
                    if key_sort_order[index] in key_names_remap.keys():
                        key_sort_order[index] = key_names_remap[key]
            return sort_object_keys(new_state,
                                    key_sort_order,
                                    drop_invalid_keys=True)

        return new_state

    @xray.capture()
    @logger.log
    def to_slack_markdown_section(self,
                                  regex_field_filters: List[str] = None,
                                  field_names_remap: Dict[str, str] = None,
                                  field_sort_order: List[str] = None,
                                  link: Dict[str, str] = None,
                                  drop_empty_values: bool = True) -> Tuple:
        """ Convert our internal state to a slack json block

        Parameters
        ----------

        regex_field_filters: List[str]
          Use this option to pass multiple regular expressions to supress keys

        field_names_remap: Dict[str, str]
          Use this option to rename fields for display

        fields_sort_order: List[str]
          Use this to specify the sort order of fields for data

        link: Dict[str, str]
          Use this to link IOBotObjects via Slack UI block elements

        Returns
        -------
        data : Tuple
          A collection of slack markdown sections populated with request
          mutations


        Testing
        -------

        >>> data = IOBotObject(**{ 'hello':'world','foo_bar':'bar_foo', 'this_field_will_be_split':'some value', 'account_id':1,'contact_id':1,'tower_id':1, 'next_link':'http://www.foo.com/api' })

        >>> data.to_slack_markdown_section()
        {'type': 'section', 'text': {'type': 'mrkdwn', 'text': '*Hello:* world\\n*Foo Bar:* bar_foo\\n*This Field Will Be Split:* some value'}}

        >>> data.to_slack_markdown_section(field_names_remap={'foo_bar':'bar_foo'})
        {'type': 'section', 'text': {'type': 'mrkdwn', 'text': '*Hello:* world\\n*Bar Foo:* bar_foo\\n*This Field Will Be Split:* some value'}}

        >>> data.to_slack_markdown_section(regex_field_filters = [r'_id$',r'hello'])
        {'type': 'section', 'text': {'type': 'mrkdwn', 'text': '*Foo Bar:* bar_foo\\n*This Field Will Be Split:* some value\\n*Next Link:* http://www.foo.com/api'}}

        """
        if not regex_field_filters:
            regex_field_filters = [
                r'_id$',
                r'\bnext_link\b',
                r'\brow_num\b'
            ]

        if not field_names_remap:
            field_names_remap = {}

        section = {
            "type": "section",
            "text": {
                "type": "mrkdwn",
                "text": ""
            }
        }

        data = self.format_keys(self.data,
                                regex_key_filters=regex_field_filters,
                                key_names_remap=field_names_remap,
                                key_sort_order=field_sort_order)

        lines: str = []
        for key, value in data.items():

            if '_' in key:
                key = ' '.join(key.split('_')).title()
                if 'T Appid' in key:
                    key = key.replace('T Appid', 't_AppID')
            else:
                key = key.title()

            if value is None:
                value = ''

            if not drop_empty_values or (drop_empty_values and value):
                lines.append(f"*{key}:* {value}")

        section['text']['text'] = "\n".join(lines)

        if link:
            if link.get('block_id'):
                section['block_id'] = link.get('block_id')

            section['accessory'] = {
                "type": "button",
                "text": {
                    "type": "plain_text",
                    "text": link.get('label'),
                    "emoji": True
                },
                "value": link.get('value'),
                "action_id": link.get('action_id')
            }

        return section

    @xray.capture()
    @logger.log
    def to_modal_list_item(self,
                           regex_field_filters: List[str] = None,
                           field_names_remap: Dict[str, str] = None,
                           field_sort_order: List[str] = None,
                           link: Dict[str, str] = None) -> Tuple:
        """ Convert our internal state to a slack json block

        Parameters
        ----------

        regex_field_filters: List[str]
          Use this option to pass multiple regular expressions to supress keys

        field_names_remap: Dict[str, str]
          Use this option to rename fields for display

        fields_sort_order: List[str]
          Use this to specify the sort order of fields for data

        link: Dict[str, str]
          Use this to link IOBotObjects via Slack UI block elements

        Returns
        -------
        data : Tuple
          A collection of slack markdown sections populated with request
          mutations


        Testing
        -------

        >>> data = IOBotObject(**{ 'hello':'world','foo_bar':'bar_foo', 'this_field_will_be_split':'some value', 'account_id':1,'contact_id':1,'tower_id':1, 'next_link':'http://www.foo.com/api' })

        >>> data.to_slack_markdown_section()
        {'type': 'section', 'text': {'type': 'mrkdwn', 'text': '*Hello:* world\\n*Foo Bar:* bar_foo\\n*This Field Will Be Split:* some value'}}

        >>> data.to_slack_markdown_section(field_names_remap={'foo_bar':'bar_foo'})
        {'type': 'section', 'text': {'type': 'mrkdwn', 'text': '*Hello:* world\\n*Bar Foo:* bar_foo\\n*This Field Will Be Split:* some value'}}

        >>> data.to_slack_markdown_section(regex_field_filters = [r'_id$',r'hello'])
        {'type': 'section', 'text': {'type': 'mrkdwn', 'text': '*Foo Bar:* bar_foo\\n*This Field Will Be Split:* some value\\n*Next Link:* http://www.foo.com/api'}}

        """
        if not regex_field_filters:
            regex_field_filters = [
                r'_id$',
                r'\bnext_link\b',
                r'\brow_num\b'
            ]

        if not field_names_remap:
            field_names_remap = {}

        section = {
            "type": "section",
            "text": {
                "type": "mrkdwn",
                "text": ""
            }
        }

        data = self.format_keys(self.data,
                                regex_key_filters=regex_field_filters,
                                key_names_remap=field_names_remap,
                                key_sort_order=field_sort_order)

        lines: str = []
        for key, value in data.items():

            if '_' in key:
                key = ' '.join(key.split('_')).title()
                if 'T Appid' in key:
                    key = key.replace('T Appid', 't_AppID')
            else:
                key = key.title()

            if value:
                lines.append(f"*{key}:* {value}")

        section['text']['text'] = "\n".join(lines)

        if link:
            if link.get('block_id'):
                section['block_id'] = link.get('block_id')

            section['accessory'] = {
                "type": "button",
                "text": {
                    "type": "plain_text",
                    "text": link.get('label'),
                    "emoji": True
                },
                "value": link.get('value'),
                "action_id": link.get('action_id')
            }

        return section

    @xray.capture()
    @logger.log
    def to_slack_markdown_sections(self,
                                   regex_field_filters: List[str] = None,
                                   field_names_remap: Dict[str, str] = None,
                                   field_sort_order: List[str] = None) -> Tuple:
        """ Convert our internal state to a slack json block

        Parameters
        ----------

        regex_field_filters: List[str]
          Use this option to pass multiple regular expressions to supress keys

        field_names_remap: Dict[str, str]
          Use this option to rename fields for display

        fields_sort_order: List[str]
          Use this to specify the sort order of fields for data

        Returns
        -------
        data : Tuple
          A collection of slack markdown sections populated with request
          mutations


        Testing
        -------

        >>> data = IOBotObject(**{ 'hello':'world','foo_bar':'bar_foo', 'this_field_will_be_split':'some value', 'account_id':1,'contact_id':1,'tower_id':1, 'next_link':'http://www.foo.com/api' })

        >>> data.to_slack_markdown_sections()
        [{'type': 'section', 'text': {'type': 'mrkdwn', 'text': '*Hello:* world'}}, {'type': 'section', 'text': {'type': 'mrkdwn', 'text': '*Foo Bar:* bar_foo'}}, {'type': 'section', 'text': {'type': 'mrkdwn', 'text': '*This Field Will Be Split:* some value'}}]

        >>> data.to_slack_markdown_sections(field_names_remap={'foo_bar':'bar_foo'})
        [{'type': 'section', 'text': {'type': 'mrkdwn', 'text': '*Hello:* world'}}, {'type': 'section', 'text': {'type': 'mrkdwn', 'text': '*Bar Foo:* bar_foo'}}, {'type': 'section', 'text': {'type': 'mrkdwn', 'text': '*This Field Will Be Split:* some value'}}]

        >>> data.to_slack_markdown_sections(regex_field_filters = [r'_id$',r'hello'])
        [{'type': 'section', 'text': {'type': 'mrkdwn', 'text': '*Foo Bar:* bar_foo'}}, {'type': 'section', 'text': {'type': 'mrkdwn', 'text': '*This Field Will Be Split:* some value'}}, {'type': 'section', 'text': {'type': 'mrkdwn', 'text': '*Next Link:* http://www.foo.com/api'}}]

        """
        if not regex_field_filters:
            regex_field_filters = [
                r'_id$',
                r'\bnext_link\b',
                r'\brow_num\b'
            ]

        if not field_names_remap:
            field_names_remap = {}

        sections = []
        data = self.format_keys(self.data,
                                regex_key_filters=regex_field_filters,
                                key_names_remap=field_names_remap,
                                key_sort_order=field_sort_order)

        for key, value in data.items():

            if '_' in key:
                key = ' '.join(key.split('_')).title()
                if 'T Appid' in key:
                    key = key.replace('T Appid', 't_AppID')
            else:
                key = key.title()

            if value:
                sections.append({
                    "type": "section",
                    "text": {
                        "type": "mrkdwn",
                        "text": f"*{key}:* {value}"
                    }
                })

        return sections

    @xray.capture()
    @logger.log
    def to_slack_plaintext_section(self,
                                   regex_field_filters: List[str] = None,
                                   field_names_remap: Dict[str, str] = None,
                                   field_sort_order: List[str] = None) -> Tuple:
        """ Convert our internal state to a slack json block


        Parameters
        ----------

        regex_field_filters: List[str]
          Use this option to pass multiple regular expressions to supress keys

        field_names_remap: Dict[str, str]
          Use this option to rename fields for display

        fields_sort_order: List[str]
          Use this to specify the sort order of fields for data

        Returns
        -------
        data : Tuple
          A collection of slack markdown sections populated with request
          mutations


        Testing
        -------


        >>> data = IOBotObject(**{ 'hello':'world','foo_bar':'bar_foo', 'this_field_will_be_split':'some value', 'account_id':1,'contact_id':1,'tower_id':1 })

        >>> data.to_slack_plaintext_section()
         [{'type': 'section', 'text': {'type': 'plain_text', 'text': 'Hello: world', 'emoji': True}}, {'type': 'section', 'text': {'type': 'plain_text', 'text': 'Foo Bar: bar_foo', 'emoji': True}}, {'type': 'section', 'text': {'type': 'plain_text', 'text': 'This Field Will Be Split: some value', 'emoji': True}}, {'type': 'section', 'text': {'type': 'plain_text', 'text': 'Account Id: 1', 'emoji': True}}, {'type': 'section', 'text': {'type': 'plain_text', 'text': 'Contact Id: 1', 'emoji': True}}, {'type': 'section', 'text': {'type': 'plain_text', 'text': 'Tower Id: 1', 'emoji': True}}]

        >>> data.to_slack_plaintext_section(field_names_remap={'foo_bar':'bar_foo'})
        [{'type': 'section', 'text': {'type': 'plain_text', 'text': 'Hello: world', 'emoji': True}}, {'type': 'section', 'text': {'type': 'plain_text', 'text': 'Bar Foo: bar_foo', 'emoji': True}}, {'type': 'section', 'text': {'type': 'plain_text', 'text': 'This Field Will Be Split: some value', 'emoji': True}}, {'type': 'section', 'text': {'type': 'plain_text', 'text': 'Account Id: 1', 'emoji': True}}, {'type': 'section', 'text': {'type': 'plain_text', 'text': 'Contact Id: 1', 'emoji': True}}, {'type': 'section', 'text': {'type': 'plain_text', 'text': 'Tower Id: 1', 'emoji': True}}]

        >>> data.to_slack_plaintext_section(regex_field_filters = [r'_id$',r'hello'])
        [{'type': 'section', 'text': {'type': 'plain_text', 'text': 'Foo Bar: bar_foo', 'emoji': True}}, {'type': 'section', 'text': {'type': 'plain_text', 'text': 'This Field Will Be Split: some value', 'emoji': True}}]

        """
        if not regex_field_filters:
            regex_field_filters = []

        sections = []
        data = self.format_keys(self.data,
                                regex_key_filters=regex_field_filters,
                                key_names_remap=field_names_remap,
                                key_sort_order=field_sort_order)

        for key, value in data.items():

            if '_' in key:
                key = ' '.join(key.split('_')).title()
                if 'T Appid' in key:
                    key = key.replace('T Appid', 't_AppID')
            else:
                key = key.title()

            if value:
                sections.append({
                    "type": "section",
                    "text": {
                        "type": "plain_text",
                        "text": f"{key}: {value}",
                        "emoji": True
                    }
                })

        return sections

    @xray.capture()
    @logger.log
    def to_slack_plaintext_fields_section(self,
                                          regex_field_filters: List[str] = None,
                                          field_names_remap: Dict[str, str] = None,
                                          field_sort_order: List[str] = None) -> Tuple:
        """ Convert our internal state to a slack json block


        Parameters
        ----------

        regex_field_filters: List[str]
          Use this option to pass multiple regular expressions to supress keys

        field_names_remap: Dict[str, str]
          Use this option to rename fields for display

        fields_sort_order: List[str]
          Use this to specify the sort order of fields for data

        Returns
        -------
        data : Tuple
          A collection of slack markdown sections populated with request
          mutations


        Testing
        -------


        >>> data = IOBotObject(**{ 'hello':'world','foo_bar':'bar_foo', 'this_field_will_be_split':'some value', 'account_id':1,'contact_id':1,'tower_id':1 })

        >>> data.to_slack_plaintext_section()
        [{'type': 'section', 'text': {'type': 'plain_text', 'text': 'Hello: world', 'emoji': True}}, {'type': 'section', 'text': {'type': 'plain_text', 'text': 'Foo Bar: bar_foo', 'emoji': True}}, {'type': 'section', 'text': {'type': 'plain_text', 'text': 'This Field Will Be Split: some value', 'emoji': True}}, {'type': 'section', 'text': {'type': 'plain_text', 'text': 'Account Id: 1', 'emoji': True}}, {'type': 'section', 'text': {'type': 'plain_text', 'text': 'Contact Id: 1', 'emoji': True}}, {'type': 'section', 'text': {'type': 'plain_text', 'text': 'Tower Id: 1', 'emoji': True}}]

        >>> data.to_slack_plaintext_section(field_names_remap={'foo_bar':'bar_foo'})
        [{'type': 'section', 'text': {'type': 'plain_text', 'text': 'Hello: world', 'emoji': True}}, {'type': 'section', 'text': {'type': 'plain_text', 'text': 'Bar Foo: bar_foo', 'emoji': True}}, {'type': 'section', 'text': {'type': 'plain_text', 'text': 'This Field Will Be Split: some value', 'emoji': True}}, {'type': 'section', 'text': {'type': 'plain_text', 'text': 'Account Id: 1', 'emoji': True}}, {'type': 'section', 'text': {'type': 'plain_text', 'text': 'Contact Id: 1', 'emoji': True}}, {'type': 'section', 'text': {'type': 'plain_text', 'text': 'Tower Id: 1', 'emoji': True}}]

        >>> data.to_slack_plaintext_section(regex_field_filters = [r'_id$',r'hello'])
        [{'type': 'section', 'text': {'type': 'plain_text', 'text': 'Foo Bar: bar_foo', 'emoji': True}}, {'type': 'section', 'text': {'type': 'plain_text', 'text': 'This Field Will Be Split: some value', 'emoji': True}}]

        >>> data.to_slack_plaintext_section(regex_field_filters = [r'_id$',r'hello'], field_sort_order = ['this_field_will_be_split','foo_bar'])
        [{'type': 'section', 'text': {'type': 'plain_text', 'text': 'This Field Will Be Split: some value', 'emoji': True}}, {'type': 'section', 'text': {'type': 'plain_text', 'text': 'Foo Bar: bar_foo', 'emoji': True}}]
        """
        if not regex_field_filters:
            regex_field_filters = []

        section = {
            "type": "section",
            "fields": []
        }

        data = self.format_keys(self.data,
                                regex_key_filters=regex_field_filters,
                                key_names_remap=field_names_remap,
                                key_sort_order=field_sort_order)

        for key, value in data.items():

            if '_' in key:
                key = ' '.join(key.split('_')).title()
                if 'T Appid' in key:
                    key = key.replace('T Appid', 't_AppID')
            else:
                key = key.title()

            if value:
                section['fields'].append({
                    "type": "plain_text",
                    "text": f'{key}:',
                    "emoji": True
                })
                section['fields'].append({
                    "type": "plain_text",
                    "text": value,
                    "emoji": True
                })

        return section

    @xray.capture()
    @logger.log
    def GetObjectType(self, underscore: bool = False) -> str:
        name: str = str(type(self).__name__)
        if underscore:
            return re.sub(r'(?<!^)(?=[A-Z])', '_', name).lower()
        else:
            return name

    def to_human_readable(self):
        pass

    @xray.capture()
    @logger.log
    def to_slack_modal_form(self,
                            regex_field_filters: List[str] = None,
                            field_names_remap: Dict[str, str] = None,
                            field_sort_order: List[str] = None,
                            slack_element_type_map: Dict[str, str] = None,
                            slack_element_options: Dict = None) -> Tuple:
        """ Convert our internal state to a slack json block


        Parameters
        ----------

        regex_field_filters: List[str]
          Use this option to pass multiple regular expressions to supress keys

        field_names_remap: Dict[str, str]
          Use this option to rename fields for display

        fields_sort_order: List[str]
          Use this to specify the sort order of fields for data

        slack_element_type_map: Dict[str, str]
          Mapping of key/value pairs that represent slack block_ids and
          slack element type. By default all IOBotObject elements will convert
          to 'plain_text_input'.

          Currently supported input types:
              - plain_text_input: str
              - radio_buttons: List[str,str]
              - datepicker: datetime.date
              - static_select: List[str,str]
              - external_select: str for initial_title and action_id

        Returns
        -------
        data : Tuple
          A collection of slack markdown sections populated with request
          mutations


        Testing
        -------


        >>> data = IOBotObject(**{ 'hello':'world','foo_bar':'bar_foo_value', 'this_field_will_be_split':'some value', 'account_id':1,'contact_id':1,'tower_id':1, 'active':0, 'empty_string':'', 'updated_last':'2021-01-10', 'decommissioned':1 })  # noqa E501

        >>> json.dumps(data.to_slack_modal_form(slack_element_type_map = {'hello':'plain_text_input', 'account_id':'external_select','contact_id':'external_select', 'tower_id':'external_select', 'active':'radio_buttons', 'updated_last':'datepicker', 'decommissioned':'static_select' }, slack_element_options = {'account_id': {'options': [], 'transform':[], 'initial_title': 'Atari', 'action_id': 'slater_selected_accounts' }, 'active': { 'options': [{'Yes':'0'},{'No':'1'}], 'transform': [{'0':'Yes'},{'1':'No'}] }, 'contact_id': {'options': [], 'transform':[], 'initial_title': 'Atari@root.net', 'action_id': 'slater_selected_contacts' }, 'decommissioned':{ 'options': [{'Yes':'0'},{'No':'1'}], 'transform': [{'0':'Yes'},{'1':'No'}] }, 'tower_id': {'options': [], 'transform':[], 'initial_title': 'Dark Tower', 'action_id': 'slater_selected_towers' }}))  # noqa E501
        '[{"type": "input", "block_id": "hello", "element": {"type": "plain_text_input", "action_id": "hello", "initial_value": "world"}, "label": {"type": "plain_text", "text": "Hello", "emoji": true}}, {"type": "input", "block_id": "foo_bar", "element": {"type": "plain_text_input", "action_id": "foo_bar", "initial_value": "bar_foo_value"}, "label": {"type": "plain_text", "text": "Foo Bar", "emoji": true}}, {"type": "input", "block_id": "this_field_will_be_split", "element": {"type": "plain_text_input", "action_id": "this_field_will_be_split", "initial_value": "some value"}, "label": {"type": "plain_text", "text": "This Field Will Be Split", "emoji": true}}, {"type": "input", "block_id": "account_id", "label": {"type": "plain_text", "text": "Account Id"}, "element": {"type": "external_select", "action_id": "slater_selected_accounts", "placeholder": {"type": "plain_text", "text": "Choose account_id"}, "min_query_length": 2, "initial_option": {"text": {"type": "plain_text", "text": "Atari"}, "value": "1"}}}, {"type": "input", "block_id": "contact_id", "label": {"type": "plain_text", "text": "Contact Id"}, "element": {"type": "external_select", "action_id": "slater_selected_contacts", "placeholder": {"type": "plain_text", "text": "Choose contact_id"}, "min_query_length": 2, "initial_option": {"text": {"type": "plain_text", "text": "Atari@root.net"}, "value": "1"}}}, {"type": "input", "block_id": "tower_id", "label": {"type": "plain_text", "text": "Tower Id"}, "element": {"type": "external_select", "action_id": "slater_selected_towers", "placeholder": {"type": "plain_text", "text": "Choose tower_id"}, "min_query_length": 2, "initial_option": {"text": {"type": "plain_text", "text": "Dark Tower"}, "value": "1"}}}, {"type": "input", "block_id": "active", "element": {"type": "radio_buttons", "options": [{"text": {"type": "plain_text", "text": "Yes"}, "value": "0"}, {"text": {"type": "plain_text", "text": "No"}, "value": "1"}], "initial_option": {"text": {"type": "plain_text", "text": "Yes"}, "value": "0"}}, "label": {"type": "plain_text", "text": "Active", "emoji": true}}, {"type": "input", "block_id": "empty_string", "element": {"type": "plain_text_input", "action_id": "empty_string"}, "label": {"type": "plain_text", "text": "Empty String", "emoji": true}}, {"type": "input", "block_id": "updated_last", "element": {"type": "datepicker", "action_id": "updated_last", "initial_date": "2021-01-10", "placeholder": {"type": "plain_text", "text": "Choose a date", "emoji": true}, "confirm": {"title": {"type": "plain_text", "text": "Change Updated Last Date"}, "text": {"type": "mrkdwn", "text": "Do you really want to change the Updated Last date?"}, "confirm": {"type": "plain_text", "text": "Yes, I do!"}, "deny": {"type": "plain_text", "text": "No, I do not!"}}}, "label": {"type": "plain_text", "text": "Updated Last", "emoji": true}}, {"type": "input", "block_id": "decommissioned", "label": {"type": "plain_text", "text": "Decommissioned"}, "element": {"type": "static_select", "action_id": "decommissioned", "placeholder": {"type": "plain_text", "text": "Choose one"}, "options": [{"text": {"type": "plain_text", "text": "Yes"}, "value": "0"}, {"text": {"type": "plain_text", "text": "No"}, "value": "1"}], "initial_option": {"text": {"type": "plain_text", "text": "No"}, "value": "1"}}}]'

        >>> json.dumps(data.to_slack_modal_form(field_names_remap={'foo_bar':'bar_foo'}, slack_element_type_map = {'hello':'plain_text_input', 'account_id':'external_select','contact_id':'external_select', 'tower_id':'external_select', 'active':'radio_buttons', 'updated_last':'datepicker', 'decommissioned':'static_select' }, slack_element_options = {'account_id': {'options': [], 'transform':[], 'initial_title': 'Atari', 'action_id': 'slater_selected_accounts' }, 'active': { 'options': [{'Yes':'0'},{'No':'1'}], 'transform': [{'0':'Yes'},{'1':'No'}] }, 'contact_id': {'options': [], 'transform':[], 'initial_title': 'Atari@root.net', 'action_id': 'slater_selected_contacts' }, 'decommissioned':{ 'options': [{'Yes':'0'},{'No':'1'}], 'transform': [{'0':'Yes'},{'1':'No'}] }, 'tower_id': {'options': [], 'transform':[], 'initial_title': 'Dark Tower', 'action_id': 'slater_selected_towers' }}))  # noqa E501
        '[{"type": "input", "block_id": "hello", "element": {"type": "plain_text_input", "action_id": "hello", "initial_value": "world"}, "label": {"type": "plain_text", "text": "Hello", "emoji": true}}, {"type": "input", "block_id": "bar_foo", "element": {"type": "plain_text_input", "action_id": "bar_foo", "initial_value": "bar_foo_value"}, "label": {"type": "plain_text", "text": "Bar Foo", "emoji": true}}, {"type": "input", "block_id": "this_field_will_be_split", "element": {"type": "plain_text_input", "action_id": "this_field_will_be_split", "initial_value": "some value"}, "label": {"type": "plain_text", "text": "This Field Will Be Split", "emoji": true}}, {"type": "input", "block_id": "account_id", "label": {"type": "plain_text", "text": "Account Id"}, "element": {"type": "external_select", "action_id": "slater_selected_accounts", "placeholder": {"type": "plain_text", "text": "Choose account_id"}, "min_query_length": 2, "initial_option": {"text": {"type": "plain_text", "text": "Atari"}, "value": "1"}}}, {"type": "input", "block_id": "contact_id", "label": {"type": "plain_text", "text": "Contact Id"}, "element": {"type": "external_select", "action_id": "slater_selected_contacts", "placeholder": {"type": "plain_text", "text": "Choose contact_id"}, "min_query_length": 2, "initial_option": {"text": {"type": "plain_text", "text": "Atari@root.net"}, "value": "1"}}}, {"type": "input", "block_id": "tower_id", "label": {"type": "plain_text", "text": "Tower Id"}, "element": {"type": "external_select", "action_id": "slater_selected_towers", "placeholder": {"type": "plain_text", "text": "Choose tower_id"}, "min_query_length": 2, "initial_option": {"text": {"type": "plain_text", "text": "Dark Tower"}, "value": "1"}}}, {"type": "input", "block_id": "active", "element": {"type": "radio_buttons", "options": [{"text": {"type": "plain_text", "text": "Yes"}, "value": "0"}, {"text": {"type": "plain_text", "text": "No"}, "value": "1"}], "initial_option": {"text": {"type": "plain_text", "text": "Yes"}, "value": "0"}}, "label": {"type": "plain_text", "text": "Active", "emoji": true}}, {"type": "input", "block_id": "empty_string", "element": {"type": "plain_text_input", "action_id": "empty_string"}, "label": {"type": "plain_text", "text": "Empty String", "emoji": true}}, {"type": "input", "block_id": "updated_last", "element": {"type": "datepicker", "action_id": "updated_last", "initial_date": "2021-01-10", "placeholder": {"type": "plain_text", "text": "Choose a date", "emoji": true}, "confirm": {"title": {"type": "plain_text", "text": "Change Updated Last Date"}, "text": {"type": "mrkdwn", "text": "Do you really want to change the Updated Last date?"}, "confirm": {"type": "plain_text", "text": "Yes, I do!"}, "deny": {"type": "plain_text", "text": "No, I do not!"}}}, "label": {"type": "plain_text", "text": "Updated Last", "emoji": true}}, {"type": "input", "block_id": "decommissioned", "label": {"type": "plain_text", "text": "Decommissioned"}, "element": {"type": "static_select", "action_id": "decommissioned", "placeholder": {"type": "plain_text", "text": "Choose one"}, "options": [{"text": {"type": "plain_text", "text": "Yes"}, "value": "0"}, {"text": {"type": "plain_text", "text": "No"}, "value": "1"}], "initial_option": {"text": {"type": "plain_text", "text": "No"}, "value": "1"}}}]'

        >>> json.dumps(data.to_slack_modal_form(regex_field_filters = [r'_id$',r'hello'], slack_element_type_map = {'hello':'plain_text_input', 'account_id':'external_select','contact_id':'external_select', 'tower_id':'external_select', 'active':'radio_buttons', 'updated_last':'datepicker', 'decommissioned':'static_select' }, slack_element_options = {'account_id': {'options': [], 'transform':[], 'initial_title': 'Atari', 'action_id': 'slater_selected_accounts' }, 'active': { 'options': [{'Yes':'0'},{'No':'1'}], 'transform': [{'0':'Yes'},{'1':'No'}] }, 'contact_id': {'options': [], 'transform':[], 'initial_title': 'Atari@root.net', 'action_id': 'slater_selected_contacts' }, 'decommissioned':{ 'options': [{'Yes':'0'},{'No':'1'}], 'transform': [{'0':'Yes'},{'1':'No'}] }, 'tower_id': {'options': [], 'transform':[], 'initial_title': 'Dark Tower', 'action_id': 'slater_selected_towers' }}))  # noqa E501
        '[{"type": "input", "block_id": "foo_bar", "element": {"type": "plain_text_input", "action_id": "foo_bar", "initial_value": "bar_foo_value"}, "label": {"type": "plain_text", "text": "Foo Bar", "emoji": true}}, {"type": "input", "block_id": "this_field_will_be_split", "element": {"type": "plain_text_input", "action_id": "this_field_will_be_split", "initial_value": "some value"}, "label": {"type": "plain_text", "text": "This Field Will Be Split", "emoji": true}}, {"type": "input", "block_id": "active", "element": {"type": "radio_buttons", "options": [{"text": {"type": "plain_text", "text": "Yes"}, "value": "0"}, {"text": {"type": "plain_text", "text": "No"}, "value": "1"}], "initial_option": {"text": {"type": "plain_text", "text": "Yes"}, "value": "0"}}, "label": {"type": "plain_text", "text": "Active", "emoji": true}}, {"type": "input", "block_id": "empty_string", "element": {"type": "plain_text_input", "action_id": "empty_string"}, "label": {"type": "plain_text", "text": "Empty String", "emoji": true}}, {"type": "input", "block_id": "updated_last", "element": {"type": "datepicker", "action_id": "updated_last", "initial_date": "2021-01-10", "placeholder": {"type": "plain_text", "text": "Choose a date", "emoji": true}, "confirm": {"title": {"type": "plain_text", "text": "Change Updated Last Date"}, "text": {"type": "mrkdwn", "text": "Do you really want to change the Updated Last date?"}, "confirm": {"type": "plain_text", "text": "Yes, I do!"}, "deny": {"type": "plain_text", "text": "No, I do not!"}}}, "label": {"type": "plain_text", "text": "Updated Last", "emoji": true}}, {"type": "input", "block_id": "decommissioned", "label": {"type": "plain_text", "text": "Decommissioned"}, "element": {"type": "static_select", "action_id": "decommissioned", "placeholder": {"type": "plain_text", "text": "Choose one"}, "options": [{"text": {"type": "plain_text", "text": "Yes"}, "value": "0"}, {"text": {"type": "plain_text", "text": "No"}, "value": "1"}], "initial_option": {"text": {"type": "plain_text", "text": "No"}, "value": "1"}}}]'

        """

        if not regex_field_filters:
            regex_field_filters = []

        if slack_element_type_map:
            self.set_slack_element_type_map(slack_element_type_map)

        sections = []
        data = self.format_keys(self.data,
                                regex_key_filters=regex_field_filters,
                                key_names_remap=field_names_remap,
                                key_sort_order=field_sort_order)

        for raw_key, value in data.items():

            key: str = ""
            if '_' in raw_key:
                key = ' '.join(raw_key.split('_')).title()
                if 'T Appid' in key:
                    key = key.replace('T Appid', 't_AppID')
            else:
                key = raw_key.title()


            element_type = self.get_element_type_by_key(raw_key)
            if not element_type:
                element_type = "plain_text_input"

            # print(f'element_type: {element_type}')

            if element_type == 'plain_text_input':
                section = {
                    "type": "input",
                    "block_id": f"{raw_key}",
                    "element": {
                        "type": f"{element_type}",
                        "action_id": f"{raw_key}"
                    },
                    "label": {
                        "type": "plain_text",
                        "text": f"{key}",
                        "emoji": True
                    }
                }
                if value:
                    section['element']['initial_value'] = f"{pad_if_blank_or_none(value)}"

                if len(key) >= 50:
                    section['element']['multiline'] = True

                sections.append(section)

            elif element_type == 'radio_buttons':
                options: list = []
                initial_option: Dict = {}
                option_title: str = ''

                if slack_element_options and (raw_key in slack_element_options):

                    for item in slack_element_options[raw_key]['options']:
                        for current_key, current_value in item.items():

                            if 'transform' in slack_element_options[raw_key]:
                                if current_value in slack_element_options[raw_key]['transform']:
                                    option_title = slack_element_options[raw_key]['transform'][current_value]
                                else:
                                    option_title = current_key
                            if 'label' in slack_element_options[raw_key]:
                                key = slack_element_options[raw_key]['label']
                            if pad_if_blank_or_none(current_value) == pad_if_blank_or_none(value):
                                # make this button the 'active' or
                                # 'initial_option'. it will also be added by
                                # default to 'options' below, otherwise it
                                # will not display as set.
                                initial_option = {
                                    "text": {
                                        "type": "plain_text",
                                        "text": f"{option_title}",
                                    },
                                    "value": f"{pad_if_blank_or_none(current_value)}"
                                }

                            options.append({
                                "text": {
                                    "type": "plain_text",
                                    "text": f"{option_title}",
                                },
                                "value": f"{pad_if_blank_or_none(current_value)}"
                            })

                section: Dict = {
                    "type": "input",
                    "block_id": f"{raw_key}",
                    "element": {
                        "type": element_type,
                    },
                    "label": {
                        "type": "plain_text",
                        "text": f"{key}",
                        "emoji": True
                    }
                }

                if options:
                    section['element']['options'] = options

                if initial_option:
                    section['element']['initial_option'] = initial_option

                sections.append(section)

            elif element_type == 'datepicker':
                sections.append({
                    "type": "input",
                    "block_id": f"{raw_key}",
                    "element": {
                        "type": f"{element_type}",
                        "action_id": f"{raw_key}",
                        "initial_date": f"{value}",
                        "placeholder": {
                            "type": "plain_text",
                            "text": "Choose a date",
                            "emoji": True
                        },
                        "confirm": {
                            "title": {
                                "type": "plain_text",
                                "text": f"Change {key} Date"
                            },
                            "text": {
                                "type": "mrkdwn",
                                "text": f"Do you really want to change the {key} date?"  # noqa E501
                            },
                            "confirm": {
                                "type": "plain_text",
                                "text": "Yes, I do!"
                            },
                            "deny": {
                                "type": "plain_text",
                                "text": "No, I do not!"
                            }
                        },
                    },
                    "label": {
                        "type": "plain_text",
                        "text": f"{key}",
                        "emoji": True
                    }
                })

            elif element_type in ['static_select','multi_static_select']:
                options: list = []
                initial_option: Dict = {}
                option_title: str = ''
                max_selected: str = ''

                if slack_element_options and (raw_key in slack_element_options):
                    for item in slack_element_options[raw_key]['options']:
                        for current_key, current_value in item.items():

                            if 'transform' in slack_element_options[raw_key]:
                                if current_value in slack_element_options[raw_key]['transform']:
                                    option_title = slack_element_options[raw_key]['transform'][current_value]
                                else:
                                    option_title = current_key
                            if 'label' in slack_element_options[raw_key]:
                                key = slack_element_options[raw_key]['label']

                            if pad_if_blank_or_none(current_value) == pad_if_blank_or_none(value):
                                # make this selection the 'active' or
                                # 'initial_option'. it will also be added by
                                # default to 'options' below, otherwise it
                                # will not display as set.
                                initial_option = {
                                    "text": {
                                        "type": "plain_text",
                                        "text": f"{option_title}"
                                    },
                                    "value": f"{current_value}"
                                }

                            options.append({
                                "text": {
                                    "type": "plain_text",
                                    "text": f"{option_title}"
                                },
                                "value": f"{current_value}"
                            })
                        
                    max_selected = slack_element_options[raw_key].get('max_selected_items')

                section: Dict = {
                    "type": "input",
                    "block_id": f"{raw_key}",
                    "label": {
                        "type": "plain_text",
                        "text": f"{key.title()}",
                    },
                    "element": {
                        "type": f"{element_type}",
                        "action_id": f"{raw_key}",
                        "placeholder": {
                            "type": "plain_text",
                            "text": "Choose one",
                        },
                        "options": options,
                        "initial_option": initial_option
                    }
                }

                if options:
                    section['element']['options'] = options

                if initial_option:
                    section['element']['initial_option'] = initial_option
                
                if element_type == 'multi_static_select':
                    section['element'].pop('initial_option', None)
                
                if max_selected:
                    section['element']['max_selected_items'] = max_selected

                sections.append(section)

            elif element_type == 'multi_external_select':
                action_id: str = raw_key
                option_title: str = ''
                max_selected: str = ''

                if slack_element_options and (raw_key in slack_element_options):
                    if 'action_id' in slack_element_options[raw_key]:
                        action_id = slack_element_options[raw_key]['action_id']
                    
                    max_selected = slack_element_options[raw_key].get('max_selected_items')
                
                section = {
                    "type": "section",
                    "block_id": f"{raw_key}",
                    "text": {
                        "type": "mrkdwn",
                        "text": f"{key}"
                    },
                    "accessory": {
                        "action_id": f"{action_id}",
                        "type": "multi_external_select",
                        "placeholder": {
                            "type": "plain_text",
                            "text": f"Select {raw_key}"
                        },
                        "min_query_length": 2
                    }
                }

                if max_selected:
                    section['accessory']['max_selected_items'] = max_selected
                
                sections.append(section)

            elif element_type == 'external_select':
                action_id: str = raw_key
                option_title: str = ''
                max_selected: str = ''

                if slack_element_options and (raw_key in slack_element_options):
                    if 'action_id' in slack_element_options[raw_key]:
                        action_id = slack_element_options[raw_key]['action_id']

                    if 'initial_title' in slack_element_options[raw_key]:
                        option_title = slack_element_options[raw_key]['initial_title']

                    if 'label' in slack_element_options[raw_key]:
                        key = slack_element_options[raw_key]['label']

                    max_selected = slack_element_options[raw_key].get('max_selected_items')

                section = {
                    "type": "input",
                    "block_id": f"{raw_key}",
                    "label": {
                        "type": "plain_text",
                        "text": f"{key}"
                    },
                    "element": {
                        "type": f"{element_type}",
                        "action_id": f"{action_id}",
                        "placeholder": {
                            "type": "plain_text",
                            "text": f"Choose {raw_key}"
                        },
                        "min_query_length": 2
                    }
                }

                if option_title:
                    section['element']['initial_option'] = {
                        "text": {
                            "type": "plain_text",
                            "text": f"{option_title}"
                        },
                        "value": f"{value}"
                    }

                if max_selected:
                    section['element']['max_selected_items'] = max_selected

                sections.append(section)

            elif element_type == 'multiline_markdown_section':
                section = [
                    {
                        "type": "section",
                        "block_id": f"{raw_key}-title",
                        "text": {
                            "type": "mrkdwn",
                            "text": f"*{key}*"
                        }
                    },
                    {
                        "type": "section",
                        "block_id": f"{raw_key}",
                        "text": {
                            "type": "mrkdwn",
                            "text": str(value)
                        }
                    }
                ]

                sections.extend(section)

        return sections


@xray.capture()
class IOBotList(UserList):
    @xray.capture()
    @logger.log
    def __init__(self, objects: list, limit: int = -1, next_token: int = -1) -> None:
        self.data = []
        self.limit = limit
        self.next_token = next_token

        for obj in objects:
            self.data.append(obj)

    @xray.capture()
    @logger.log
    def to_external_select(self) -> Tuple:
        select = {
            'options': []
        }
        for item in self.data:
            select.get('options').append(item.to_external_select_option())
        return select

    @xray.capture()
    @logger.log
    @staticmethod
    def to_slack_modal_search(action_id: str,
                              block_id: str,
                              search_info: str,
                              search_placeholder: str) -> Tuple:
        """ Convert our internal state to a slack json block


        Parameters
        ----------

        action_id: str
          The external select type ahead you want to search against

        block_id: str
          name of this search action section that you want to connect
          this search view to - See SlackEventsInteractions.app

        search_info: str
          The search title text you want to appear in the view

        search_placeholder:
          The search field placeholder text you want to appear as an example

        Returns
        -------
        data : Tuple
          A collection of slack markdown sections populated with request
          mutations


        Testing
        -------


        >>> data = IOBotObject(**{ 'hello':'world','foo_bar':'bar_foo', 'this_field_will_be_split':'some value', 'account_id':1,'contact_id':1,'tower_id':1 })

        >>> data.to_slack_plaintext_section()
         [{'type': 'section', 'text': {'type': 'plain_text', 'text': 'Hello: world', 'emoji': True}}, {'type': 'section', 'text': {'type': 'plain_text', 'text': 'Foo Bar: bar_foo', 'emoji': True}}, {'type': 'section', 'text': {'type': 'plain_text', 'text': 'This Field Will Be Split: some value', 'emoji': True}}, {'type': 'section', 'text': {'type': 'plain_text', 'text': 'Account Id: 1', 'emoji': True}}, {'type': 'section', 'text': {'type': 'plain_text', 'text': 'Contact Id: 1', 'emoji': True}}, {'type': 'section', 'text': {'type': 'plain_text', 'text': 'Tower Id: 1', 'emoji': True}}]

        >>> data.to_slack_plaintext_section(field_names_remap={'foo_bar':'bar_foo'})
        [{'type': 'section', 'text': {'type': 'plain_text', 'text': 'Hello: world', 'emoji': True}}, {'type': 'section', 'text': {'type': 'plain_text', 'text': 'Bar Foo: bar_foo', 'emoji': True}}, {'type': 'section', 'text': {'type': 'plain_text', 'text': 'This Field Will Be Split: some value', 'emoji': True}}, {'type': 'section', 'text': {'type': 'plain_text', 'text': 'Account Id: 1', 'emoji': True}}, {'type': 'section', 'text': {'type': 'plain_text', 'text': 'Contact Id: 1', 'emoji': True}}, {'type': 'section', 'text': {'type': 'plain_text', 'text': 'Tower Id: 1', 'emoji': True}}]

        >>> data.to_slack_plaintext_section(regex_field_filters = [r'_id$',r'hello'])
        [{'type': 'section', 'text': {'type': 'plain_text', 'text': 'Foo Bar: bar_foo', 'emoji': True}}, {'type': 'section', 'text': {'type': 'plain_text', 'text': 'This Field Will Be Split: some value', 'emoji': True}}]

        """

        sections: list = [
            {
                "type": "section",
                "block_id": f"{block_id}",
                "text": {
                    "type": "mrkdwn",
                    "text": f"{search_info}"
                },
                "accessory": {
                    "action_id": f"{action_id}",
                    "type": "external_select",
                    "placeholder": {
                        "type": "plain_text",
                        "text": f"{search_placeholder}"
                    },
                    "min_query_length": 2
                }
            },
            {
                "type": "divider"
            },
            {
                "type": "context",
                "elements": [{
                    "type": "mrkdwn",
                    "text": "*SLater* all rights reserved <https://pearson.com|:pearson: Pearson>"
                }]
            }
        ]

        return sections

    @xray.capture()
    @logger.log
    def to_json(self, type_name='objects', indent=2):
        """ Test that json loads into an object properly
        >>> objects = IOBotList([IOBotObject(name='Ricktacula', id='42', ref='https://multiverse/morties/42', description='who-cares')])
        >>> data = json.loads(objects.to_json())
        >>> assert(data['objects'][0]['name'] == 'Ricktacula'), f"expected name: 'Ricktacula', got {data.name}"
        """
        # TODO: rework how this encodes objects over 'plain' keys
        objects: list = []
        for item in self.data:
            if any('IOBotObject' in obj.__name__ for obj in item.__class__.__bases__):
                reloaded = json.loads(item.to_json())
                objects.append(reloaded)
            else:
                objects.append(item.data)
        return json.dumps({type_name: objects}, indent=indent)

    @xray.capture()
    @logger.log
    def to_csv(self, index: bool = True, header: bool = True, encoding: str = 'utf-8', columns: list = None):
        """ Test that csv data loads into a pandas dataframe
        >>> objects = IOBotList([IOBotObject('Ricktacula', '42', 'https://multiverse/morties/42', 'who-cares')])
        >>> data = objects.to_csv().lstrip().splitlines()
        >>> assert (len(data) == 2), f'expected two rows (header and one data record), got {len(data)} rows'
        """
        if not columns:
            columns = []

        dataframe = pandas.DataFrame(self.data)
        return dataframe.to_csv(index=index, header=header, encoding=encoding, columns=columns)

    # TODO: deprecate and migrate to slater downstream...

    @xray.capture()
    @logger.log
    def initialize_clients(self, creds, log,  secrets_manager, slater_client) -> None:
        """ Test that each object is not None
        >>> objects = IOBotList([IOBotObject('Ricktacula', '42', 'https://multiverse/morties/42', 'who-cares')])
        >>> objects.initialize_clients({},{},{},{})
        >>> assert(objects._creds is not None), f'expected creds to be not None, got {objects._creds}'
        """
        self._creds = creds
        self._log = log
        self._secrets = secrets_manager
        self.slater = slater_client

    @xray.capture()
    @logger.log
    def GetObjectType(self, underscore: bool = False) -> str:
        name: str = str(type(self).__name__)
        if underscore:
            return re.sub(r'(?<!^)(?=[A-Z])', '_', name).lower()
        else:
            return name
