"""The Hangouts Bot."""
import logging

from homeassistant.helpers import dispatcher, intent

from .const import (
    ATTR_MESSAGE, ATTR_TARGET, CONF_CONVERSATIONS, DOMAIN,
    EVENT_HANGOUTS_CONNECTED, EVENT_HANGOUTS_CONVERSATIONS_CHANGED,
    EVENT_HANGOUTS_DISCONNECTED, EVENT_HANGOUTS_MESSAGE_RECEIVED,
    CONF_MATCHERS, CONF_CONVERSATION_ID,
    CONF_CONVERSATION_NAME, EVENT_HANGOUTS_CONVERSATIONS_RESOLVED, INTENT_HELP)

_LOGGER = logging.getLogger(__name__)


class HangoutsBot:
    """The Hangouts Bot."""

    def __init__(self, hass, refresh_token, intents,
                 default_convs, error_suppressed_convs):
        """Set up the client."""
        self.hass = hass
        self._connected = False

        self._refresh_token = refresh_token

        self._intents = intents
        self._conversation_intents = None

        self._client = None
        self._user_list = None
        self._conversation_list = None
        self._default_convs = default_convs
        self._default_conv_ids = None
        self._error_suppressed_convs = error_suppressed_convs
        self._error_suppressed_conv_ids = None

        dispatcher.async_dispatcher_connect(
            self.hass, EVENT_HANGOUTS_MESSAGE_RECEIVED,
            self._async_handle_conversation_message)

    def _resolve_conversation_id(self, obj):
        if CONF_CONVERSATION_ID in obj:
            return obj[CONF_CONVERSATION_ID]
        if CONF_CONVERSATION_NAME in obj:
            conv = self._resolve_conversation_name(obj[CONF_CONVERSATION_NAME])
            if conv is not None:
                return conv.id_
        return None

    def _resolve_conversation_name(self, name):
        for conv in self._conversation_list.get_all():
            if conv.name == name:
                return conv
        return None

    def async_update_conversation_commands(self):
        """Refresh the commands for every conversation."""
        self._conversation_intents = {}

        for intent_type, data in self._intents.items():
            if data.get(CONF_CONVERSATIONS):
                conversations = []
                for conversation in data.get(CONF_CONVERSATIONS):
                    conv_id = self._resolve_conversation_id(conversation)
                    if conv_id is not None:
                        conversations.append(conv_id)
                data['_' + CONF_CONVERSATIONS] = conversations
            elif self._default_conv_ids:
                data['_' + CONF_CONVERSATIONS] = self._default_conv_ids
            else:
                data['_' + CONF_CONVERSATIONS] = \
                    [conv.id_ for conv in self._conversation_list.get_all()]

            for conv_id in data['_' + CONF_CONVERSATIONS]:
                if conv_id not in self._conversation_intents:
                    self._conversation_intents[conv_id] = {}

                self._conversation_intents[conv_id][intent_type] = data

        try:
            self._conversation_list.on_event.remove_observer(
                self._async_handle_conversation_event)
        except ValueError:
            pass
        self._conversation_list.on_event.add_observer(
            self._async_handle_conversation_event)

    def async_resolve_conversations(self, _):
        """Resolve the list of default and error suppressed conversations."""
        self._default_conv_ids = []
        self._error_suppressed_conv_ids = []

        for conversation in self._default_convs:
            conv_id = self._resolve_conversation_id(conversation)
            if conv_id is not None:
                self._default_conv_ids.append(conv_id)

        for conversation in self._error_suppressed_convs:
            conv_id = self._resolve_conversation_id(conversation)
            if conv_id is not None:
                self._error_suppressed_conv_ids.append(conv_id)
        dispatcher.async_dispatcher_send(self.hass,
                                         EVENT_HANGOUTS_CONVERSATIONS_RESOLVED)

    async def _async_handle_conversation_event(self, event):
        from hangups import ChatMessageEvent
        if isinstance(event, ChatMessageEvent):
            dispatcher.async_dispatcher_send(self.hass,
                                             EVENT_HANGOUTS_MESSAGE_RECEIVED,
                                             event.conversation_id,
                                             event.user_id, event)

    async def _async_handle_conversation_message(self,
                                                 conv_id, user_id, event):
        """Handle a message sent to a conversation."""
        user = self._user_list.get_user(user_id)
        if user.is_self:
            return
        message = event.text

        _LOGGER.debug("Handling message '%s' from %s",
                      message, user.full_name)

        intents = self._conversation_intents.get(conv_id)
        if intents is not None:
            is_error = False
            try:
                intent_result = await self._async_process(intents, message,
                                                          conv_id)
            except (intent.UnknownIntent, intent.IntentHandleError) as err:
                is_error = True
                intent_result = intent.IntentResponse()
                intent_result.async_set_speech(str(err))

            if intent_result is None:
                is_error = True
                intent_result = intent.IntentResponse()
                intent_result.async_set_speech(
                    "Sorry, I didn't understand that")

            message = intent_result.as_dict().get('speech', {})\
                .get('plain', {}).get('speech')

            if (message is not None) and not (
                    is_error and conv_id in self._error_suppressed_conv_ids):
                await self._async_send_message(
                    [{'text': message, 'parse_str': True}],
                    [{CONF_CONVERSATION_ID: conv_id}])

    async def _async_process(self, intents, text, conv_id):
        """Detect a matching intent."""
        for intent_type, data in intents.items():
            for matcher in data.get(CONF_MATCHERS, []):
                match = matcher.match(text)

                if not match:
                    continue
                if intent_type == INTENT_HELP:
                    return await self.hass.helpers.intent.async_handle(
                        DOMAIN, intent_type,
                        {'conv_id': {'value': conv_id}}, text)

                return await self.hass.helpers.intent.async_handle(
                    DOMAIN, intent_type,
                    {key: {'value': value}
                     for key, value in match.groupdict().items()}, text)

    async def async_connect(self):
        """Login to the Google Hangouts."""
        from .hangups_utils import HangoutsRefreshToken, HangoutsCredentials

        from hangups import Client
        from hangups import get_auth
        session = await self.hass.async_add_executor_job(
            get_auth, HangoutsCredentials(None, None, None),
            HangoutsRefreshToken(self._refresh_token))

        self._client = Client(session)
        self._client.on_connect.add_observer(self._on_connect)
        self._client.on_disconnect.add_observer(self._on_disconnect)

        self.hass.loop.create_task(self._client.connect())

    def _on_connect(self):
        _LOGGER.debug('Connected!')
        self._connected = True
        dispatcher.async_dispatcher_send(self.hass, EVENT_HANGOUTS_CONNECTED)

    def _on_disconnect(self):
        """Handle disconnecting."""
        _LOGGER.debug('Connection lost!')
        self._connected = False
        dispatcher.async_dispatcher_send(self.hass,
                                         EVENT_HANGOUTS_DISCONNECTED)

    async def async_disconnect(self):
        """Disconnect the client if it is connected."""
        if self._connected:
            await self._client.disconnect()

    async def async_handle_hass_stop(self, _):
        """Run once when Home Assistant stops."""
        await self.async_disconnect()

    async def _async_send_message(self, message, targets):
        conversations = []
        for target in targets:
            conversation = None
            if CONF_CONVERSATION_ID in target:
                conversation = self._conversation_list.get(
                    target[CONF_CONVERSATION_ID])
            elif CONF_CONVERSATION_NAME in target:
                conversation = self._resolve_conversation_name(
                    target[CONF_CONVERSATION_NAME])
            if conversation is not None:
                conversations.append(conversation)

        if not conversations:
            return False

        from hangups import ChatMessageSegment, hangouts_pb2
        messages = []
        for segment in message:
            if messages:
                messages.append(ChatMessageSegment('',
                                                   segment_type=hangouts_pb2.
                                                   SEGMENT_TYPE_LINE_BREAK))
            if 'parse_str' in segment and segment['parse_str']:
                messages.extend(ChatMessageSegment.from_str(segment['text']))
            else:
                if 'parse_str' in segment:
                    del segment['parse_str']
                messages.append(ChatMessageSegment(**segment))

        if not messages:
            return False
        for conv in conversations:
            await conv.send_message(messages)

    async def _async_list_conversations(self):
        import hangups
        self._user_list, self._conversation_list = \
            (await hangups.build_user_conversation_list(self._client))
        conversations = {}
        for i, conv in enumerate(self._conversation_list.get_all()):
            users_in_conversation = []
            for user in conv.users:
                users_in_conversation.append(user.full_name)
            conversations[str(i)] = {CONF_CONVERSATION_ID: str(conv.id_),
                                     CONF_CONVERSATION_NAME: conv.name,
                                     'users': users_in_conversation}

        self.hass.states.async_set("{}.conversations".format(DOMAIN),
                                   len(self._conversation_list.get_all()),
                                   attributes=conversations)
        dispatcher.async_dispatcher_send(self.hass,
                                         EVENT_HANGOUTS_CONVERSATIONS_CHANGED,
                                         conversations)

    async def async_handle_send_message(self, service):
        """Handle the send_message service."""
        await self._async_send_message(service.data[ATTR_MESSAGE],
                                       service.data[ATTR_TARGET])

    async def async_handle_update_users_and_conversations(self, _=None):
        """Handle the update_users_and_conversations service."""
        await self._async_list_conversations()

    def get_intents(self, conv_id):
        """Return the intents for a specific conversation."""
        return self._conversation_intents.get(conv_id)
