# -*- coding: utf-8; -*-
################################################################################
#
#  Rattail -- Retail Software Framework
#  Copyright © 2010-2019 Lance Edgar
#
#  This file is part of Rattail.
#
#  Rattail is free software: you can redistribute it and/or modify it under the
#  terms of the GNU General Public License as published by the Free Software
#  Foundation, either version 3 of the License, or (at your option) any later
#  version.
#
#  Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
#  WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
#  FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
#  details.
#
#  You should have received a copy of the GNU General Public License along with
#  Rattail.  If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Message Views
"""

from __future__ import unicode_literals, absolute_import

import six

from rattail.db import model
from rattail.time import localtime

import colander
from deform import widget as dfwidget
from pyramid import httpexceptions
from webhelpers2.html import tags, HTML

# from tailbone import forms
from tailbone.db import Session
from tailbone.views import MasterView
from tailbone.util import raw_datetime


class MessagesView(MasterView):
    """
    Base class for message views.
    """
    model_class = model.Message
    editable = False
    deletable = False
    checkboxes = True
    replying = False
    reply_header_sent_format = '%a %d %b %Y at %I:%M %p'

    grid_columns = [
        'subject',
        'sender',
        'recipients',
        'sent',
    ]

    form_fields = [
        'sender',
        'recipients',
        'sent',
        'subject',
        'body',
    ]

    def get_index_title(self):
        if self.listing:
            return self.index_title
        if self.viewing:
            message = self.get_instance()
            recipient = self.get_recipient(message)
            if recipient and recipient.status == self.enum.MESSAGE_STATUS_ARCHIVE:
                return "Message Archive"
            elif not recipient:
                return "Sent Messages"
        return "Message Inbox"

    def get_index_url(self, **kwargs):
        # not really used, but necessary to make certain other code happy
        return self.request.route_url('messages.inbox')

    def index(self):
        if not self.request.user:
            raise httpexceptions.HTTPForbidden
        return super(MessagesView, self).index()

    def get_instance(self):
        if not self.request.user:
            raise httpexceptions.HTTPForbidden
        message = super(MessagesView, self).get_instance()
        if not self.associated_with(message):
            raise httpexceptions.HTTPForbidden
        return message

    def associated_with(self, message):
        if message.sender is self.request.user:
            return True
        for recip in message.recipients:
            if recip.recipient is self.request.user:
                return True
        return False

    def query(self, session):
        return session.query(model.Message)\
                      .outerjoin(model.MessageRecipient)\
                      .filter(model.MessageRecipient.recipient == self.request.user)

    def configure_grid(self, g):
        
        g.joiners['sender'] = lambda q: q.join(model.User, model.User.uuid == model.Message.sender_uuid).outerjoin(model.Person)
        g.filters['sender'] = g.make_filter('sender', model.Person.display_name,
                                            default_active=True, default_verb='contains')
        g.sorters['sender'] = g.make_sorter(model.Person.display_name)

        g.filters['subject'].default_active = True
        g.filters['subject'].default_verb = 'contains'

        g.set_sort_defaults('sent', 'desc')

        g.set_renderer('sent', self.render_sent)
        g.set_renderer('sender', self.render_sender)
        g.set_renderer('recipients', self.render_recipients)

        g.set_link('subject')

        g.set_label('sender', "From")
        g.set_label('recipients', "To")

    def render_sent(self, message, column_name):
        return raw_datetime(self.rattail_config, message.sent)

    def render_sender(self, message, field):
        sender = message.sender
        if sender is self.request.user:
            return 'you'
        return six.text_type(sender)

    def render_subject_bold(self, message, field):
        if not message.subject:
            return ""
        return HTML.tag('span', c=message.subject, style='font-weight: bold;')

    def render_recipients(self, message, column_name):
        recipients = message.recipients
        if recipients:
            recips = [r for r in recipients if r.recipient is not self.request.user]
            recips = sorted([r.recipient.display_name for r in recips])
            if len(recips) < len(recipients) and (
                    message.sender is not self.request.user or not recips):
                recips.insert(0, "you")
            if len(recips) < 5:
                return ", ".join(recips)
            return "{}, ...".format(', '.join(recips[:4]))
        return ""

    def render_recipients_full(self, message, field):
        recipients = message.recipients
        if not recipients:
            return ""

        use_buefy = self.get_use_buefy()

        # remove current user from displayed list, even if they're a recipient
        recips = [r for r in recipients
                  if r.recipient is not self.request.user]

        # sort recipients by display name
        recips = sorted([r.recipient.display_name for r in recips])

        # if we *did* remove current user from list, insert them at front of list
        if len(recips) < len(recipients) and (
                message.sender is not self.request.user or not recips):
            recips.insert(0, 'you')

        # we only want to show the first 5 recipients by default, with
        # client-side JS allowing the user to view all if they want
        max_display = 5
        if len(recips) > max_display:
            if use_buefy:
                basic = HTML.tag('span', c="{}, ".format(', '.join(recips[:max_display-1])))
                more = tags.link_to("({} more)".format(len(recips[max_display-1:])), '#', **{
                    'v-show': '!showingAllRecipients',
                    '@click.prevent': 'showMoreRecipients()',
                })
                everyone = HTML.tag('span', c=', '.join(recips[max_display-1:]), **{
                    'v-show': 'showingAllRecipients',
                    '@click': 'hideMoreRecipients()',
                    'class_': 'everyone',
                })
                return HTML.tag('div', c=[basic, more, everyone])
            else:
                basic = HTML.literal("{}, ".format(', '.join(recips[:max_display-1])))
                more = tags.link_to("({} more)".format(len(recips[max_display-1:])), '#', class_='more')
                everyone = HTML.tag('span', class_='everyone', c=', '.join(recips[max_display-1:]))
                return basic + more + everyone

        # show the full list if there are few enough recipients for that
        return ', '.join(recips)

    # TODO!!
    # def make_form(self, instance, **kwargs):
    #     form = super(MessagesView, self).make_form(instance, **kwargs)
    #     if self.creating:
    #         form.id = 'new-message'
    #         form.cancel_url = self.request.get_referrer(default=self.request.route_url('messages.inbox'))
    #         form.create_label = "Send Message"
    #     return form

    def configure_form(self, f):
        super(MessagesView, self).configure_form(f)
        use_buefy = self.get_use_buefy()

        f.submit_label = "Send Message"

        if not use_buefy:
            # we have custom logic to disable submit button
            f.auto_disable = False
            f.auto_disable_save = False

        # TODO: A fair amount of this still seems hacky...

        f.set_renderer('sender', self.render_sender)
        f.set_label('sender', "From")

        f.set_type('sent', 'datetime')

        # recipients
        f.set_renderer('recipients', self.render_recipients_full)
        f.set_label('recipients', "To")

        # subject
        if use_buefy:
            f.set_renderer('subject', self.render_subject_bold)
            if self.creating:
                f.set_widget('subject', dfwidget.TextInputWidget(
                    placeholder="please enter a subject",
                    autocomplete='off'))
                f.set_required('subject')

        # body
        f.set_widget('body', dfwidget.TextAreaWidget(cols=50, rows=15))

        if self.creating:
            f.remove('sender', 'sent')

            # recipients
            f.insert_after('recipients', 'set_recipients')
            f.remove('recipients')
            f.set_node('set_recipients', colander.SchemaNode(colander.Set()))
            if use_buefy:
                f.set_widget('set_recipients', RecipientsWidgetBuefy())
            else:
                f.set_widget('set_recipients', RecipientsWidget())
            f.set_label('set_recipients', "To")

            if self.replying:
                old_message = self.get_instance()
                f.set_default('subject', "Re: {}".format(old_message.subject))
                f.set_default('body', self.get_reply_body(old_message))

                # Determine an initial set of recipients, based on reply method.

                # If replying to all, massage the list a little so that the
                # current user is not listed, and the sender is listed first.
                if self.replying == 'all':
                    value = [(r.recipient.uuid, r.recipient.person.display_name)
                             for r in old_message.recipients
                             if self.filter_reply_recipient(r.recipient)]
                    value = dict(value)
                    value.pop(self.request.user.uuid, None)
                    value = sorted(value.items(), key=lambda r: r[1])
                    value = [r[0] for r in value]
                    if old_message.sender is not self.request.user and old_message.sender.active:
                        value.insert(0, old_message.sender_uuid)
                    f.set_default('set_recipients', ','.join(value))

                # Just a normal reply, to sender only.
                elif self.filter_reply_recipient(old_message.sender):
                    f.set_default('set_recipients', old_message.sender.uuid)

                # TODO?
                # # Set focus to message body instead of recipients, when replying.
                # fs.focus = fs.body

        elif self.viewing:
            f.remove('body')

    def objectify(self, form, data=None):
        if data is None:
            data = form.validated
        message = super(MessagesView, self).objectify(form, data)

        if self.creating:
            if self.request.user:
                message.sender = self.request.user

            for uuid in data['set_recipients']:
                user = self.Session.query(model.User).get(uuid)
                if user:
                    message.add_recipient(user, status=self.enum.MESSAGE_STATUS_INBOX)

        return message

    def flash_after_create(self, obj):
        self.request.session.flash("Message has been sent: {}".format(
            self.get_instance_title(obj)))

    def filter_reply_recipient(self, user):
        return user.active

    def get_reply_header(self, message):
        sent = localtime(self.rattail_config, message.sent, from_utc=True)
        sent = sent.strftime(self.reply_header_sent_format)
        return "On {}, {} wrote:".format(sent, message.sender.person.display_name)

    def get_reply_body(self, message):
        """
        Given an original message, this method should return the default body
        value for a "reply" message, i.e. with ">" prefixes etc.
        """
        header = self.get_reply_header(message)
        lines = message.body.split('\n')
        if lines and lines[0]:
            lines.insert(0, '')
        lines = ['', '', '', header] + ["> {}".format(line) for line in lines]
        return '\n'.join(lines)

    def get_recipient(self, message):
        """
        Fetch the recipient from the given message, which corresponds to the
        current (request) user.
        """
        for recipient in message.recipients:
            if recipient.recipient is self.request.user:
                return recipient

    def template_kwargs_create(self, **kwargs):
        use_buefy = self.get_use_buefy()

        recips = self.get_available_recipients()
        if use_buefy:
            kwargs['recipient_display_map'] = recips
        recips = list(recips.items())
        recips.sort(key=self.recipient_sortkey)
        kwargs['available_recipients'] = recips

        if self.replying:
            kwargs['original_message'] = self.get_instance()

        if use_buefy:
            kwargs['index_url'] = None
            kwargs['index_title'] = "New Message"
        return kwargs

    def recipient_sortkey(self, recip):
        uuid, entry = recip
        if isinstance(entry, dict):
            return entry['name']
        return entry

    def get_available_recipients(self):
        """
        Return the full mapping of recipients which may be included in a
        message sent by the current user.
        """
        recips = {}
        users = Session.query(model.User)\
                       .join(model.Person)\
                       .filter(model.User.active == True)
        for user in users:
            recips[user.uuid] = user.person.display_name
        return recips

    def template_kwargs_view(self, **kwargs):
        message = kwargs['instance']
        recipient = self.get_recipient(message)

        kwargs['message'] = message
        kwargs['recipient'] = recipient

        if recipient and recipient.status == self.enum.MESSAGE_STATUS_ARCHIVE:
            kwargs['index_url'] = self.request.route_url('messages.archive')

        return kwargs

    def reply(self):
        """
        Reply to a message, i.e. create a new one with first sender as recipient.
        """
        self.replying = True
        return self.create()

    def reply_all(self):
        """
        Reply-all to a message, i.e. create a new one with all original
        recipients listed again in the new message.
        """
        self.replying = 'all'
        return self.create()

    def move(self):
        """
        Move a message, either to the archive or back to the inbox.
        """
        message = self.get_instance()
        recipient = self.get_recipient(message)
        if not recipient:
            raise httpexceptions.HTTPForbidden

        dest = self.request.GET.get('dest')
        if dest not in ('inbox', 'archive'):
            self.request.session.flash("Sorry, I couldn't make sense out of that request.")
            return self.redirect(self.request.get_referrer(
                default=self.request.route_url('messages_inbox')))

        new_status = self.enum.MESSAGE_STATUS_INBOX if dest == 'inbox' else self.enum.MESSAGE_STATUS_ARCHIVE
        if recipient.status != new_status:
            recipient.status = new_status
        return self.redirect(self.request.route_url('messages.{}'.format(
            'archive' if dest == 'inbox' else 'inbox')))

    def move_bulk(self):
        """
        Move messages in bulk, to the archive or back to the inbox.
        """
        dest = self.request.POST.get('destination', 'archive')
        if self.request.method == 'POST':
            uuids = self.request.POST.get('uuids', '').split(',')
            if uuids:
                new_status = self.enum.MESSAGE_STATUS_INBOX if dest == 'inbox' else self.enum.MESSAGE_STATUS_ARCHIVE
                for uuid in uuids:
                    recip = self.Session.query(model.MessageRecipient)\
                                        .filter(model.MessageRecipient.message_uuid == uuid)\
                                        .filter(model.MessageRecipient.recipient_uuid == self.request.user.uuid)\
                                        .first()
                    if recip and recip.status != new_status:
                        recip.status = new_status
        route = 'messages.{}'.format('archive' if dest == 'inbox' else 'inbox')
        return self.redirect(self.request.route_url(route))

    @classmethod
    def defaults(cls, config):
        """
        Extra default config for message views.
        """

        # reply
        config.add_route('messages.reply', '/messages/{uuid}/reply')
        config.add_view(cls, attr='reply', route_name='messages.reply',
                        permission='messages.create')

        # reply-all
        config.add_route('messages.reply_all', '/messages/{uuid}/reply-all')
        config.add_view(cls, attr='reply_all', route_name='messages.reply_all',
                        permission='messages.create')

        # move (single)
        config.add_route('messages.move', '/messages/{uuid}/move')
        config.add_view(cls, attr='move', route_name='messages.move')

        # move bulk
        config.add_route('messages.move_bulk', '/messages/move-bulk')
        config.add_view(cls, attr='move_bulk', route_name='messages.move_bulk')

        cls._defaults(config)


class InboxView(MessagesView):
    """
    Inbox message view.
    """
    url_prefix = '/messages/inbox'
    grid_key = 'messages.inbox'
    index_title = "Message Inbox"

    def get_index_url(self, **kwargs):
        return self.request.route_url('messages.inbox')

    def query(self, session):
        q = super(InboxView, self).query(session)
        return q.filter(model.MessageRecipient.status == self.enum.MESSAGE_STATUS_INBOX)


class ArchiveView(MessagesView):
    """
    Archived message view.
    """
    url_prefix = '/messages/archive'
    grid_key = 'messages.archive'
    index_title = "Message Archive"

    def get_index_url(self, **kwargs):
        return self.request.route_url('messages.archive')

    def query(self, session):
        q = super(ArchiveView, self).query(session)
        return q.filter(model.MessageRecipient.status == self.enum.MESSAGE_STATUS_ARCHIVE)


class SentView(MessagesView):
    """
    Sent messages view.
    """
    url_prefix = '/messages/sent'
    grid_key = 'messages.sent'
    checkboxes = False
    index_title = "Sent Messages"

    def get_index_url(self, **kwargs):
        return self.request.route_url('messages.sent')

    def query(self, session):
        return session.query(model.Message)\
                      .filter(model.Message.sender == self.request.user)

    def configure_grid(self, g):
        super(SentView, self).configure_grid(g)
        g.filters['sender'].default_active = False
        g.joiners['recipients'] = lambda q: q.join(model.MessageRecipient)\
                                             .join(model.User, model.User.uuid == model.MessageRecipient.recipient_uuid)\
                                             .join(model.Person)
        g.filters['recipients'] = g.make_filter('recipients', model.Person.display_name,
                                                default_active=True, default_verb='contains')


class RecipientsWidget(dfwidget.TextInputWidget):

    def deserialize(self, field, pstruct):
        if pstruct is colander.null:
            return []
        elif not isinstance(pstruct, six.string_types):
            raise colander.Invalid(field.schema, "Pstruct is not a string")
        if self.strip:
            pstruct = pstruct.strip()
        if not pstruct:
            return []
        return pstruct.split(',')


class RecipientsWidgetBuefy(dfwidget.Widget):
    """
    Custom "message recipients" widget, for use with Buefy / Vue.js themes.
    """
    template = 'message_recipients_buefy'

    def deserialize(self, field, pstruct):
        if pstruct is colander.null:
            return colander.null
        if not isinstance(pstruct, six.string_types):
            raise colander.Invalid(field.schema, "Pstruct is not a string")
        if not pstruct:
            return colander.null
        pstruct = pstruct.split(',')
        return pstruct

    def serialize(self, field, cstruct, **kw):
        if cstruct in (colander.null, None):
            cstruct = ""
        template = self.template
        values = self.get_template_values(field, cstruct, kw)
        return field.renderer(template, **values)


def includeme(config):

    config.add_tailbone_permission('messages', 'messages.list', "List/Search Messages")

    # inbox
    config.add_route('messages.inbox', '/messages/inbox/')
    config.add_view(InboxView, attr='index', route_name='messages.inbox',
                    permission='messages.list')

    # archive
    config.add_route('messages.archive', '/messages/archive/')
    config.add_view(ArchiveView, attr='index', route_name='messages.archive',
                    permission='messages.list')

    # sent
    config.add_route('messages.sent', '/messages/sent/')
    config.add_view(SentView, attr='index', route_name='messages.sent',
                    permission='messages.list')

    MessagesView.defaults(config)
