# -*- coding: utf-8 -*-

from __future__ import absolute_import

import importlib
import traceback
import logging
import json
import re

from six import iteritems

from mmpy_bot.utils import WorkerPool
from mmpy_bot import settings

logger = logging.getLogger(__name__)

MESSAGE_MATCHER = re.compile(r'^(@.*?\:?)\s(.*)', re.MULTILINE | re.DOTALL)
BOT_ICON = settings.BOT_ICON if hasattr(settings, 'BOT_ICON') else None
BOT_EMOJI = settings.BOT_EMOJI if hasattr(settings, 'BOT_EMOJI') else None


class MessageDispatcher(object):
    def __init__(self, client, plugins):
        self._client = client
        self._pool = WorkerPool(self.dispatch_msg, settings.WORKERS_NUM)
        self._plugins = plugins
        self._channel_info = {}
        self.event = None

    def start(self):
        self._pool.start()

    @staticmethod
    def get_message(msg):
        return msg.get('data', {}).get('post', {}).get('message', '').strip()

    @staticmethod
    def get_sender(msg):
        return msg.get('data', {}).get('sender_name', '').strip()

    def ignore(self, _msg):
        return self._ignore_notifies(_msg) or self._ignore_sender(_msg)

    def _ignore_notifies(self, _msg):
        # ignore message containing specified item, such as "@all"
        msg = self.get_message(_msg)
        return True if any(
            item in msg for item in settings.IGNORE_NOTIFIES) else False

    def _ignore_sender(self, _msg):
        # ignore message from senders specified in settings
        sender_name = self.get_sender(_msg)
        return True if sender_name.lower() in (
            name.lower() for name in settings.IGNORE_USERS) else False

    def is_mentioned(self, msg):
        mentions = msg.get('data', {}).get('mentions', [])
        return self._client.user['id'] in mentions

    def is_personal(self, msg):
        try:
            channel_id = msg['data']['post']['channel_id']
            if channel_id in self._channel_info:
                channel_type = self._channel_info[channel_id]
            else:
                channel = self._client.api.channel(channel_id)
                channel_type = channel['channel']['type']
                self._channel_info[channel_id] = channel_type
            return channel_type == 'D'
        except KeyError:
            logger.info('Once time workpool exception caused by \
                         bot [added to/leave] [team/channel].')
            return False

    def dispatch_msg(self, msg):
        category = msg[0]
        msg = msg[1]
        text = self.get_message(msg)
        responded = False
        msg['message_type'] = '?'
        if self.is_personal(msg):
            msg['message_type'] = 'D'

        for func, args in self._plugins.get_plugins(category, text):
            if func:
                responded = True
                try:
                    func(Message(self._client, msg, self._pool), *args)
                except Exception as err:
                    logger.exception(err)
                    reply = '[%s] I have problem when handling "%s"\n' % (
                        func.__name__, text)
                    reply += '```\n%s\n```' % traceback.format_exc()
                    self._client.channel_msg(
                        msg['data']['post']['channel_id'], reply)

        if not responded and category == 'respond_to':
            if settings.DEFAULT_REPLY_MODULE is not None:
                mod = importlib.import_module(settings.DEFAULT_REPLY_MODULE)
                if hasattr(mod, 'default_reply'):
                    return getattr(mod, 'default_reply')(self, msg)
            self._default_reply(msg)

    def _on_new_message(self, msg):
        if self.ignore(msg) is True:
            return

        msg = self.filter_text(msg)
        if self.is_mentioned(msg) or self.is_personal(msg):
            self._pool.add_task(('respond_to', msg))
        else:
            self._pool.add_task(('listen_to', msg))

    def filter_text(self, msg):
        text = self.get_message(msg)
        if self.is_mentioned(msg):
            m = MESSAGE_MATCHER.match(text)
            if m:
                msg['data']['post']['message'] = m.group(2).strip()
        return msg

    def load_json(self):
        for item in ['post', 'mentions']:
            if self.event.get('data', {}).get(item):
                self.event['data'][item] = json.loads(
                    self.event['data'][item])

    def loop(self):
        for self.event in \
            self._client.messages(True, ['posted', 'added_to_team',
                                         'leave_team', 'user_added',
                                         'user_removed']):
            if self.event:
                self.load_json()
                self._on_new_message(self.event)

    def _default_reply(self, msg):
        if settings.DEFAULT_REPLY:
            return self._client.channel_msg(
                msg['data']['post']['channel_id'], settings.DEFAULT_REPLY)

        default_reply = [
            u'Bad command "%s", Here is what I currently know '
            u'how to do:\n' % self.get_message(msg),
        ]

        # create dictionary organizing commands by plugin
        modules = {}
        for p, v in iteritems(self._plugins.commands['respond_to']):
            key = v.__module__.title()
            if key not in modules:
                modules[key] = []
            modules[key].append((p.regex.pattern, v.__doc__))

        if settings.PLUGINS_ONLY_DOC_STRING:
            docs_fmt = u'\t{1}'
        else:
            docs_fmt = u'\t`{0}` - {1}'

        for module, commands in modules.items():
            default_reply += [u'Plugin: **{}**'.format(module)]
            commands.sort(key=lambda x: x[0])
            for pattern, description in commands:
                default_reply += [docs_fmt.format(pattern, description)]

        self._client.channel_msg(
            msg['data']['post']['channel_id'], '\n'.join(default_reply))


class Message(object):
    users = {}
    channels = {}

    def __init__(self, client, body, pool):
        from mmpy_bot.bot import PluginsManager

        self._plugins = PluginsManager()
        self._client = client
        self._body = body
        self._pool = pool

    def get_user_info(self, key, user_id=None):
        user_id = user_id or self._body['data']['post']['user_id']
        user_info = self._client.api.get_user_info(user_id)
        return user_info[key]

    def get_username(self, user_id=None):
        if user_id is None:
            return self._get_sender_name()
        return self.get_user_info('username', user_id)

    def get_user_mail(self, user_id=None):
        return self.get_user_info('email', user_id)

    def get_user_id(self, user_id=None):
        return self.get_user_info('id', user_id)

    def get_channel_name(self, channel_id=None):
        channel_id = channel_id or self.channel
        if channel_id in self.channels:
            channel_name = self.channels[channel_id]
        else:
            channel = self._client.api.channel(channel_id)
            channel_name = channel['channel']['name']
            self.channels[channel_id] = channel_name
        return channel_name

    def get_team_id(self):
        return self._body['data'].get('team_id', '').strip()

    def get_message(self):
        return self._body['data']['post']['message'].strip()

    def is_direct_message(self):
        return self._body['message_type'] == 'D'

    def get_busy_workers(self):
        return self._pool.get_busy_workers()

    def get_mentions(self):
        return self._body['data'].get('mentions')

    def get_file_link(self, file_id):
        return self._client.api.get_file_link(file_id)

    def upload_file(self, file):
        return self._client.api.upload_file(file, self.channel)

    def _gen_at_message(self, text):
        return '@{}: {}'.format(self.get_username(), text)

    def _gen_reply(self, text):
        if self._body['message_type'] == '?':
            return self._gen_at_message(text)
        return text

    def _get_sender_name(self):
        return self._body['data'].get('sender_name', '').strip()

    @staticmethod
    def _get_webhook_url_by_id(hook_id):
        base = '/'.join(settings.BOT_URL.split('/')[:3])
        return '%s/hooks/%s' % (base, hook_id)

    def reply_webapi(self, text, *args, **kwargs):
        self.send_webapi(self._gen_reply(text), *args, **kwargs)

    def send_webapi(self, text, attachments=None, channel_id=None, **kwargs):
        webhook_id = kwargs.get('webhook_id', settings.WEBHOOK_ID)
        if not webhook_id:
            logger.warning(
                'send_webapi with webhook_id={}. message "{}" is not sent.'
                .format(webhook_id, text)
            )
            return
        url = self._get_webhook_url_by_id(webhook_id)
        kwargs['username'] = kwargs.get(
            'username', self.get_username(self._client.user['id']))
        kwargs['icon_url'] = kwargs.get('icon_url', BOT_ICON)
        kwargs['icon_emoji'] = kwargs.get('icon_emoji', BOT_EMOJI)
        self._client.api.in_webhook(
            url, self.get_channel_name(channel_id), text,
            attachments=attachments, ssl_verify=self._client.api.ssl_verify,
            **kwargs)

    def reply(self, text, files=None, props={}):
        self.send(self._gen_reply(text), files=files, props=props)

    def send(self, text, channel_id=None, files=None, props={}):
        return self._client.channel_msg(
            channel_id or self.channel, text, files=files, props=props)

    def update(self, text, message_id, channel_id=None):
        return self._client.update_msg(
            message_id, channel_id or self.channel,
            text
        )

    def react(self, emoji_name):
        self._client.react_msg(
            self._body['data']['post']['id'], emoji_name)

    def comment(self, message):
        self.react(message)

    def docs_reply(self, docs_format='    • `{0}` {1}'):
        reply = [docs_format.format(v.__name__, v.__doc__ or "")
                 for p, v in iteritems(self._plugins.commands['respond_to'])]
        return '\n'.join(reply)

    @property
    def channel(self):
        return self._body['data']['post']['channel_id']

    @property
    def body(self):
        return self._body
