# -*- coding: utf-8; -*-
################################################################################
#
#  Rattail -- Retail Software Framework
#  Copyright © 2010-2024 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 rattail.db import model
from rattail.time import localtime

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

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


class MessageView(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'
    listable = False

    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 self.forbidden()
        return super().index()

    def get_instance(self):
        if not self.request.user:
            raise self.forbidden()
        message = super().get_instance()
        if not self.associated_with(message):
            raise self.forbidden()
        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):
        super().configure_grid(g)
        model = self.model

        # sender
        g.set_joiner('sender',
                     lambda q: q.join(model.User,
                                      model.User.uuid == model.Message.sender_uuid)\
                     .outerjoin(model.Person))
        g.set_sorter('sender', model.Person.display_name)
        g.set_filter('sender', model.Person.display_name,
                     default_active=True,
                     default_verb='contains')

        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 str(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 ""

        # 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:
            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])

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

    # TODO!!
    # def make_form(self, instance, **kwargs):
    #     form = super(MessageView, 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().configure_form(f)

        f.submit_label = "Send Message"

        # 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
        f.set_renderer('subject', self.render_subject_bold)
        if self.creating:
            f.set_widget('subject', dfwidget.TextInputWidget(
                placeholder="please enter a subject",
                autocomplete='off',
                attributes={'@keydown.native': 'subjectKeydown'}))
            f.set_required('subject')

        # body
        f.set_widget('body', dfwidget.TextAreaWidget(
            cols=50, rows=15, attributes={'ref': 'messageBody'}))

        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()))
            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', 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().objectify(form, data)

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

            for uuid in data['set_recipients']:
                user = self.Session.get(model.User, 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):

        recips = self.get_available_recipients()
        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()

        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 self.forbidden()

        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)

# TODO: deprecate / remove this
MessagesView = MessageView


class InboxView(MessageView):
    """
    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().query(session)
        return q.filter(model.MessageRecipient.status == self.enum.MESSAGE_STATUS_INBOX)


class ArchiveView(MessageView):
    """
    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().query(session)
        return q.filter(model.MessageRecipient.status == self.enum.MESSAGE_STATUS_ARCHIVE)


class SentView(MessageView):
    """
    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().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.Widget):
    """
    Custom "message recipients" widget, for use with Vue.js themes.
    """
    template = 'message_recipients'

    def deserialize(self, field, pstruct):
        if pstruct is colander.null:
            return colander.null
        if not isinstance(pstruct, str):
            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 defaults(config, **kwargs):
    base = globals()

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

    # inbox
    InboxView = kwargs.get('InboxView', base['InboxView'])
    config.add_route('messages.inbox', '/messages/inbox/')
    config.add_view(InboxView, attr='index',
                    route_name='messages.inbox',
                    permission='messages.list')

    # archive
    ArchiveView = kwargs.get('ArchiveView', base['ArchiveView'])
    config.add_route('messages.archive', '/messages/archive/')
    config.add_view(ArchiveView, attr='index',
                    route_name='messages.archive',
                    permission='messages.list')

    # sent
    SentView = kwargs.get('SentView', base['SentView'])
    config.add_route('messages.sent', '/messages/sent/')
    config.add_view(SentView, attr='index',
                    route_name='messages.sent',
                    permission='messages.list')

    MessageView = kwargs.get('MessageView', base['MessageView'])
    MessageView.defaults(config)


def includeme(config):
    defaults(config)
