"""
The Slack Bot and its loops.
"""

import tornado.ioloop
import asyncio
import logging
#import motorengine as me
from   tornado.platform.asyncio import AsyncIOMainLoop


from  .slack     import SlackRTMConnection, is_direct_channel
from  .singleton import Borg
from  .implant   import yield_implants_from, is_implant, get_implant
from  .guardian  import GUARD

log = logging.getLogger(__name__)

BOT   = []


class SlackBot(Borg):

    def __init__(self, slack_token='', name='', mongo_dbname='helbot'):
        Borg.__init__(self)

        self.name           = name
        self.event_loop     = None
        self.slack_token    = slack_token
        self.rtm_connection = None
        self._mongo_dbname  = mongo_dbname
        self.implants       = []
        self._implant_names = set()

    def connect(self, event_loop, slack_token=''):
        if not slack_token and not self.slack_token:
            raise AttributeError('Slack TOKEN to connect to API is missing.')
        elif slack_token:
            self.slack_token = slack_token

        self.event_loop     = event_loop
        self.rtm_connection = SlackRTMConnection(self, self.slack_token)

    def add_implants_from(self, source, name, config={}):
        """ Add all implants found in source.
        Source can be a .py file or a Python module.

        :param source: str or Python module
            Path to a Python file or a module
        """
        implants = list(yield_implants_from(source))
        if not implants:
            raise AttributeError('Could not find implants in {}.'.format(source))

        if len(implants) == 1:
            if not name:
                name = implants[0].__name__
            self.add_implant(implants[0], name, config)
        else:
            for implant in implants:
                self.add_implant(implant, implant.__name__, config)

    def add_implant_from(self, source, name, config={}):
        """ Add the implant with name found in source.
        Source can be a .py file or a Python module.

        :param source: str or Python module
            Path to a Python file or a module
        """
        implant = get_implant(source, name)
        if implant is None:
            raise AttributeError('Could not find implant {} in {}.'.format(name, source))

        self.add_implant(implant, name, config)

    def add_implant(self, implant, name, config={}):
        if not is_implant(implant):
            raise ValueError('Implant {} is not a BotImplant.'.format(implant.__name__))

        if name in self._implant_names:
            log.debug('Implant {} has been already added before.'.format(name))
            return

        log.debug('Adding implant {} from {}.'.format(name, implant.__name__))
        try:
            im_obj = implant(self, config)
            im_obj.name = name
            self.implants.append(im_obj)
            self._implant_names.add(implant.__name__)
        except:
            log.exception('Error adding implant {}.'.format(implant.__name__))
            raise

    def get_implants_regexes(self):
        regexes_in_use = {}
        for im in self.implants:
            if hasattr(im, 'get_regexes'):
                regexes_in_use[type(im).__name__] = im.get_regexes()
        return regexes_in_use

    def get_implant(self, im_name):
        for im in self.implants:
            if type(im).__name__ == im_name:
                return im

            if hasattr(im, 'name'):
                if im.name == im_name:
                    return im
        return None

    #def connect_to_mongo(self):
    #    AsyncIOMainLoop().install()
    #    io_loop = tornado.ioloop.IOLoop.instance()
    #    me.connection.connect(self._mongo_dbname, io_loop=io_loop)

    @asyncio.coroutine
    def run(self):
        #self.connect_to_mongo()
        yield from self.rtm_connection.connect()
        for im in self.implants:
            yield from im.start()
        yield from self.rtm_connection.poll()

    @asyncio.coroutine
    def handle_slack_event(self, event):
        for im in self.implants:
            yield from im.handle_slack_event(event)

    @asyncio.coroutine
    def stop(self):
        for im in self.implants:
            im.stop()
        yield from self.rtm_connection.close()

    @asyncio.coroutine
    def send_message(self, text, channel, user_id=None, add_mention=True, mkdown=False, **kwargs):
        if text is None:
            return False

        if is_direct_channel(channel):
            user = self.rtm_connection.find_user(user_id)
            yield from user.send_message(text, mkdown=mkdown, **kwargs)
        else:
            reply         = {'type': 'message', 'channel': channel, 'mkdown': mkdown}
            reply['text'] = '<@{}> {}'.format(user_id, text) if add_mention else text
            reply.update(**kwargs)
            yield from self.rtm_connection.send_event(reply)

        return True


def setup_bot_and_guardian(bot_name, slack_token, implant_configs):
    """ Set up the SlackBot and the Guardian

    :param bot_name: str
    :param slack_token: str
    :param implant_configs: dict
    """
    log.info('Initializing the bot loop.')
    init_bot_loop(bot_name, slack_token)

    log.info('Setting up the SlackBot implants.')
    setup_bot_implants(implant_configs)

    log.info('Setting up the Guardian permissions.')
    setup_guardian_permissions(implant_configs)


def init_bot_loop(bot_name, slack_token):
    """ Initialize the BOT loop

    :param slack_token: str
    """
    global BOT

    # setup bot
    BOT = SlackBot(name=bot_name)

    # get event loop
    event_loop = asyncio.get_event_loop()
    BOT.connect(event_loop, slack_token)


def setup_bot_implants(implant_configs):
    """ Setup the SlackBot.

    :param bot_implant_configs: dict
    """
    # load implants from config
    for name in implant_configs:
        im_file   = implant_configs[name].get('file',   {})
        im_config = implant_configs[name].get('config', {})
        BOT.add_implant_from(im_file, name, im_config)


def setup_guardian_permissions(implant_configs):
    """ Setup the GUARD and its permissions

    :param bot_implant_configs:
    """
    for name in implant_configs:
        im_users    = implant_configs[name].get('allowed_users',    [])
        im_channels = implant_configs[name].get('allowed_channels', [])
        GUARD.add_implant_permissions(name, im_users, im_channels)


def bot_loop():
    """ Start the SlackBot loop. """
    # run loop
    try:
        BOT.event_loop.run_until_complete(BOT.run())
    except KeyboardInterrupt:
        BOT.event_loop.run_until_complete(BOT.stop())
    except:
        log.exception('Error running bot.')
        raise
