from AccessControl import Unauthorized
from functools import partial
from plone import api
from plone.event.utils import pydt
from plone.memoize.view import memoize
from plone.tiles import Tile
from ploneintranet import api as piapi
from ploneintranet.activitystream.interfaces import ISimplifiedCommentTrail
from ploneintranet.core import _
from ploneintranet.layout.interfaces import IDiazoDashboardTemplate
from ploneintranet.network.interfaces import INetworkGraph
from ploneintranet.userprofile.content.userprofile import IUserProfile
from ploneintranet.workspace.workspacefolder import IWorkspaceFolder
from Products.Five.browser import BrowserView
from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
from zope.component import queryUtility
from zope.interface import implementer
from zope.publisher.interfaces import IPublishTraverse

import datetime
import logging
import six
import six.moves.urllib.error
import six.moves.urllib.parse
import six.moves.urllib.request

logger = logging.getLogger(__name__)


class StreamBase:
    """Shared base class for activity streams"""

    manual_load_more = False
    show_filter = False
    wrap_batch = False

    def __init__(self, context, request):
        self.context = context
        self.request = request
        self.tag = None  # only used in TagStream below
        self.last_seen = request.get("last_seen")
        self.stop_asking = False

    @property
    def empty(self):
        return self.stop_asking and self.last_seen is None

    @property
    @memoize
    def count(self):
        """The count can be configured through the b_size parameter"""
        try:
            return int(self.request.form.get("b_size"))
        except TypeError:
            if self.stream_num_columns > 1:
                return 20
            return 5

    @property
    def stream_filter(self):
        """Show all items unless request parameter specifies differently"""
        return self.request.get("stream_filter", "all")

    @property
    def next_max(self):
        if self.last_seen:
            return int(self.last_seen) - 1

    @property
    @memoize
    def toLocalizedTime(self):
        """Facade for the toLocalizedTime method"""
        return api.portal.get_tool("translation_service").toLocalizedTime

    @property
    @memoize
    def microblog_context(self):
        """Returns the microblog context"""
        return piapi.microblog.get_microblog_context(self.context)

    @property
    def id_suffix(self):
        if IWorkspaceFolder.providedBy(self.microblog_context):
            return "-workspaces"
        return ""

    def filter_statusupdates(self, statusupdates):
        """This method filters the microblog StatusUpdates

        The idea is:
         - if a StatusUpdate is a comment return the parent StatusUpdate
         - show threads only once

        This deduplication is operative across the autoload batch size:
        it will only show the most recent update even if updates within the
        same thread are spread across different batch slices.

        Additionally:
        - if a content_context has multiple content share posts,
          suppress older content shares without replies.

        The effectiveness of this for content_context is limited
        by the autoexpand: the current view "sees" only it's current
        15 updates.

        Additionally, this performs a postprocessing filter on content updates
        in case a user has access to a microblog_context workspace
        but not to the (unpublished) content_context object
        """
        seen_content_contexts = set()
        good_statusupdates = []
        container = piapi.microblog.get_microblog()

        if not self.request.get("all_comments", False):
            # show only most recent post in thread, except when
            # explicitly rendering the whole thread (which re-uses
            # this stream tile, a bit awkwardly)
            if self.tag is None:
                statusupdates = filter(
                    container.is_most_recent_in_thread, statusupdates
                )
            else:
                statusupdates = filter(
                    partial(container.is_most_recent_in_thread, tags=[self.tag]),
                    statusupdates,
                )

        for su in statusupdates:
            if su.thread_id:
                # resolve reply into toplevel
                thread_su = container.get(su.thread_id)
            else:
                thread_su = su

            # content updates postprocessing filter
            try:
                # su.content_context always equals thread_su.content_context
                content_context = su.content_context
            except Unauthorized:
                # skip thread on inaccessible content (e.g. draft)
                continue

            # Some Statusapdates should never be shown in the stream
            if ISimplifiedCommentTrail.providedBy(content_context):
                continue

            # double content share filter works on original reply/toplevel
            if content_context:
                if content_context in seen_content_contexts and not su.thread_id:
                    # older toplevel share, suppress
                    continue
                seen_content_contexts.add(content_context)

            # suppress future dated, e.g. newsitem_published
            if thread_su.date is not None and pydt(
                thread_su.date
            ) > datetime.datetime.now(datetime.UTC):
                continue
            # even suppress 'created' posts for newsitems scheduled in the future
            if content_context and content_context.effective() is not None:
                if pydt(content_context.effective()) > datetime.datetime.now(
                    datetime.UTC
                ):
                    continue

            good_statusupdates.append(thread_su)

        return good_statusupdates

    def get_statusupdates(self):
        """This will return all the StatusUpdates which are not comments

        The activity are sorted by reverse chronological order
        """
        container = piapi.microblog.get_microblog()
        post_id = self.request.get("post_id")
        if post_id:
            try:
                return [container.get(post_id)]
            except KeyError:
                return []

        if self.microblog_context:
            # support ploneintranet.workspace integration
            statusupdates = container.context_values(
                self.microblog_context, max=self.next_max, limit=self.count
            )
        elif IUserProfile.providedBy(self.context):
            # Get the updates for this user
            statusupdates = container.user_values(
                self.context.username, max=self.next_max, limit=self.count
            )
        elif self.stream_filter == "network":
            # Only activities from people and things I follow
            graph = api.portal.get_tool("ploneintranet_network")
            users = graph.unpack(graph.get_following("user"))
            users.append(api.user.get_current().id)  # show own updates also
            tags = graph.unpack(graph.get_following("tag"))
            statusupdates = container.values(
                max=self.next_max, limit=self.count, users=users, tags=tags
            )
        elif self.stream_filter == "content":
            # show only content updates and replies to those
            statusupdates = container.is_content_values(
                max=self.next_max, limit=self.count
            )
        elif self.stream_filter == "human":
            # exclude auto-created content updates without replies
            statusupdates = container.is_human_values(
                max=self.next_max, limit=self.count
            )
        elif self.stream_filter in ("interactions", "posted", "likes"):
            raise NotImplementedError(
                "unsupported stream filter: %s" % self.stream_filter
            )
        else:
            # default implementation: all activities
            statusupdates = container.values(
                max=self.next_max, limit=self.count, tags=self.tag
            )
        return statusupdates

    @property
    @memoize
    def statusupdates_autoexpand(self):
        """The list of our activities"""
        # unfiltered for autoexpand management
        statusupdates = [x for x in self.get_statusupdates()]

        # filtered for display
        yield from self.filter_statusupdates(statusupdates)

        # stop autoexpand when last batch is empty
        if len(statusupdates) == 0:
            self.stop_asking = True
        else:
            self.last_seen = statusupdates[-1].id

    @property
    @memoize
    def post_views(self):
        """The activity as views"""
        for statusupdate in self.statusupdates_autoexpand:
            yield api.content.get_view("post.html", statusupdate, self.request)

    @property
    @memoize
    def stream_num_columns(self):
        if self.microblog_context:
            return getattr(self.microblog_context, "stream_num_columns", 1)
        return 0


class StreamTile(StreamBase, Tile):
    """Stream as a tile, for use in other views (userprofile, workspace, etc.)"""

    index = ViewPageTemplateFile("templates/stream_tile.pt")


class PortletStreamTile(StreamBase, Tile):
    """Stream as a tile, for use in the dashboard portlet"""

    index = ViewPageTemplateFile("templates/stream_tile.pt")
    manual_load_more = True
    show_filter = True
    wrap_batch = True

    @property
    def stream_filter(self):
        """we now default to the 'human' filter"""
        return self.request.get("stream_filter", "human")


@implementer(IPublishTraverse, IDiazoDashboardTemplate)
class TagStream(StreamBase, BrowserView):
    """Show the stream, filtered by a tag.

    Gets tag from the url.
    For example, /tagstream/foo will display
    the stream of activities tagged #foo.
    """

    wrap_batch = True

    @property
    def stream_filter(self):
        """do not apply any filter"""
        return None

    def publishTraverse(self, request, name):
        self.tag = name
        # stop traversing, we have arrived
        request["TraversalRequestNameStack"] = []
        # return self so the publisher calls this view
        return self

    def handle_action(self):
        g = queryUtility(INetworkGraph)
        if g.is_following("tag", self.tag):
            g.unfollow("tag", self.tag)
        else:
            g.follow("tag", self.tag)

    @property
    def show_stream(self):
        """Optimize by not rendering stream on POST"""
        return self.request.method == "GET"

    @property
    def following(self):
        g = queryUtility(INetworkGraph)
        return g.is_following("tag", self.tag)

    @property
    def url(self):
        return "{}/@@tagstream/{}".format(
            self.context.absolute_url(),
            six.moves.urllib.parse.quote(self.tag),
        )

    def __call__(self):
        if self.request.method == "POST":
            self.handle_action()
        if not self.tag:
            api.portal.show_message(
                message=_("No tag selected"), request=self.request, type="warning"
            )
            return self.request.response.redirect(self.context.absolute_url())
        return super().__call__()
