import asyncio
import datetime
import logging

from slack_sdk.web.async_client import AsyncWebClient

from slackminion.dispatcher import MessageDispatcher
from slackminion.exceptions import NotSetupError
from slackminion.plugin import PluginManager
from slackminion.plugins.core import version as my_version
from slackminion.slack import SlackConversation, SlackEvent, SlackUser
from slackminion.slack.rtm_client import MyRTMClient
from slackminion.utils.async_task import AsyncTaskManager
from slackminion.webserver import Webserver

ignore_subtypes = [
    "bot_message",
    "message_changed",
    "message_replied",
    "message_deleted",
]


class Bot(object):
    rtm_client = None
    api_client = None
    webserver = None
    _info = {}
    _channels = {}
    runnable = True
    shutting_down = False
    always_send_dm = []
    is_setup = False
    bot_start_time = None
    timers = []

    def __init__(self, config, test_mode=False, dev_mode=False):
        self.config = config
        self.dispatcher = MessageDispatcher()
        self.log = logging.getLogger(type(self).__name__)
        self.plugin_manager = PluginManager(self, test_mode)
        self.plugins = self.plugin_manager  # backward compatibility
        self.test_mode = test_mode
        self.dev_mode = dev_mode
        self.event_loop = asyncio.get_event_loop()

        if self.test_mode:
            self.metrics = {"startup_time": 0}

        self.version = my_version
        try:
            from slackminion.plugins.core import commit

            self.commit = commit
        except ImportError:
            self.commit = "HEAD"

    @property
    def sc(self):
        return self.api_client

    @property
    def my_username(self):
        return self._info.get("name")

    @property
    def my_userid(self):
        return self._info.get("user_id")

    @property
    def channels(self):
        if self.is_setup:
            if self._channels:
                return self._channels
            else:
                self.log.warning(
                    "Bot.channels was called but self._bot_channels was empty!"
                )
                return {}
        self.log.warning("Bot.channels was called before bot was setup.")
        return {}

    async def get_my_conversations(self, *args, **kwargs):
        return await self.api_client.users_conversations(
            exclude_archived="true",
            limit=200,
            types="public_channel,private_channel",
            *args,
            **kwargs,
        )

    async def update_channels(self):
        self.log.debug("Starting update_channels")
        try:
            resp = await self.get_my_conversations()
            results = resp.get("channels")

            while resp.get("response_metadata").get("next_cursor"):
                cursor = resp.get("response_metadata").get("next_cursor")
                resp = await self.get_my_conversations(cursor=cursor)
                results.extend(resp.get("channels"))
                await asyncio.sleep(1)

            for channel in results:
                self._channels.update(
                    {
                        channel.get("id"): SlackConversation(
                            conversation=channel, api_client=self.api_client
                        )
                    }
                )
        except Exception:  # noqa
            self.log.exception("update_channels failed due to exception")
        self.log.debug(f"Loaded {len(self.channels)} channels.")

    def start(self):
        """Initializes the bot, plugins, and everything."""
        self.log.info(f"Starting SlackMinion version {self.version}")
        self.task_manager = AsyncTaskManager(self)
        self.bot_start_time = datetime.datetime.now()

        self.log.debug("Slack clients initialized.")
        self.webserver = Webserver(
            self.config["webserver"]["host"], self.config["webserver"]["port"]
        )

        self.plugin_manager.load()
        self.plugin_manager.load_state()

        self.rtm_client = MyRTMClient(
            token=self.config.get("slack_token"), run_async=True
        )
        self.api_client = AsyncWebClient(token=self.config.get("slack_token"))

        self.always_send_dm = ["_unauthorized_"]
        if "always_send_dm" in self.config:
            self.always_send_dm.extend(["!" + x for x in self.config["always_send_dm"]])

        self._add_event_handlers()
        self.is_setup = True
        if self.test_mode:
            self.metrics["startup_time"] = (
                datetime.datetime.now() - self.bot_start_time
            ).total_seconds() * 1000.0

    def graceful_shutdown(self):
        if not self.shutting_down:
            self.shutting_down = True
            self.log.debug("Starting graceful shutdown.")
            self.task_manager.runnable = False
            self.runnable = False

    async def run(self):
        """
        Connects to slack and enters the main loop.
        """
        # Fail out if setup wasn't run
        if not self.is_setup:
            raise NotSetupError

        # Start the web server
        self.log.debug("Starting Web Server")
        self.webserver.start()
        first_connect = True

        self._info = await self.api_client.auth_test()

        while self.runnable:
            if first_connect:
                self.log.debug("Starting RTM Client")
                self.task_manager.start_rtm_client(self.rtm_client)
                self.plugin_manager.connect()
                self.task_manager.start_periodic_task(600, self.update_channels)
                first_connect = False
            await self.task_manager.start()
            await asyncio.sleep(1)

    async def stop(self):
        """Does cleanup of bot and plugins."""
        if not self.test_mode:
            self.plugin_manager.save_state()
        self.log.debug("Stopping Task Manager")
        await self.task_manager.shutdown()
        self.log.debug("Stopping RTM client.")

        # cleanup any running timer threads so bot doesn't hang on shutdown
        for t in self.timers:
            t.cancel()
        if self.webserver is not None:
            self.webserver.stop()
        self.plugin_manager.unload_all()

    async def send_message(
        self,
        channel,
        text,
        thread=None,
        reply_broadcast=None,
        attachments=None,
        parse=None,
        link_names=1,
    ):
        """
        Sends a message to the specified channel

        * channel - The channel to send to.  This can be a SlackConversation object, a channel id, or a channel name
        (without the #)
        * text - String to send
        * thread - reply to the thread. See https://api.slack.com/docs/message-threading#threads_party
        * reply_broadcast - Set to true to indicate your reply is germane to all members of a channel
        * parse - Set to "full" for the slack api to linkify names and channels
        """
        if not text:
            self.log.debug("send_message was called without text to send")
            return
        # This doesn't want the # in the channel name
        if isinstance(channel, SlackConversation):
            channel = channel.channel_id
        self.log.debug(f"Trying to send to {channel}: {text[:40]} (truncated)")
        await self.api_client.chat_postMessage(
            as_user=True,
            channel=channel,
            text=text,
            thread_ts=thread,
            reply_broadcast=reply_broadcast,
            attachments=attachments,
            parse=parse,
            link_names=link_names,
        )

    async def send_im(self, user, text, parse=None):
        """
        Sends a message to a user as an IM

        * user - The user to send to.  This can be a SlackUser object, a user id, or the username (without the @)
        * text - String to send
        """
        if isinstance(user, SlackUser):
            channelid = user.user_id
        else:
            channelid = user
        await self.send_message(channelid, text, parse)

    async def at_user(self, user, channel_id, text, **kwargs):
        """
        Appends @user Slack formatting to the beginning of a message.

        * user - The SlackUser to send to.
        * channel_id - The channel ID of the channel to send  to
        * text - String to send
        * kwargs - add'l keyword arguments to pass to send_message()
        """
        message = f"{user.at_user}: {text}"
        await self.send_message(channel_id, message, **kwargs)

    def _load_user_rights(self, user):
        self.log.debug(f"Loading user rights for {user}")
        if user is not None:
            if "bot_admins" in self.config:
                if user.username in self.config["bot_admins"]:
                    user.set_admin(True)

    # Parse incoming event and return a corresponding SlackEvent object
    async def _parse_event(self, payload):
        self.log.debug(payload)
        event_type, data = self._unpack_payload(**payload)
        subtype = data.get("subtype")

        # ignore message subtypes we aren't interested in
        if subtype and subtype in ignore_subtypes:
            self.log.info(f"Ignoring message subtype {subtype} from {data.get('user')}")
            self.log.debug(data.get("text"))
            return

        event = SlackEvent(event_type=event_type, **payload)
        self.log.debug("Received event type: %s", event.event_type)

        if event.user_id and event.user_id != self.my_userid:
            if hasattr(self, "user_manager"):
                event.user = self.user_manager.get(event.user_id)
                if event.user is None:
                    slack_user = SlackUser(
                        user_id=event.user_id, api_client=self.api_client
                    )
                    await slack_user.load()
                    event.user = self.user_manager.set(slack_user)
        if event.channel_id:
            event.channel = await self.get_channel(event.channel_id)

        return event

    def _unpack_payload(self, **payload):
        data = payload["data"]
        event_type = data["type"]
        return event_type, data

    def _add_event_handlers(self):
        MyRTMClient.on(event="channel_joined", callback=self._event_channel_joined)
        MyRTMClient.on(event="message", callback=self._event_message)
        MyRTMClient.on(event="error", callback=self._event_error)
        for plugin in self.plugin_manager.plugins:
            if plugin.notify_event_types:
                t = type(plugin.notify_event_types)
                try:
                    assert t == list
                except AssertionError:
                    self.log.exception(
                        f"notify_event_types for {plugin.__class__.__name__} type should be list, got {t}"
                    )
                for event_type in plugin.notify_event_types:
                    self.log.info(
                        f"Registering handler for {event_type} for plugin {plugin.__class__.__name__}"
                    )
                    try:
                        MyRTMClient.on(event=event_type, callback=self._event_plugin)
                    except Exception as e:
                        self.log.exception(
                            f"Unexpected exception when attempting to register event handler for "
                            f'type {event_type} for plugin {plugin.__class__.__name__}" [{e}] '
                        )

    # generic handler for handling event types registered by plugins via notify_event_types class attribute
    async def _event_plugin(self, **payload):
        event_type, data = self._unpack_payload(**payload)
        await self.plugin_manager.broadcast_event(event_type, data)

    # when the bot is invited to a channel, add the channel to self.channels
    async def _event_channel_joined(self, **payload):
        try:
            event_type, data = self._unpack_payload(**payload)
            self.log.debug(f"Received channel_joined event: {data}")
            channel_info = data.get("channel")
            channel = SlackConversation(
                conversation=channel_info, api_client=self.api_client
            )
            self._channels.update({channel.id: channel})
        except Exception:  # noqa
            self.log.exception("Uncaught exception")

    async def _event_message(self, **payload):
        msg = await self._parse_event(payload)
        if not msg:
            return

        # The user manager should load rights when a user is added
        if not hasattr(self, "user_manager"):
            self._load_user_rights(msg.user)
        try:
            self.log.debug(f"Sending to dispatcher: {msg}")
            cmd, output, cmd_options = await self.dispatcher.push(msg, self.dev_mode)
            self.log.debug(f"Output from dispatcher: {output}")

            if output:
                await self._prepare_and_send_output(cmd, msg, cmd_options, output)
        except Exception:
            self.log.exception("Unhandled exception")

    async def _prepare_and_send_output(self, cmd, msg, cmd_options, output):
        self.log.debug(
            f"Preparing to send  output for  {cmd} with options {cmd_options}"
        )
        if msg.thread_ts:
            thread_ts = msg.thread_ts
        elif cmd_options.get("reply_in_thread"):
            thread_ts = msg.ts
        else:
            thread_ts = None
        parse = cmd_options.get("parse", None)
        if cmd in self.always_send_dm or cmd_options.get("always_send_dm"):
            await self.send_im(msg.user, output, parse=parse)
        else:
            await self.send_message(
                msg.channel,
                output,
                thread=thread_ts,
                reply_broadcast=cmd_options.get("reply_broadcast"),
                parse=parse,
            )

    async def _event_error(self, **payload):
        event_type, data = self._unpack_payload(**payload)
        await self.plugin_manager.broadcast_event(event_type, data)
        self.log.error(f"Received an error response from Slack: {payload}")

    def get_channel_by_name(self, channel_name):
        channels = [x for x in self.channels.values() if channel_name in x.all_names]
        if len(channels) == 0:
            raise RuntimeError(f"Unable to find channel {channel_name}")
        if len(channels) > 1:
            self.log.warning(f"Found more than one channel named {channel_name}")
        return channels[0]

    async def get_channel(self, channel_id):
        if channel_id in self.channels.keys():
            channel = self.channels.get(channel_id)
        else:
            channel = SlackConversation(None, self.api_client)
            await channel.load(channel_id)
            self._channels.update({channel_id: channel})
        if channel:
            return channel
