#!/usr/bin/env python
# -*- coding: utf-8 -*-
import hashlib
import hmac
import json
import os
import re
import time
import unicodedata
from collections import UserDict
from datetime import timedelta
from enum import Enum
from random import randint
from typing import Dict
from typing import List
from typing import Tuple

import slack_sdk
from aws_xray_sdk.core import xray_recorder as xray

from psi_logger import logger
from psi_secrets import SecretsManager
from iobot.utils import get_envvar
from iobot.utils import init_config

repo_url = "https://gitlab.com/pearson-io-bots/iobot"  # noqa: E501

@xray.capture()
class SlackException(Exception):
    pass


@xray.capture()
class NoneTypeUserError(SlackException):
    pass


@xray.capture()
class InvalidAccountTypeError(SlackException):
    pass


@xray.capture()
class UnAuthorizedUserGroupError(SlackException):
    pass


@xray.capture()
class SlackRuntimeEntitlementLevel(Enum):
    UNAUTHORIZED = 0
    UNRESTRICTED = 1
    PERMIT_GUESTS = 2
    FULL_MEMBERS_ONLY = 3
    ADMINS_ONLY = 4
    OWNERS_ONLY = 5
    PRIMARY_OWNER_ONLY = 6


@xray.capture()
class SlackUserEntitlementLevel(Enum):
    UNAUTHORIZED = SlackRuntimeEntitlementLevel.UNAUTHORIZED.value
    SINGLE_CHANNEL_GUEST = SlackRuntimeEntitlementLevel.UNRESTRICTED.value
    MULTI_CHANNEL_GUEST = SlackRuntimeEntitlementLevel.PERMIT_GUESTS.value
    FULL_MEMBER = SlackRuntimeEntitlementLevel.FULL_MEMBERS_ONLY.value
    ADMIN = SlackRuntimeEntitlementLevel.ADMINS_ONLY.value
    OWNER = SlackRuntimeEntitlementLevel.OWNERS_ONLY.value
    PRIMARY_OWNER = SlackRuntimeEntitlementLevel.PRIMARY_OWNER_ONLY.value


@xray.capture()
class SlackStatement(UserDict):
    """SlackStatement is a text wrapper object. Upon initialization, it converts
    the statement into a series of useful formats.

    Given the example:

    "<@B1234522|foo> _qualys agent report please\u00a0SVC01006\u00a0for\u00a0prd\u00a0in\u00a0012597783113"

    Would translate into
    {
        "raw": "<@B1234522|foo> _qualys agent report please\u00a0SVC01006\u00a0for\u00a0prd\u00a0in\u00a0012597783113",
        "normalized": "<@B1234522|foo> _qualys agent report please_ SVC01006 _for_ prd _in_ 012597783113",
        "lower": "<@B1234522|foo> _qualys agent report please_ svc01006 _for_ prd _in_ 012597783113",
        "mentionless": "_qualys agent report please_ svc01006 _for_ prd _in_ 012597783113",
        "plain_text": "qualys agent report please svc01006 for prd in 012597783113",
        "words": ["qualys","agent","report","please","svc01006","for","prd","in","012597783113"]
    }

    Parameters
    ----------
    statement : str
        Unicode string statement
    """
    @xray.capture()
    @logger.log
    def __init__(self, statement: str) -> None:
        """ Validate we can process statements easily

        >>> os.environ['STACK_NAME'] = 'iobots-stack'

        >>> testing.export_secret_arns(os.environ['STACK_NAME'])

        >>> statement: SlackStatement = SlackStatement("<@B1234522|foo> _qualys agent report please_ SVC02222 _for_ prd _in_ 0123456789012")

        >>> statement.get('raw')
        '<@B1234522|foo> _qualys agent report please_ SVC02222 _for_ prd _in_ 0123456789012'

        >>> statement.get('lower')
        '<@b1234522|foo> _qualys agent report please_ svc02222 _for_ prd _in_ 0123456789012'

        >>> statement.get('mentionless')
        '_qualys agent report please_ svc02222 _for_ prd _in_ 0123456789012'

        >>> statement.get('plain_text')
        'qualys agent report please svc02222 for prd in 0123456789012'

        >>> statement: SlackStatement = SlackStatement("_qualys agent report please SVC02222 for prd in 0123456789012_")

        >>> statement.get('plain_text')
        'qualys agent report please svc02222 for prd in 0123456789012'

        >>> statement: SlackStatement = SlackStatement("qualys agent report please SVC02222 for prd in 0123456789012")

        >>> statement.get('plain_text')
        'qualys agent report please svc02222 for prd in 0123456789012'

        >>> statement: SlackStatement = SlackStatement("qualys agent report please SVC02222 for prd")

        >>> statement.get('plain_text')
        'qualys agent report please svc02222 for prd'

        >>> statement: SlackStatement = SlackStatement("qualys agent report please SVC02222")

        >>> statement.get('plain_text')
        'qualys agent report please svc02222'

        >>> statement: SlackStatement = SlackStatement("_qualys agent report please_ SVC02222")

        >>> statement.get('plain_text')
        'qualys agent report please svc02222'

        >>> statement: SlackStatement = SlackStatement("qualys agent report please")

        >>> statement.get('plain_text')
        'qualys agent report please'

        Validate unicode character handling
        >>> statement: SlackStatement = SlackStatement("_qualys agent report please\u00a0SVC01006\u00a0for\u00a0prd\u00a0in\u00a0012597783113_")

        >>> statement.get('plain_text')
        'qualys agent report please svc01006 for prd in 012597783113'

        >>> testing.unexport_secret_arns()
        >>> del os.environ['STACK_NAME']
        """
        self.data = {
            "raw": statement,
            "normalized": "",
            "lower": "",
            "mentionless": "",
            "plain_text": "",
            "words": []
        }

        if statement:
            normalized: str = ""
            if statement.isprintable():
                normalized = statement
            else:
                normalized = unicodedata.normalize('NFKD', statement)

            lower: str = normalized.lower()
            mention: re.Match = re.match(r'^<@(\w+)(?:\|\w+)?>\s*', lower)
            mentionless: str = re.sub(r'^<@(\w+)(?:\|\w+)?>\s*', '', lower)
            plain_text: str = re.sub(r'[^A-Za-z0-9\- ]', '', mentionless)

            self.data["lower"] = lower
            self.data["normalized"] = normalized
            self.data["mentionless"] = mentionless
            self.data["plain_text"] = plain_text
            self.data["words"] = plain_text.lstrip().split(' ')
            if mention:
                self.data["mention"] = mention.group(0)


    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()
    def __to_dict__(self):
        return self.data.__dict__()

    @xray.capture()
    def get(self, key):
        return self.data.get(key)

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

    def __repr__(self) -> str:
        """Return a string representation of the object"""
        return f"SlackStatement({self.data.get('raw')})"


@xray.capture()
class SlackHomePageView(UserDict):

    @xray.capture()
    @logger.log
    def __init__(self,
                 log,
                 client: slack_sdk.WebClient,
                 title: str = "",
                 callback_id: str = "",
                 blocks: List[dict] = None,
                 **kwargs):

        super().__init__(**kwargs)

        if not blocks:
            blocks = []

        self._log = log
        self.client = client
        self.data = {
            'type': 'home',
            'blocks': blocks
        }

        if title != "":
            self.data['title'] = {
                'type': 'plain_text',
                'text': f'{title}'
            }

    @xray.capture()
    @logger.log
    def add_block(self, block):
        self.data['blocks'].append(block)

    @xray.capture()
    @logger.log
    def publish(self, user_id: str, hash_id: str = ""):
        response = self.client.views_publish(
            user_id=user_id,
            hash_id=hash_id,
            view=json.dumps(self.data)
        )
        return response

    @xray.capture()
    @logger.log
    def push(self, trigger_id: str):
        response = self.client.views_push(
            trigger_id=trigger_id,
            view=json.dumps(self.data)
        )
        return response

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

    def __getitem__(self, key):
        return self.__getattribute__(key)
    
    def __repr__(self) -> str:
        """Return a string representation of the object"""
        return f"{type(self)}"

@xray.capture()
class SlackModalView(UserDict):
    @xray.capture()
    @logger.log
    def __init__(self,
                 log,
                 client: slack_sdk.WebClient,
                 callback_id: str = "",
                 title: str = "",
                 close: str = "Close",
                 blocks: List[dict] = None,
                 metadata: dict = None,
                 request: dict = None,
                 **kwargs):

        super().__init__(**kwargs)

        if blocks is None:
            blocks = []

        self._log = log
        self.client = client
        self.data = {
            'type': 'modal',
            'callback_id': f'{callback_id}',
            'title': {
                'type': 'plain_text',
                'text': f'{title}'
            },
            'close': {
                'type': 'plain_text',
                'text': f'{close}',
                'emoji': True
            }
        }
        self.data['blocks'] = blocks
        self.metadata = metadata if metadata is not None else {}
        self.request = request

    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

    def __to_dict__(self):
        return self.data.__dict__()

    def __repr__(self) -> str:
        """Return a string representation of the object"""
        return f"{type(self)}"

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

    @xray.capture()
    @logger.log
    def Blocks(self) -> list:
        return self.data['blocks']

    @xray.capture()
    @logger.log
    def Metadata(self) -> dict:
        return self.metadata

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

    @xray.capture()
    @logger.log
    def add_block(self, block):
        self.data['blocks'].append(block)

    @xray.capture()
    @logger.log
    def add_blocks(self, blocks: list):
        for block in blocks:
            self.data['blocks'].append(block)

    @xray.capture()
    @logger.log
    def add_submit_button(self, title="Submit"):
        self.data['submit'] = {
            'type': 'plain_text',
            'text': f'{title}',
            'emoji': True
        }

    @xray.capture()
    @logger.log
    def add_metadata(self, metadata):
        """Merge provided metadata with existing metadata"""
        self.metadata.update(metadata)

    @xray.capture()
    @logger.log
    def SetResponseAction(self):
        self.data['response_action'] = 'update'

    @xray.capture()
    @logger.log
    def open(self, trigger_id: str):
        response = self.client.views_open(
            trigger_id=trigger_id,
            view=self.toJSON()
        )
        return response

    @xray.capture()
    @logger.log
    def update(self, view_id: str, hash_id: str):
        response = self.client.views_update(
            view_id=view_id,
            hash=hash_id,
            view=self.toJSON()
        )
        return response

    @xray.capture()
    @logger.log
    def publish(self, user_id: str, hash_id: str):
        response = self.client.views_publish(
            user_id=user_id,
            hash_id=hash_id,
            view=self.toJSON()
        )
        self._log.debug(f"response: {response}")
        return response

    @xray.capture()
    @logger.log
    def push(self, trigger_id: str):
        response = self.client.views_push(
            trigger_id=trigger_id,
            view=self.toJSON()
        )
        self._log.debug(f"response: {response}")
        return response

    @xray.capture()
    @logger.log
    def toJSON(self):
        if self.metadata:
            self.data['private_metadata'] = json.dumps(self.metadata)
        return json.dumps(self.data, default=lambda o: o.__dict__,
                          sort_keys=True, indent=2)

    @xray.capture()
    @logger.log
    def to_slack_message(self):
        keys: list = ['type', 'title', 'close', 'callback_id']
        copy = self

        for attr in keys:
            del copy.data[attr]

        # since we conditionally add
        if copy.data.get('submit'):
            del copy.data['submit']

        if self.metadata:
            copy.data['private_metadata'] = json.dumps(self.metadata)
        return copy

@xray.capture()
class Agent(object):
    """Initialize a copy of iobot.slack.Agent() for use

    Parameters
    ----------
    log : logging.Logger()
        Traditional python logger
    session : boto3.session.Session()
        Boto3 Session Object
    client : slack_sdk.WebClient()
        Slack WebClient
    config : configparser.ConfigParser()
        Per https://docs.python.org/2.0/ref/function.html set config
        explicitly to None and evaluate how it should be set if no value
        is passed. See iobot.utils for more information.
    config_file: str
        Defaults to iobot.ini
        If config is not passed, we will attempt to load config from this file.
    entitlement_level: int
        Defaults to 'SlackRuntimeEntitlementLevel.FULL_MEMBERS_ONLY'.
        Raise to restrict further or lower to relax.

        Maps to slack users.info settings:
        - SlackRuntimeEntitlementLevel.UNRESTRICTED (Lvl1):
            Used for testing and debugging purposes. Supports NoneType users.

            Note: Relaxes all entitlement checks for the slack client

        - SlackRuntimeEntitlementLevel.PERMIT_GUESTS (Lvl2):
            Requires Guest - determined via is_restricted()

            Note: Both Admin and Owners (including Primary) will test true

        - SlackRuntimeEntitlementLevel.FULL_MEMBERS_ONLY (Lvl3):
            Requires Full Member - determined via is_full_member_user()

            Note: Both Admin and Owners (including Primary) will also test true

        - SlackRuntimeEntitlementLevel.ADMINS_ONLY (Lvl4)
            Requires Admin - determined via is_admin_user()

            Note: Both Owner and Primary Owner will test true

        - SlackRuntimeEntitlementLevel.OWNERS_ONLY (Lvl5)
            Requires Owner - determined via is_owner_user()

            Note: Both Owner and Primary Owner will test true

        - SlackRuntimeEntitlementLevel.PRIMARY_OWNER_ONLY (Lvl6)
            Requires Primary Owner - determined via is_primary_owner_user()

    slack_user_id: str
        Slack user id. This may seem counter-intuitive, but the expectation is
        that this API will be used im a slack context and so the payload
        will contain a valid id.

        If you don't have a valid user, set the entitlement level to
        SlackRuntimeEntitlementLevel.UNRESTRICTED

  Raises
  ------

  NoneTypeUserError : SlackException
    When entitlement_level != SlackRuntimeEntitlementLevel.UNRESTRICTED

  InvalidAccountTypeError : SlackException
     when entitlement_level > effective_level

  UnAuthorizedUserGroupError : SlackException
      when not 'is_member_of_authorized_user_group'
      Not implemented for crud yet
    """
    @xray.capture()
    @logger.log
    def __init__(self,
                 log: Dict,
                 session: Dict,
                 client: Dict,
                 config: Dict = None,
                 config_file: str = 'iobot.ini',
                 entitlement_level: int = SlackRuntimeEntitlementLevel.FULL_MEMBERS_ONLY,  # noqa: E501
                 slack_user_id: str = "") -> None:
        """Validate we can create an instance of an Agent object

        Testing
        -------
        Setup testing environment
        >>> os.environ['STACK_NAME'] = 'iobots-stack'
        >>> testing.export_secret_arns(os.environ['STACK_NAME'])

        Create an Agent object and assert its config
        >>> client = slack_sdk.WebClient(token=testing.slack_bearer_token())

        # noqa: E501
        >>> guest_user = {'id': 'U0MF1NEAZ', 'team_id': 'T03QUQY6W', 'name': 'carl.hoffman', 'deleted': False, 'color': '8f4a2b', 'real_name': 'Carl Hoffman', 'tz': 'Australia/Canberra', 'tz_label': 'Australian Eastern Daylight Time', 'tz_offset': 39600, 'profile': {'title': '', 'phone': '', 'skype': '', 'real_name': 'Carl Hoffman', 'real_name_normalized': 'Carl Hoffman', 'display_name': 'carl.hoffman', 'display_name_normalized': 'carl.hoffman', 'fields': None, 'status_text': '', 'status_emoji': '', 'status_expiration': 0, 'avatar_hash': 'gd6eced9b512', 'guest_invited_by': 'U03QUQY7G', 'email': 'carl.hoffman@pearson.com', 'first_name': 'Carl', 'last_name': 'Hoffman', 'image_24': 'https://secure.gravatar.com/avatar/dd6eced9b512f4207cf07041700d5635.jpg?s=24&d=https%3A%2F%2Fa.slack-edge.com%2Fdf10d%2Fimg%2Favatars%2Fava_0001-24.png', 'image_32': 'https://secure.gravatar.com/avatar/dd6eced9b512f4207cf07041700d5635.jpg?s=32&d=https%3A%2F%2Fa.slack-edge.com%2Fdf10d%2Fimg%2Favatars%2Fava_0001-32.png', 'image_48': 'https://secure.gravatar.com/avatar/dd6eced9b512f4207cf07041700d5635.jpg?s=48&d=https%3A%2F%2Fa.slack-edge.com%2Fdf10d%2Fimg%2Favatars%2Fava_0001-48.png', 'image_72': 'https://secure.gravatar.com/avatar/dd6eced9b512f4207cf07041700d5635.jpg?s=72&d=https%3A%2F%2Fa.slack-edge.com%2Fdf10d%2Fimg%2Favatars%2Fava_0001-72.png', 'image_192': 'https://secure.gravatar.com/avatar/dd6eced9b512f4207cf07041700d5635.jpg?s=192&d=https%3A%2F%2Fa.slack-edge.com%2Fdf10d%2Fimg%2Favatars%2Fava_0001-192.png', 'image_512': 'https://secure.gravatar.com/avatar/dd6eced9b512f4207cf07041700d5635.jpg?s=512&d=https%3A%2F%2Fa.slack-edge.com%2Fdf10d%2Fimg%2Favatars%2Fava_0001-512.png', 'status_text_canonical': '', 'team': 'T03QUQY6W'}}

        >>> full_member_user = {'id': 'UBWG4S6NT', 'team_id': 'T03QUQY6W', 'name': 'smriti.manral', 'deleted': False, 'color': '235e5b', 'real_name': 'Smriti Manral', 'tz': 'America/Chicago', 'tz_label': 'Central Standard Time', 'tz_offset': -21600, 'profile': { 'title': '', 'phone': '', 'skype': '', 'real_name': 'Smriti Manral', 'real_name_normalized': 'Smriti Manral', 'display_name': 'Smriti Manral (SRE Team1)', 'display_name_normalized': 'Smriti Manral (SRE Team1)', 'fields': None, 'status_text': '', 'status_emoji': '', 'status_expiration': 0, 'avatar_hash': 'gd61bdfc64c4', 'email': 'smriti.manral@pearson.com', 'first_name': 'Smriti', 'last_name': 'Manral', 'image_24': 'https://secure.gravatar.com/avatar/ad61bdfc64c4a159f76e1de74cafae2f.jpg?s=24&d=https%3A%2F%2Fa.slack-edge.com%2Fdf10d%2Fimg%2Favatars%2Fava_0018-24.png', 'image_32': 'https://secure.gravatar.com/avatar/ad61bdfc64c4a159f76e1de74cafae2f.jpg?s=32&d=https%3A%2F%2Fa.slack-edge.com%2Fdf10d%2Fimg%2Favatars%2Fava_0018-32.png', 'image_48': 'https://secure.gravatar.com/avatar/ad61bdfc64c4a159f76e1de74cafae2f.jpg?s=48&d=https%3A%2F%2Fa.slack-edge.com%2Fdf10d%2Fimg%2Favatars%2Fava_0018-48.png', 'image_72': 'https://secure.gravatar.com/avatar/ad61bdfc64c4a159f76e1de74cafae2f.jpg?s=72&d=https%3A%2F%2Fa.slack-edge.com%2Fdf10d%2Fimg%2Favatars%2Fava_0018-72.png', 'image_192': 'https://secure.gravatar.com/avatar/ad61bdfc64c4a159f76e1de74cafae2f.jpg?s=192&d=https%3A%2F%2Fa.slack-edge.com%2Fdf10d%2Fimg%2Favatars%2Fava_0018-192.png', 'image_512': 'https://secure.gravatar.com/avatar/ad61bdfc64c4a159f76e1de74cafae2f.jpg?s=512&d=https%3A%2F%2Fa.slack-edge.com%2Fdf10d%2Fimg%2Favatars%2Fava_0018-512.png', 'status_text_canonical': '', 'team': 'T03QUQY6W' }, 'is_admin': False, 'is_owner': False, 'is_primary_owner': False, 'is_restricted': False, 'is_ultra_restricted': False, 'is_bot': False, 'is_app_user': False, 'updated': 1605024211 }

        >>> admin_user = {'id': 'U2JLW7KBK', 'team_id': 'T03QUQY6W', 'name': 'mike.feineman', 'deleted': False, 'color': '965d1b', 'real_name': 'Mike Feineman', 'tz': 'America/Denver', 'tz_label': 'Mountain Standard Time', 'tz_offset': -25200, 'profile': { 'title': 'SRE Manager', 'phone': '3035131355', 'skype': '', 'real_name': 'Mike Feineman', 'real_name_normalized': 'Mike Feineman', 'display_name': 'mike.feineman', 'display_name_normalized': 'mike.feineman', 'fields': None, 'status_text': '', 'status_emoji': '', 'status_expiration': 0, 'avatar_hash': 'c8fa687ac034', 'image_original': 'https://avatars.slack-edge.com/2016-10-04/87234051862_c8fa687ac034a79ed05a_original.jpg', 'is_custom_image': True, 'email': 'mike.feineman@pearson.com', 'first_name': 'Mike', 'last_name': 'Feineman', 'image_24': 'https://avatars.slack-edge.com/2016-10-04/87234051862_c8fa687ac034a79ed05a_24.jpg', 'image_32': 'https://avatars.slack-edge.com/2016-10-04/87234051862_c8fa687ac034a79ed05a_32.jpg', 'image_48': 'https://avatars.slack-edge.com/2016-10-04/87234051862_c8fa687ac034a79ed05a_48.jpg', 'image_72': 'https://avatars.slack-edge.com/2016-10-04/87234051862_c8fa687ac034a79ed05a_72.jpg', 'image_192': 'https://avatars.slack-edge.com/2016-10-04/87234051862_c8fa687ac034a79ed05a_192.jpg', 'image_512': 'https://avatars.slack-edge.com/2016-10-04/87234051862_c8fa687ac034a79ed05a_192.jpg', 'image_1024': 'https://avatars.slack-edge.com/2016-10-04/87234051862_c8fa687ac034a79ed05a_192.jpg', 'status_text_canonical': '', 'team': 'T03QUQY6W' }, 'is_admin': True, 'is_owner': False, 'is_primary_owner': False, 'is_restricted': False, 'is_ultra_restricted': False, 'is_bot': False, 'is_app_user': False, 'updated': 1604645947 }

        >>> owner_user = {'id': 'U0EKDSZE2', 'team_id': 'T03QUQY6W', 'name': 'john.torres', 'deleted': False, 'color': 'b14cbc', 'real_name': 'John Torres', 'tz': 'America/New_York', 'tz_label': 'Eastern Standard Time', 'tz_offset': -18000, 'profile': {'title': 'Talks about "john"', 'phone': '646-470-4508', 'skype': '', 'real_name': 'John Torres', 'real_name_normalized': 'John Torres', 'display_name': 'john.torres', 'display_name_normalized': 'john.torres', 'fields': None, 'status_text': '', 'status_emoji': '', 'status_expiration': 0, 'avatar_hash': 'eb7551a3fb99', 'image_original': 'https://avatars.slack-edge.com/2020-02-26/972542942199_eb7551a3fb99c3c2c85e_original.png', 'is_custom_image': True, 'email': 'john.torres@pearson.com', 'first_name': 'John', 'last_name': 'Torres', 'image_24': 'https://avatars.slack-edge.com/2020-02-26/972542942199_eb7551a3fb99c3c2c85e_24.png', 'image_32': 'https://avatars.slack-edge.com/2020-02-26/972542942199_eb7551a3fb99c3c2c85e_32.png', 'image_48': 'https://avatars.slack-edge.com/2020-02-26/972542942199_eb7551a3fb99c3c2c85e_48.png', 'image_72': 'https://avatars.slack-edge.com/2020-02-26/972542942199_eb7551a3fb99c3c2c85e_72.png', 'image_192': 'https://avatars.slack-edge.com/2020-02-26/972542942199_eb7551a3fb99c3c2c85e_192.png', 'image_512': 'https://avatars.slack-edge.com/2020-02-26/972542942199_eb7551a3fb99c3c2c85e_512.png', 'image_1024': 'https://avatars.slack-edge.com/2020-02-26/972542942199_eb7551a3fb99c3c2c85e_1024.png', 'status_text_canonical': '', 'team': 'T03QUQY6W'}, 'is_admin': True, 'is_owner': True, 'is_primary_owner': False, 'is_restricted': False, 'is_ultra_restricted': False, 'is_bot': False, 'is_app_user': False, 'updated': 1605133744}

        >>> primary_owner_user = {'id': 'U0B6BMGFR', 'team_id': 'T03QUQY6W', 'name': 'ian.wright', 'deleted': False, 'color': '619a4f', 'real_name': 'Ian Wright', 'tz': 'America/Chicago', 'tz_label': 'Central Standard Time', 'tz_offset': -21600, 'profile': { 'title': '', 'phone': '210 884-1208', 'skype': '', 'real_name': 'Ian Wright', 'real_name_normalized': 'Ian Wright', 'display_name': 'Ian', 'display_name_normalized': 'Ian', 'fields': None, 'status_text': '', 'status_emoji': '', 'status_expiration': 0, 'avatar_hash': 'fb34309f52e8', 'image_original': 'https://avatars.slack-edge.com/2020-08-24/1317942675573_fb34309f52e81bde72b6_original.png', 'is_custom_image': True, 'email': 'ian.wright@pearson.com', 'first_name': 'Ian', 'last_name': 'Wright', 'image_24': 'https://avatars.slack-edge.com/2020-08-24/1317942675573_fb34309f52e81bde72b6_24.png', 'image_32': 'https://avatars.slack-edge.com/2020-08-24/1317942675573_fb34309f52e81bde72b6_32.png', 'image_48': 'https://avatars.slack-edge.com/2020-08-24/1317942675573_fb34309f52e81bde72b6_48.png', 'image_72': 'https://avatars.slack-edge.com/2020-08-24/1317942675573_fb34309f52e81bde72b6_72.png', 'image_192': 'https://avatars.slack-edge.com/2020-08-24/1317942675573_fb34309f52e81bde72b6_192.png', 'image_512': 'https://avatars.slack-edge.com/2020-08-24/1317942675573_fb34309f52e81bde72b6_512.png', 'image_1024': 'https://avatars.slack-edge.com/2020-08-24/1317942675573_fb34309f52e81bde72b6_1024.png', 'status_text_canonical': '', 'team': 'T03QUQY6W' }, 'is_admin': True, 'is_owner': True, 'is_primary_owner': True, 'is_restricted': False, 'is_ultra_restricted': False, 'is_bot': False, 'is_app_user': False, 'updated': 1604613938 }

        Test default 'entitlement_level' and 'NoneType' user
        >>> agent = Agent(log,boto_session,client, config_file='environments/unit.ini')
        Traceback (most recent call last):
        ...
        iobot.slack.NoneTypeUserError: Only 'Valid' slack accounts are authorized

        Test default 'entitlement_level' of 'FULL_MEMBERS_ONLY' and 'guest' user
        >>> agent = Agent(log,boto_session,client, config_file='environments/unit.ini', slack_user_id=guest_user.get('id'))
        Traceback (most recent call last):
        ...
        iobot.slack.InvalidAccountTypeError: user's entitlement 'SlackUserEntitlementLevel.MULTI_CHANNEL_GUEST' has insufficient priviledge for runtime setting of 'SlackRuntimeEntitlementLevel.FULL_MEMBERS_ONLY'

        Test 'entitlement_level' of 'OWNERS_ONLY' with each user type
        >>> agent = Agent(log,boto_session,client, config_file='environments/unit.ini', entitlement_level=SlackRuntimeEntitlementLevel.OWNERS_ONLY, slack_user_id=full_member_user.get('id'))
        Traceback (most recent call last):
        ...
        iobot.slack.InvalidAccountTypeError: user's entitlement 'SlackUserEntitlementLevel.FULL_MEMBER' has insufficient priviledge for runtime setting of 'SlackRuntimeEntitlementLevel.OWNERS_ONLY'

        >>> agent = Agent(log,boto_session,client, config_file='environments/unit.ini', entitlement_level=SlackRuntimeEntitlementLevel.OWNERS_ONLY, slack_user_id=admin_user.get('id'))
        Traceback (most recent call last):
        ...
        iobot.slack.InvalidAccountTypeError: user's entitlement 'SlackUserEntitlementLevel.ADMIN' has insufficient priviledge for runtime setting of 'SlackRuntimeEntitlementLevel.OWNERS_ONLY'

        >>> agent = Agent(log,boto_session,client, config_file='environments/unit.ini', entitlement_level=SlackRuntimeEntitlementLevel.PRIMARY_OWNER_ONLY, slack_user_id=owner_user.get('id'))
        Traceback (most recent call last):
        ...
        iobot.slack.InvalidAccountTypeError: user's entitlement 'SlackUserEntitlementLevel.OWNER' has insufficient priviledge for runtime setting of 'SlackRuntimeEntitlementLevel.PRIMARY_OWNER_ONLY'

        >>> agent = Agent(log,boto_session,client, config_file='environments/unit.ini', entitlement_level=SlackRuntimeEntitlementLevel.OWNERS_ONLY, slack_user_id=primary_owner_user.get('id'))

        Test 'entitlement_level' of 'UNRESTRICTED' with 'NoneType' user does not throw an exception
        >>> agent = Agent(log,boto_session,client, config_file='environments/unit.ini', entitlement_level=SlackRuntimeEntitlementLevel.UNRESTRICTED, slack_user_id=None)

        >>> agent.config['Slack']['app_id']
        'A015B50KKAA'

        Read back a secret arn
        >>> assert(agent.config['Slack']['signing_secret_arn'].startswith('arn:aws')), f"expected 'signing_secret_arn' to start with 'arn:aws', got {agent.config['Slack']['signing_secret_arn']}"

        Tear down testing environment
        >>> testing.unexport_secret_arns()

        >>> del os.environ['STACK_NAME']
        """
        if not config:
            config = init_config(config_file=config_file, env=dict(os.environ))

        self.config = config
        self.stack_name = get_envvar("STACK_NAME")
        self._log = log
        self._session = session
        self._secrets = SecretsManager(log, session)
        self._home_page_view = SlackHomePageView(log, client)
        self._user_token_client = {}

        if client:
            self.client = client
        else:
            self.client = slack_sdk.WebClient(
                token=self._secrets.GetToken(config['Slack'].get('bearer_token_arn')))

        effective_level = SlackUserEntitlementLevel.UNAUTHORIZED

        if entitlement_level.value > \
           SlackRuntimeEntitlementLevel.UNRESTRICTED.value:

            if not slack_user_id:
                raise NoneTypeUserError(
                    "Only 'Valid' slack accounts are authorized")

            user = self.get_user_by_id(slack_user_id)
            if self.is_restricted_user(slack_user_id, user):
                effective_level = SlackUserEntitlementLevel.MULTI_CHANNEL_GUEST

            if self.is_full_member_user(slack_user_id, user):
                effective_level = SlackUserEntitlementLevel.FULL_MEMBER

            if self.is_admin_user(slack_user_id, user):
                effective_level = SlackUserEntitlementLevel.ADMIN

            if self.is_owner_user(slack_user_id, user):
                effective_level = SlackUserEntitlementLevel.OWNER

            if self.is_primary_owner_user(slack_user_id, user):
                effective_level = SlackUserEntitlementLevel.PRIMARY_OWNER

            if entitlement_level.value > effective_level.value:
                raise InvalidAccountTypeError(
                    f"user's entitlement '{effective_level}' has insufficient priviledge for runtime setting of '{entitlement_level}'")  # noqa: E501

    def __repr__(self) -> str:
        """Return a string representation of the object"""
        return f"{type(self)}"

    @xray.capture()
    @logger.log
    @staticmethod
    def entitlement_levels():
        return SlackRuntimeEntitlementLevel

    @xray.capture()
    @logger.log
    def archive_channels(self, archive_list: list = None) -> Tuple:
        """Archive channels based on a list of IDs

        Parameters
        ==========
        archive_list : list of IDs
            List of channels to archive
            E.g. ['C1234567']

        Returns
        =======
        Tuple : list of responses from slack for each archival request

        Testing
        =======

        An example of archiving channels:

        ** Setup testing environment **
        >>> skip('delete is only valid for grid and so this test case must be run manually')
        >>> os.environ['STACK_NAME'] = 'iobots-stack'

        >>> testing.export_secret_arns(os.environ['STACK_NAME'])

        ** Create an Agent object **
        >>> client = slack_sdk.WebClient(token=testing.slack_bearer_token())

        >>> agent = Agent(log,boto_session,client, config_file='environments/unit.ini')

        >>> channels: list = ['C01AD8GH9S9','C01ANGR3FN0','C01234567N0']

        ** Archive Channels **
        >>> archive_responses = agent.archive_channels(archive_list=channels)

        >>> archive_responses[0].get('id')
        'C01AD8GH9S9'

        >>> archive_responses[0].get('ok')
        True

        >>> archive_responses[2].get('id')
        'C01234567N0'

        >>> archive_responses[2].get('ok')
        False

        >>> testing.unexport_secret_arns()
        >>> del os.environ['STACK_NAME']
        """
        if not archive_list:
            archive_list = []

        responses: list = []
        for channel in archive_list:
            self._log.debug(f"joining channel: {channel}")
            try:
                response = self.client.conversations_join(channel=channel)
                if not response['ok']:
                    self._log.debug(f"failed joining: {channel}")
                    responses.append(response)
                    continue

                time.sleep(randint(0, 3))  # to avoid api rate-limit issues
                self._log.debug(f"archiving channel: {channel}")
                response = self.client.conversations_archive(channel=channel)
                responses.append(
                    {'id': channel, 'ok': (response.get('ok') or False)})
            except Exception as err:
                responses.append(
                    {'id': channel, 'ok': False, 'error': err})

        return responses

    # backward compatible alias to archive_channels - deprecate later...
    archive = archive_channels

    @xray.capture()
    @logger.log
    def unarchive_channels(self, unarchive_list: list = None) -> Tuple:
        """Unarchive channels based on a list of IDs

        Note: Requires a user token with channels:write

        Parameters
        ==========
        unarchive_list : list of IDs
            List of channels to unarchive
            E.g. ['C1234567']

        Returns
        =======
        Tuple : list of responses from slack for each archival request

        Testing
        =======

        An example of unarchiving channels:

        ** Setup testing environment **
        >>> skip('requires user token')
        >>> os.environ['STACK_NAME'] = 'iobots-stack'

        >>> testing.export_secret_arns(os.environ['STACK_NAME'])

        ** Create an Agent object **
        >>> client = slack_sdk.WebClient(token=testing.slack_bearer_token())

        >>> user_client = slack_sdk.WebClient(token=testing.slack_user_token())

        >>> agent = Agent(log,boto_session,client, config_file='environments/unit.ini')

        >>> agent.add_user_token_client(user_client)

        >>> channels: list = ['C01AD8GH9S9','C01ANGR3FN0','C01234567N0']

        ** Unarchive Channels **
        >>> unarchive_responses = agent.unarchive_channels(unarchive_list=channels)

        >>> unarchive_responses[0].get('id')
        'C01AD8GH9S9'

        >>> unarchive_responses[0].get('ok')
        True

        >>> unarchive_response[2].get('id')
        'C01234567N0'

        >>> unarchive_response[2].get('ok')
        False

        >>> testing.unexport_secret_arns()
        >>> del os.environ['STACK_NAME']
        """
        if not unarchive_list:
            unarchive_list = []

        responses: list = []
        for channel in unarchive_list:
            self._log.debug(f"joining channel: {channel}")
            try:
                time.sleep(randint(0, 3))  # to avoid api rate-limit issues
                self._log.debug(f"unarchiving channel: {channel}")
                response = self._user_token_client.conversations_unarchive(
                    channel=channel)
                responses.append(
                    {'id': channel, 'ok': (response.get('ok') or False)})
            except Exception as err:
                self._log.error(f'failed to unarchive the channel: {err}')
                responses.append(
                    {'id': channel, 'ok': False})

        return responses

    @xray.capture()
    @logger.log
    def create_channels(self, channel_list: list = None, invite_members: bool = False) -> Tuple:
        """Creates channels and invites users based on a list of mappings

        Note: inviting members is currently not implemented


        Parameters
        ==========
        channel_list : list of dictionaries
            List of channels to create
            E.g. [{'name':'mynewchannel','description':'my new channel','members':['U1233455969']}]

        Returns
        =======
        Tuple : list of responses from slack for each create request

        Testing
        =======

        An example of of testing create:

        ** Setup testing environment **
        >>> skip('delete is only valid for grid and so this test case must be run manually')
        >>> os.environ['STACK_NAME'] = 'iobots-stack'
        >>> testing.export_secret_arns(os.environ['STACK_NAME'])

        ** Create an Agent object **
        >>> client = slack_sdk.WebClient(token=testing.slack_bearer_token())
        >>> agent = Agent(log,boto_session,client, config_file='environments/unit.ini')

        >>> channels: list = [{'name':'test-channel-with-members', 'is_private': False, 'description':'iobot test data - do not modify','members':['U0EKDSZE2']},{'name':'test-channel-without-members', 'is_private': False, 'description':'iobot test data - do not modify','members':[]},{'name':'jt-test-support', 'is_private': False, 'description':'existing channel, we will ignore','members':[]}]

        ** Create the channels without members **
        >>> create_responses = agent.create_channels(channel_list=channels)

        >>> create_responses[0].get('name')
        'test-channel-with-members'

        >>> create_responses[0].get('created')
        True

        >>> create_response[2].get('name')
        'jt-test-support'

        >>> create_response[2].get('skipped')
        True

        >>> testing.unexport_secret_arns()
        >>> del os.environ['STACK_NAME']
        """
        if not channel_list:
            channel_list = []

        existing_channels: list = self.list_channels()
        responses: list = []
        for channel in channel_list:
            self._log.debug(
                f"testing if channel exists: {channel.get('name')}")
            if not any(existing_channel.get('name') == channel.get('name') for existing_channel in existing_channels):
                self._log.debug(f"creating channel {channel.get('name')}")
                response = self.client.conversations_create(
                    name=channel.get('name'), is_private=channel.get('is_private'))
                if response['ok']:
                    self._log.debug(
                        f"created channel: {channel.get('name')}: {response}")
                    responses.append({'name': channel.get('name'), 'id': response.get(
                        'id'), 'ok': True, 'created': True, 'failed': False, 'skipped': False})
                else:
                    self._log.debug(
                        f"failed to create channel: {channel.get('name')}: {response}")
                    responses.append({'name': channel.get(
                        'name'), 'id': None, 'ok': False, 'created': False, 'failed': True, 'skipped': False})
            else:
                self._log.debug(
                    f"channel {channel.get('name')} exists skipping")
                responses.append({'name': channel.get(
                    'name'), 'id': None, 'ok': False, 'created': False, 'failed': False, 'skipped': True})

                continue

            time.sleep(randint(0, 3))  # to avoid api rate-limit issues

        return responses

    @xray.capture()
    @logger.log
    def delete_channels(self, channel_list: list = None) -> Tuple:
        """Delete channels based on a list of mappings

        Note: requires a user token with admin.conversations.write which is only
        valid on Grid

        Testing
        -------
        Setup testing environment
        >>> skip('this test case is only valid for grid')
        >>> os.environ['STACK_NAME'] = 'iobots-stack'
        >>> testing.export_secret_arns(os.environ['STACK_NAME'])

        Create an Agent object
        >>> client = slack_sdk.WebClient(token=testing.slack_bearer_token())
        >>> agent = Agent(log,boto_session,client, config_file='environments/unit.ini')

        >>> channels: list = [{'name':'test-channel-with-members', 'is_private': False, 'description':'iobot test data - do not modify'},{'name':'test-channel-without-members', 'is_private': False, 'description':'iobot test data - do not modify'},{'name':'test-bogus-name-delete-channel', 'is_private': False, 'description':'non-existing channel, we will ignore'}]

        Delete Channels
        >>> deleted_responses = agent.delete_channels(channel_list=channels)
        >>> deleted_responses[0].get('name')
        'test-channel-with-members'
        >>> deleted_responses[0].get('deleted')
        True
        >>> deleted_responses[2].get('name')
        'test-bogus-name-delete-channel'
        >>> deleted_responses[2].get('skipped')
        True

        >>> testing.unexport_secret_arns()
        >>> del os.environ['STACK_NAME']
        """
        if not channel_list:
            channel_list = []

        existing_channels: list = self.list_channels()
        responses: list = []
        for channel in channel_list:
            self._log.debug(
                f"testing if channel exists: {channel.get('name')}")
            if not any(existing_channel.get('name') == channel.get('name') for existing_channel in existing_channels):
                self._log.debug(
                    f"channel {channel.get('name')} does not exist, skipping")
                responses.append({'name': channel.get(
                    'name'), 'id': 'n/a', 'ok': False, 'deleted': False, 'failed': False, 'skipped': True})

                continue
            else:
                self._log.debug(f"deleting channel {channel.get('name')}")
                for channel_to_delete in existing_channels:
                    if channel_to_delete.get('name') == channel.get('name'):
                        response = self.client.admin_conversations_delete(
                            channel_id=channel_to_delete.get('id'))
                        if response['ok']:
                            self._log.debug(
                                f"deleted channel: {channel.get('name')}: {response}")
                            responses.append({'name': channel_to_delete.get('name'), 'id': channel_to_delete.get(
                                'id'), 'ok': True, 'deleted': True, 'failed': False, 'skipped': False})
                        else:
                            self._log.debug(
                                f"failed to delete channel: {channel.get('name')}: {response}")
                            responses.append({'name': channel_to_delete.get('name'), 'id': channel_to_delete.get(
                                'id'), 'ok': False, 'deleted': False, 'failed': True, 'skipped': False})

                        # to avoid api rate-limit issues
                        time.sleep(randint(0, 3))

        return responses

    @xray.capture()
    @logger.log
    def get_home_page_view(self, **kwargs):
        return SlackHomePageView(self._log, self.client, **kwargs)

    @xray.capture()
    @logger.log
    def get_modal_page_view(self, **kwargs):
        return SlackModalView(self._log, self.client, **kwargs)

    @xray.capture()
    @logger.log
    def add_user_token_client(self, agent: Dict) -> None:
        self._log.debug("added user_token client")
        self._user_token_client = agent
        return

    @xray.capture()
    @logger.log
    def get_user_by_id(self, id) -> Dict:
        try:
            user = self.client.users_info(user=id)
            return user['user']
        except Exception as e:
            self._log.error(f"error: '{e}'")
            return {}

    @xray.capture()
    @logger.log
    def get_user_by_email(self, email) -> Dict:
        try:
            user = self.client.users_lookupByEmail(email=email)
            return user['user']
        except Exception as e:
            self._log.error(f"error: '{e}'")
            return {}

    @xray.capture()
    @logger.log
    def get_usergroup_by_id(self, group_id) -> Dict:
        team = self.client.usergroups_users_list(usergroup=group_id)
        return team

    @xray.capture()
    @logger.log
    def get_user_ids_by_usergroup(self, group_id) -> Dict:
        team = self.client.usergroups_users_list(usergroup=group_id)
        return team['users']

    @xray.capture()
    @logger.log
    def get_usergroup_multiselect_view_block_input(self, usergroups,
                                                   block_id="impacted_teams_aka_who",
                                                   label="Who is impacted by this request?",
                                                   placeholder="Select impacted teams") -> Dict:
        block = {
            "type": "input",
            "block_id": f"{block_id}",
            "label": {
                "type": "plain_text",
                "text": f"{label}"
            },
            "element": {
                "type": "multi_static_select",
                "action_id": "account_selections",
                "placeholder": {
                    "type": "plain_text",
                    "text": f"{placeholder}"
                },
                "options": []
            }
        }
        sorted_groups = sorted(
            usergroups, key=lambda o: o['handle'], reverse=False)
        for count, usergroup in enumerate(sorted_groups, start=1):
            if count < 100:
                block['element']['options'].append({
                    "text": {
                        "type": "plain_text",
                        "text": f"{usergroup['name'].title()} (<@{usergroup['handle']}>)"
                    },
                    "value": f"{usergroup['id']}"
                })
        return block

    @xray.capture()
    @logger.log
    def get_usergroup_multiselect_view_block_section(self, usergroups,
                                                     block_id="impacted_teams_aka_who",
                                                     action_id="account_selections",
                                                     label="Who is impacted by this request?",
                                                     placeholder="Select impacted teams") -> Dict:
        block = {
            "type": "section",
            "block_id": f"{block_id}",
            "text": {
                "type": "mrkdwn",
                "text": f"{label}"
            },
            "accessory": {
                "type": "multi_static_select",
                "action_id": f"{action_id}",
                "placeholder": {
                    "type": "plain_text",
                    "text": f"{placeholder}"
                },
                "options": []
            }
        }
        sorted_groups = sorted(
            usergroups, key=lambda o: o['handle'], reverse=False)
        for count, usergroup in enumerate(sorted_groups, start=1):
            if count < 100:
                block['accessory']['options'].append({
                    "text": {
                        "type": "plain_text",
                        "text": f"{usergroup['name'].title()} (<@{usergroup['handle']}>)"
                    },
                    "value": f"{usergroup['id']}"
                })
        return block

    @xray.capture()
    @logger.log
    def get_usergroup_select_view_block_section(self, usergroups,
                                                block_id="usergroup_select_view",
                                                action_id="usergroups_select_view",
                                                label="Who is impacted by this request?",
                                                placeholder="Select impacted teams") -> Dict:
        block = {
            "type": "section",
            "block_id": f"{block_id}",
            "text": {
                "type": "mrkdwn",
                "text": f"{label}"
            },
            "accessory": {
                "type": "static_select",
                "action_id": f"{action_id}",
                "placeholder": {
                    "type": "plain_text",
                    "text": f"{placeholder}"
                },
                "options": []
            }
        }
        sorted_groups = sorted(
            usergroups, key=lambda o: o['handle'], reverse=False)
        for count, usergroup in enumerate(sorted_groups, start=1):
            if count < 100:
                block['accessory']['options'].append({
                    "text": {
                        "type": "plain_text",
                        "text": f"{usergroup['name'].title()} (<@{usergroup['handle']}>)"
                    },
                    "value": f"{usergroup['id']}"
                })
        return block

    @xray.capture()
    @logger.log
    def get_user_ids_by_channel(self, channel_id) -> Tuple:
        conversations = self.client.conversations_members(
            channel=channel_id, limit=2000)
        return conversations['members']

    @xray.capture()
    @logger.log
    def get_users_by_channel(self, channel_id) -> Tuple:
        conversations = self.client.conversations_members(
            channel=channel_id, limit=2000)
        users = []
        for member in conversations['members']:
            users.append(self.get_user_by_id(member))

        return users

    @xray.capture()
    @logger.log
    def get_users_by_usergroup(self, group_id) -> Tuple:
        team = self.client.usergroups_users_list(usergroup=group_id)

        users: List[Dict] = []
        for user_id in team['users']:
            user = self.get_user_by_id(user_id)
            users.append(user['user'])  # EDIT

        return users

    @xray.capture()
    @logger.log
    def get_usergroup_by_id_with_populated_users(self, group_id, group_label) -> Dict:
        usergroup = {
            'id': group_id,
            'label': group_label,
            'users': []
        }

        members = self.client.usergroups_users_list(usergroup=group_id)

        for user_id in members['users']:
            user = self.get_user_by_id(user_id)
            usergroup['users'].append(user)

        return usergroup

    @xray.capture()
    @logger.log
    def is_restricted_user(self, id, user: dict = None) -> bool:
        if not user:
            user = self.get_user_by_id(id)
        self._log.debug(
            f"user: {user}: restricted? {user.get('is_restricted')}, ultra restricted? {user.get('is_ultra_restricted')}")
        return any((user.get('is_restricted'), user.get('is_ultra_restricted')))

    @xray.capture()
    @logger.log
    def is_full_member_user(self, id, user: dict = None) -> bool:
        if not user:
            user = self.get_user_by_id(id)
        self._log.debug(
            f"user: {user}: admin? {user.get('is_admin')}, owner? {user.get('is_owner')}, primary? {user.get('is_primary_owner')}")
        return any((user.get('is_admin'), user.get('is_owner'), user.get('is_primary_owner'), not user.get('is_restricted'), not user.get('is_ultra_restricted')))

    @xray.capture()
    @logger.log
    def is_admin_user(self, id, user: dict = None) -> bool:
        if not user:
            user = self.get_user_by_id(id)
        self._log.debug(
            f"user: {user}: admin? {user.get('is_admin')}, owner? {user.get('is_owner')}, primary? {user.get('is_primary_owner')}")
        return any((user.get('is_admin'), user.get('is_owner'), user.get('is_primary_owner')))

    @xray.capture()
    @logger.log
    def is_owner_user(self, id, user: dict = None) -> bool:
        if not user:
            user = self.get_user_by_id(id)
        self._log.debug(
            f"user: {user}:  owner? {user.get('is_owner')}, primary? {user.get('is_primary_owner')}")
        return any((user.get('is_owner'), user.get('is_primary_owner')))

    @xray.capture()
    @logger.log
    def is_primary_owner_user(self, id, user: dict = None) -> bool:
        if not user:
            user = self.get_user_by_id(id)
        self._log.debug(
            f"user: {user}:  primary? {user.get('is_primary_owner')}")
        return user.get('is_primary_owner')

    @xray.capture()
    @logger.log
    def is_member_of_usergroup(self, user_id, group_id) -> bool:
        team = self.client.usergroups_users_list(usergroup=group_id)
        return user_id in team['users']

    @xray.capture()
    @logger.log
    def is_member_of_usergroup_by_name(self, user_id, group_name) -> bool:
        team = self.list_usergroups(filter=group_name)
        return user_id in team['users']

    @xray.capture()
    @logger.log
    # TODO: finish populating users for this method...
    def list_channels(self, exclude_archived: str = "true", limit: int = 2000,
                      include_members: bool = False) -> Tuple:
        """Returns a list of channel objects
        >>> os.environ['STACK_NAME'] = 'iobots-stack'
        >>> testing.export_secret_arns(os.environ['STACK_NAME'])

        >>> client = slack_sdk.WebClient(token=testing.slack_bearer_token())
        >>> agent = Agent(log,boto_session,client,config_file='environments/unit.ini', entitlement_level=Agent.entitlement_levels().UNRESTRICTED)

        Ensure we retrieve one or more records
        >>> channel_count: int = len(agent.list_channels())
        >>> assert(channel_count > 0), f"expected than zero, got {channel_count}"

        >>> testing.unexport_secret_arns()
        >>> del os.environ['STACK_NAME']
        """
        channels = self.client.conversations_list(limit=2000,
                                                  exclude_archived=exclude_archived)

        # TODO: implement include_members
        # channel_members = [{
        #   id: "",
        #   name: "",
        #   members: [],
        #   last: 0.0
        # }]

        # for channel in channels:
        #   for member in channel['members']:
        #     print(f"{channel}: {self.member}")
        #       channel
        return channels.get('channels')

    @xray.capture()
    @logger.log
    def list_usergroups(self, limit: int = 1000, filters: list = [], include_users: str = 'false') -> Tuple:
        """Create a contact object and save it to SLater
        >>> os.environ['STACK_NAME'] = 'iobots-stack'
        >>> testing.export_secret_arns(os.environ['STACK_NAME'])

        >>> client = slack_sdk.WebClient(token=testing.slack_bearer_token())
        >>> agent = Agent(log,boto_session,client,config_file='environments/unit.ini', entitlement_level=Agent.entitlement_levels().UNRESTRICTED)
        """
        groups: list = self.client.usergroups_list(limit=limit,
                                                   include_users=include_users)['usergroups']

        # self._log.debug(f'from inside list_usergroups: {groups}')
        if filters:
            filtered: list = []
            for handle in filters:
                for group in groups:
                    if handle == group.get('handle'):
                        filtered.append(group)

            return filtered
        else:
            return groups

    @xray.capture()
    @logger.log
    @staticmethod
    def render_progress_bar(percent_complete: int, current: int, total: int, caption: str = "Processing..."):
        return f'{caption} {current} of {total}:\n{round(percent_complete / 5) * chr(9608)} {percent_complete:.2f}%'

    @xray.capture()
    @logger.log
    def send(self, msg, channel, thread_ts, blocks=[]):
        status = self.client.chat_postMessage(
            text=msg,
            channel=channel,
            thread_ts=thread_ts,
            blocks=blocks
        )
        return status

    @xray.capture()
    @logger.log
    def set_reminder_for_users(self, who: List[Dict], what: str, when: str = "1") -> bool:
        self._log.debug("sending reminder to users...")
        for user in who:
            self._log.debug(f"who: {user['name']}, what: {what}, when: {when}")
            reminder = self._user_token_client.reminders_add(
                user=user['id'], text=what, time=when)
            if not reminder['ok']:
                self._log.debug(f"reminder: {reminder}")
                return False

        return True

    @xray.capture()
    @logger.log
    def upload_data_to_channel_as_file(self, content: bytes, channel_id: str, filename: str = "", content_type: str = 'csv', title: str = 'Report', comment: str = 'Requested report') -> Dict:
        """Uploads bytes data as ax file to a slack channel
        Bootstrap libraries for doctest
        >>> from io import StringIO
        >>> os.environ['STACK_NAME'] = 'iobots-stack'
        >>> testing.export_secret_arns(os.environ['STACK_NAME'])
        >>> slack_client = slack_sdk.WebClient(token=testing.slack_bearer_token())
        >>> t = Agent(log,boto_session,slack_client,config_file='environments/unit.ini', entitlement_level=Agent.entitlement_levels().UNRESTRICTED)
        >>> from io import BytesIO
        >>> data = BytesIO(bytes('Name,Description,Created,Last active,Total membership,Messages posted by members,Members who posted,Change in members who posted,Slack Channel Status\\n2019soc2audit_abbi,,13-Nov-19,19-Nov-19,11,0,0,0,Archive','utf-8'))
        >>> result = t.upload_data_to_channel_as_file(data, 'CJGRXLNRK', title='csv test export', filename='test.csv', content_type='csv')
        >>> assert (result['ok']), f'should not complain about an error {result}'
        >>> testing.unexport_secret_arns()
        >>> del os.environ['STACK_NAME']
        """
        token = self._secrets.GetToken(
            self.config['Slack']['bearer_token_arn'])
        uploaded = self.client.files_upload(
            token=token,
            channels=channel_id,
            file=content,
            initial_comment=comment,
            title=title,
            filename=filename,
            filetype=content_type
        )

        self._log.debug(uploaded)
        return uploaded

    @xray.capture()
    @logger.log
    def upload_file_to_channel(self, file_path: str, channel_id: str) -> Dict:
        """Uploads a file to a slack channel

        Bootstrap libraries for doctest
        >>> os.environ['STACK_NAME'] = 'iobots-stack'

        >>> testing.export_secret_arns(os.environ['STACK_NAME'])

        >>> slack_client = slack_sdk.WebClient(token=testing.slack_bearer_token())

        >>> t = Agent(log,boto_session,slack_client,config_file='environments/unit.ini', entitlement_level=Agent.entitlement_levels().UNRESTRICTED)

        Validate file_path exists
        >>> t.upload_file_to_channel('foobar','C12334565')
        Traceback (most recent call last):
        ...
        AssertionError: path does not exist: foobar

        >>> testing.unexport_secret_arns()

        >>> del os.environ['STACK_NAME']
        """
        error_message = f"path does not exist: {file_path}"
        assert (os.path.exists(file_path)), error_message
        with open(file_path) as file:
            content = file.read()
            uploaded = self.client.files_upload(
                channel=channel_id, content=content)

        return uploaded

    @xray.capture()
    @logger.log
    def update(self, msg, channel, message_ts, blocks=[]):
        status = self.client.chat_update(
            channel=channel,
            text=msg,
            ts=message_ts,
            blocks=blocks
        )
        return status

    # def publish(self, msg, ch)


@xray.capture()
@logger.log
class Authenticator(object):
    @logger.log
    def __init__(self, log, session, config):
        self._log = log
        self._session = session
        self._secrets_client = SecretsManager(log, session)
        self._secrets = None
        self._config = config

    @logger.log
    def get_secret(self, secret) -> str:
        if self._secrets is None:
            slack_auth_arn: str = self._config['Slack']['auth_arn']
            self._secrets = self._secrets_client.GetSecret(slack_auth_arn)
        return self._secrets.get(secret)

    @logger.log
    def validate_message(self, event, version='v0', max_drift=300):
        enabled = ["true", "yes", "on"]
        if 'SIGNING_SECRET_CHECKING_ENABLED' not in os.environ:
            os.environ['SIGNING_SECRET_CHECKING_ENABLED'] = "true"

        if 'REQUEST_TIME_CHECKING_ENABLED' not in os.environ:
            os.environ['REQUEST_TIME_CHECKING_ENABLED'] = "true"

        if os.environ['SIGNING_SECRET_CHECKING_ENABLED'] not in enabled:
            self._log.warning('Signing secret checking is currently disabled')
            return True

        slack_request_timestamp = int(
            event['headers']['X-Slack-Request-Timestamp']
        )
        slack_signature = event['headers']['X-Slack-Signature']
        body = event['body']

        slack_signing_secret = bytes(
            self.get_secret('slack_signing_secret'), 'utf-8')
        signature = bytes(
            f'{version}:{slack_request_timestamp}:{body}', 'utf-8')

        mac = hmac.new(
            slack_signing_secret,
            signature,
            digestmod=hashlib.sha256
        ).hexdigest()

        if os.environ['REQUEST_TIME_CHECKING_ENABLED'].lower() in enabled:
            time_difference = abs(time.time() - slack_request_timestamp)

            drifted = timedelta(seconds=time_difference)
            limit = timedelta(seconds=max_drift)

            if time_difference > max_drift:
                self._log.warning(
                    f'Request timestamp askew ({drifted}) exceeds limit ({limit})'
                )

                return False

        # TODO: validate version in the future in addition to digest:
        # mac_versioned = f'{version}={mac}'
        return hmac.compare_digest(mac, slack_signature.split("=")[1])

    @logger.log
    def authenticate(self, event) -> Tuple[bool, dict]:
        if not self.validate_message(event):
            self._log.error('Invalid request auth.validate_message failed')
            return (False, {
                'statusCode': 501,
                'body': json.dumps("{ 'denied': True }"),
                'headers': {
                    'Content-Type': 'application/json',
                }
            })
        return (True, {})


@xray.capture()
class Alert(object):
    """Sends an alert to the static messenger of choice (defaulting to slack)
    """
    @xray.capture()
    @logger.log
    def __init__(self, log, client, channel):
        self._log = log
        self.client = client
        self.channel = channel

    @xray.capture()
    @logger.log
    def send(self, msg, attachment):
        self._log.debug(f"alert: {msg}: {attachment}")
        status = self.client.api_call(
            "chat.postMessage",
            text=msg,
            channel=self.channel,
            attachments=attachment
        )
        return status

    @xray.capture()
    @logger.log
    def info(self, msg, account_id, policy_name):
        fallback = f"Account: {account_id}, policy: {policy_name}, msg: {msg}"
        attachment = [{
            "fallback": fallback,
            "color": "#36a64f",
            "pretext": f"Account: {account_id}",
            "author_name": "governator-notifier",
            "author_link": repo_url,
            "author_icon": "https://imgur.com/gallery/dTaOdQ5",
            "title": f"policy '{policy_name}' notification info: {msg}",
            "title_link": repo_url,
            "fields": [
                {
                    "title": "Status",
                    "value": ":ok:",
                    "short": False
                }
            ]
        }]
        return self.send(msg, attachment)

    @xray.capture()
    @logger.log
    def error(self, msg, account_id, policy_name):
        fallback = f"Account: {account_id}, policy: {policy_name}, msg: {msg}"
        attachment = [{
            "fallback": fallback,
            "color": "#ff0000",
            "pretext": f"Account: {account_id}",
            "author_name": "governator-notifier",
            "author_link": repo_url,
            "author_icon": "https://imgur.com/gallery/dTaOdQ5",
            "title": f"policy '{policy_name}' notification error: {msg}",
            "title_link": repo_url,
            "fields": [
                {
                    "title": "Status",
                    "value": ":poop:",
                    "short": False
                }
            ]
        }]
        return self.send(msg, attachment)

    @xray.capture()
    @logger.log
    def failure(self, msg, err, account_id, policy_name):
        attachment = [{
            "color": "#ff0000",
            "pretext": f"Error: _{err}_",
            "author_name": f"{policy_name} - {account_id}",
            "author_link": repo_url,
            "author_icon": "https://imgur.com/gallery/dTaOdQ5",
            "title": "View cloudwatch logs here",
            "title_link": repo_url,
            "fields": [
                {
                    "title": "Status",
                    "value": ":poop:",
                    "short": False
                }
            ]
        }]
        return self.send(msg, attachment)

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

    def __getitem__(self, key):
        return self.__getattribute__(key)
