from datetime import datetime
from datetime import timedelta
from datetime import timezone
from plone import api
from plone.app.layout.viewlets import ViewletBase
from plone.memoize.view import memoize
from ploneintranet import api as pi_api
from ploneintranet.core import _
from ploneintranet.layout.interfaces import IAppView
from ploneintranet.notifications.sql import saconfig
from Products.Five.browser import BrowserView
from urllib.parse import urlencode
from zope.interface import implementer

import json


@implementer(IAppView)
class NotificationsView(BrowserView):
    app_name = "notifications"
    batch_size = 20
    title = _("label_title_app_notifications", default="Notifications")

    @property
    @memoize
    def load_more_url(self) -> str:
        notifications = self.notifications
        if not notifications:
            return ""

        last_notification = self.notifications[-1]
        params = {
            "timestamp": last_notification.scheduled_time.isoformat(),
        }
        return f"{self.context.absolute_url()}/@@{self.__name__}?{urlencode(params)}"

    @property
    @memoize
    def notifications(self):
        """When batching, we need to take all notifications for this user into
        account, including the ones we don't want to show because they're
        invalid."""
        user = pi_api.userprofile.get_current()
        if not user:
            return []

        # always hide future notifications
        timestamp = datetime.now(timezone.utc)
        if self.request.get("timestamp"):
            # we're scrolling backward in time
            # disallow peeking into the future via request arg
            timestamp = min(
                timestamp, datetime.fromisoformat(self.request.get("timestamp"))
            )

        session = saconfig.session()
        Notification = saconfig.Notification

        notifications = session.query(Notification).filter(
            Notification.recipient_id == user.getId(),
            Notification.channel == "record",
            Notification.scheduled_time < timestamp,
        )

        # When displaying the record channel, mark all notifications as notified,
        # including those beyond the current display batch
        if notifications.filter(Notification.read == None).count():  # noqa: E711
            # on opening the app, mark all notifications as read
            # and reset the counter in the UI
            for notification in notifications.filter(
                Notification.read == None  # noqa: E711
            ).all():
                notification.mark_read()
            # Note that accessing this view directly as a standalone page, e.g.
            # from a desktop notification, fails to reset the counter to 0. I
            # suspect a race condition wherein the websocket is not fully
            # established yet when loading the notifications app.
            pi_api.push.notification_counter_changed_for(user.getId())

        return (
            notifications.order_by(
                Notification.scheduled_time.desc(),
            )
            .limit(self.batch_size)
            .all()
        )

    def get_message_for(self, notification):
        """A notification may become unreachable to the user,
        e.g. when his workspace membership is withdrawn (Unauthorized)
        or when a status update or content object is deleted.

        Instead of erroring out, we just hide the notification.
        """
        try:
            return notification.get_message()
        except saconfig.BrokenNotificationError:
            # to debug: return str(exc)
            return None


class NotificationCounter(BrowserView):
    """Simple view to update the counter using push."""

    @property
    @memoize
    def unread_notifications_query(self):
        user = api.user.get_current()
        session = saconfig.session()
        Notification = saconfig.Notification
        return session.query(Notification).filter(
            Notification.recipient_id == user.getId(),
            Notification.read == None,  # noqa: E711
            Notification.channel == "record",
            Notification.scheduled_time < datetime.now(timezone.utc),
        )

    @memoize
    def getNumber(self):
        return self.unread_notifications_query.count()

    def digits(self):
        return len(str(self.getNumber()))


class DesktopNotifications(BrowserView):
    """Get all notifications which should be shown as desktop notifications."""

    @property
    def proto_view(self):
        return api.content.get_view("proto", context=self.context, request=self.request)

    @property
    def results(self):
        user = pi_api.userprofile.get_current()
        if not user:
            return []

        session = saconfig.session()
        Notification = saconfig.Notification
        # We're working with only unread notifications here, which means those
        # notifications that have come in since the last time the user hit this
        # view.
        # - After initially logging in, that implies: all desktop notifications
        # issued since the user previously logged out.
        # - Once the user has received their first desktop notification,
        # subsequently it means: new desktop notifications issued in the
        # previous minute.
        new_desktop_notifications_query = session.query(Notification).filter(
            Notification.recipient_id == user.getId(),
            Notification.read == None,  # noqa: E711
            Notification.channel == "desktop",
        )

        new_desktop_count = new_desktop_notifications_query.count()
        if new_desktop_count == 0:
            return []

        portal_title = self.proto_view.site_title
        lang = api.portal.get_current_language()
        icon = self.proto_view.logo_icon

        title = api.portal.translate(
            _(
                "title_desktop_notification",
                default="Info from ${portal_title}.",
                mapping={"portal_title": portal_title},
            )
        )

        if new_desktop_count == 1:
            # We have just one notification to show
            notification = new_desktop_notifications_query.first()
            body = notification.message
            url = notification.absolute_url()
        else:
            # Many new desktop notifications typically results
            # when logging in after some time, then receiving the
            # first "really new" (i.e. since login) desktop notification.
            body = _(
                "title_many_desktop_notifications",
                default="You have ${number} new notifications.",
                mapping={"number": new_desktop_count},
            )
            url = f"{self.context.absolute_url()}/apps/notifications"

        # Now mark stage 2 of the delivery as completed
        for notification in new_desktop_notifications_query.all():
            notification.mark_read()

        return [
            {
                "title": title,
                "lang": lang,
                "body": api.portal.translate(body, lang=lang),
                "icon": icon,
                "requireInteraction": True,
                "data": {
                    "url": url,
                },
            }
        ]

    def __call__(self):
        self.request.response.setHeader(
            "Content-Type", "application/json; charset=utf-8"
        )
        return json.dumps(self.results)


class DesktopNotificationsViewlet(ViewletBase):
    """Display the desktop notifications viewlet."""

    @property
    def available(self):
        if not api.portal.get_registry_record(
            "ploneintranet.notifications.desktop.enabled", default=False
        ):
            return False

        personal_settings = api.content.get_view(
            "app-preferences", api.portal.get().apps.preferences, self.request
        )
        return any(
            "desktop" in personal_settings.selected_channels(category)
            for category in personal_settings.categories
        )


class PurgeView(BrowserView):
    """Purge old notifications.

    Delete all the notifications older than N days.
    """

    _minimum_days = 60

    def __call__(self):
        session = saconfig.session()
        Notification = saconfig.Notification

        now = datetime.now(timezone.utc)
        threshold = now - timedelta(days=self._minimum_days)
        base_query = session.query(Notification).filter(
            Notification.scheduled_time < threshold,
        )
        base_query.delete(synchronize_session=False)
        return "OK"
