from AccessControl import Unauthorized
from collections import defaultdict
from logging import getLogger
from plone import api
from plone.memoize.view import memoize
from ploneintranet import api as pi_api
from ploneintranet.notifications.interfaces import IAggregatedNotifications
from ploneintranet.notifications.sql import saconfig
from Products.Five import BrowserView
from zope.interface import implementer

logger = getLogger(__name__)


@implementer(IAggregatedNotifications)
class AggregatedNotifications:
    def __init__(self, recipient_id, notifications):
        self.recipient_id = recipient_id
        self._notifications = notifications

    @property
    def notifications(self):
        notifications = []
        for notification in self._notifications:
            if notification.logged_event.context is None:
                logger.info(
                    (
                        "Skipping notification %r because no context can be resolved "
                        "for its logged event %r created by %r at %s"
                    ),
                    notification.uid,
                    notification.logged_event.uid,
                    notification.logged_event.actor_id,
                    notification.logged_event.time,
                )
                continue
            try:
                notification.assert_context_is_accessible_for_recipient()
            except Unauthorized:
                logger.info(
                    "Skipping notification %r because user is unauthorized",
                    notification.uid,
                )
                continue
            except Exception:
                logger.exception(
                    "Skipping notification %r because of an error", notification.uid
                )
                continue
            notifications.append(notification)
        return notifications

    @property
    def grouped_notifications(self):
        grouped = defaultdict(lambda: defaultdict(lambda: defaultdict(list)))
        for notification in self.notifications:
            grouped[notification.get_app()][notification.category][
                notification.logged_event.context
            ].append(notification)
        return grouped


class AggregatorView(BrowserView):
    @memoize
    def get_notifications_query(self, omit_record_channel=True):
        """Query all the notifications that should be notified

        If the omit_record_channel flag is truish (the default),
        exclude the notifications for the record channel.
        """
        return saconfig.get_notifications_query(omit_record_channel)

    def get_recipient_ids(self, omit_record_channel=True):
        """The ids of the users that are going to get notifications

        If the omit_record_channel flag is truish (the default),
        exclude the recipient ids that have only
        notifications for the record channel.
        """
        base_query = self.get_notifications_query(
            omit_record_channel=omit_record_channel
        )
        results = base_query.with_entities(
            saconfig.Notification.recipient_id
        ).distinct()
        return {result.recipient_id for result in results}

    def get_recipient_notifications(self, recipient_id):
        """Returns all notifications for a given recipient that should be notified.

        This means we exclude the notifications for the record channel
        that are treated separately.
        """
        base_query = self.get_notifications_query(omit_record_channel=True)
        return (
            base_query.filter(
                saconfig.Notification.recipient_id == recipient_id,
            )
            .order_by(saconfig.Notification.scheduled_time)
            .all()
        )

    def get_recipient_notifications_by_channel(self, recipient_id):
        """Returns a mapping of channels to notifications"""
        notifications = self.get_recipient_notifications(recipient_id)
        mapping = defaultdict(list)
        for notification in notifications:
            mapping[notification.channel].append(notification)
        return mapping

    def aggregate(self):
        """Aggregate notifications per recipient and channel,
        excluding the record channel
        """
        notified = []
        recipient_ids = self.get_recipient_ids()
        for recipient_id in recipient_ids:
            if self.request is None:
                request = None
            else:
                # view memoization must be isolated per recipient
                request = self.request.clone()
            notifications_by_channel = self.get_recipient_notifications_by_channel(
                recipient_id
            )
            for channel, notifications in notifications_by_channel.items():
                aggregated = AggregatedNotifications(recipient_id, notifications)
                try:
                    view = api.content.get_view(
                        name=channel,
                        context=aggregated,
                        request=request,
                    )
                    # This sends the actual emails, via the following call flow:
                    # view() = AggregatedBase.__call__()
                    # -> AggregatedEmail.process()
                    # -> BaseEmail.send_email()
                    #
                    # For the desktop channel, the push happens here,
                    # in browser.desktop.AggregatedDesktop.process()
                    view()
                except api.exc.InvalidParameterError:
                    # No view for this channel
                    logger.warning("No view for channel %s", channel)
                    continue

                for notification in notifications:
                    # includes desktop, excludes record channel
                    notification.mark_notified()
                notified.extend(notifications)
        return notified

    def push_counter(self):
        """Send a push notification that updates the notification counter to
        users who have new (i.e. not-notified) notifications in the record
        channel"""
        Notification = saconfig.Notification
        query = self.get_notifications_query(omit_record_channel=False)
        query = query.filter(
            Notification.channel == "record",
            Notification.notified == None,  # noqa: E711
        )
        results = query.with_entities(saconfig.Notification.recipient_id).distinct()
        if not results.count():
            return
        recipient_ids = {result.recipient_id for result in results}
        pi_api.push.notification_counter_changed_for(recipient_ids)
        # Now mark al these notifications as dispatched, so we only emit a
        # counter changed event when there are new unsent notifications
        for notification in query.all():
            notification.mark_notified()

    def __call__(self):
        self.aggregate()
        self.push_counter()
        self.request.response.setHeader("Content-Type", "text/plain")
        return "OK"


class AggregatedBase(BrowserView):
    """Base class for sending aggregated notifications per channel"""

    def process(self):
        raise NotImplementedError

    def mark_notified(self):
        for notification in self.context.notifications:
            notification.mark_notified()

    def __call__(self):
        if not self.context.notifications:
            return
        self.process()
        self.mark_notified()
