from Acquisition import aq_chain
from Acquisition import aq_parent
from collective.workspace.interfaces import IWorkspace
from logging import getLogger
from plone import api
from ploneintranet import api as pi_api
from ploneintranet.core import _
from ploneintranet.library.content import ILibrarySection
from ploneintranet.microblog.interfaces import IStatusUpdate
from ploneintranet.microblog.notifications import messages
from ploneintranet.network.notifications.adapters import BaseFollowingCategory
from ploneintranet.news.content import INewsSection
from ploneintranet.notifications.adapters.category_provider import BaseCategory
from ploneintranet.notifications.adapters.category_provider import BaseCategoryProvider
from ploneintranet.notifications.interfaces import IProvideContextUID
from ploneintranet.notifications.interfaces import IResolveContextUID
from ploneintranet.workspace.notifications.adapters import BaseWorkspaceMemberCategory
from ploneintranet.workspace.notifications.adapters import BaseWorkspaceOpenCategory
from zope.component import adapter
from zope.interface import implementer
from zope.interface import provider

logger = getLogger(__name__)


@adapter(IStatusUpdate)
@implementer(IProvideContextUID)
def event_status_update_uid_provider(context):
    """Default adapter that converts the context to a UID
    that will be stored in the notification table.
    """
    if context.__parent__:
        return f"StatusUpdate:{context.__parent__.id}:{context.id}"
    return f"StatusUpdate:{context.id}"


@provider(IResolveContextUID)
def event_status_update_uid_resolver(uid):
    """Default utility that converts the UID stored in the notification table
    in to an object.
    """
    if ":" in uid:
        child_id, grandchild_id = uid.partition(":")[::2]
        child = pi_api.microblog.statusupdate.unrestricted_get(child_id)
        if child is None:
            return None
        return child[int(grandchild_id)]
    return pi_api.microblog.statusupdate.unrestricted_get(uid)


class StatusUpdateMessages:
    """Mixin that calculates the 'right' message for each recipient, for each category
    of status update notifications.

    Adapts StatusUpdate.

    NB tests for this live in ploneintranet.suite.tests.test_notifications
    """

    @property
    def thread_creator_id(self):
        return self.context.thread_parent.creator

    @property
    def thread_creator_name(self):
        profiles_view = api.content.get_view("view", api.portal.get().profiles)
        return profiles_view.get_fullname_for(self.thread_creator_id)

    @property
    def max_mentions(self):
        """Do not spam large groups"""
        # Turn this into a registry record, if you want
        return 20

    @property
    def thread_participant_ids(self):
        """Who we consider a participant depends on the level of the status update.
        - level1 post: the users mentioned
        - level2 reply:
          o the author and mentions of the level1 parent
          o any authors and mentions in sibling level2 replies
            (except future replies)
        - level3 comment on a reply:
          o the author and mentions of the level1 grandparent
          o the author and mentions of the level2 parent
            (so not any siblings of our level2 parent)
          o authors and mentions of sibling level3 comments
            (except future comments)
        """
        future_author_ids = set()
        # a standalone level1 post without replies
        if self.context.is_post:
            return set(self.context.mentions)

        # an actual thread with comments/replies: notify the initial author
        thread_participant_ids = {self.thread_creator_id}
        # Any @group mentions are already expanded into discrete user ids at
        # statusupdate creation time. So we cannot filter out the groups, we can
        # only limit the total number of mentions.
        if len(self.context.thread_parent.mentions) <= self.max_mentions:
            thread_participant_ids.update(self.context.thread_parent.mentions)

        if self.context.is_comment:
            # A level2 comment on a level1 post
            # All participants in all level2 replies are added to the thread
            # participants i.e. if you leave a reply, you will be notified of a
            # sibling reply
            for comment in self.context.thread_parent.replies():
                if comment.id > self.context.id:
                    # don't notify future participants - they are already
                    # aware of the history of this thread
                    future_author_ids.add(comment.userid)
                    continue
                thread_participant_ids.add(comment.userid)
                if len(comment.mentions) <= self.max_mentions:
                    thread_participant_ids.update(comment.mentions)

        elif self.context.is_reply:
            # A level3 reply on a comment on a post
            # Level3 replies on level2 comments only notify the users in thead
            # sub-thread i.e.
            # - those involved in the original post
            # - those involved in the level2 comment this level3 responds to,
            #   but not those involved in sibling level2 comments to the original post
            # - those involved in level3 sibling replies to the level2 parent comment
            comment = self.context.parent
            thread_participant_ids.add(comment.userid)
            if len(comment.mentions) <= self.max_mentions:
                thread_participant_ids.update(comment.mentions)
            # reply author and mentions will be added in the loop below
            for reply in comment.get_comments():  # the API naming here is unfortunate.
                if reply.id > self.context.id:
                    future_author_ids.add(reply.userid)
                    continue
                thread_participant_ids.add(reply.userid)
                if len(reply.mentions) <= self.max_mentions:
                    thread_participant_ids.update(reply.mentions)

        # Current author will already be suppressed by the sending machinery.
        # Removing here also for test comprehensibility.
        return thread_participant_ids - future_author_ids - {self.context.userid}

    def get_section_name(self, content_context):
        if not content_context:
            # No content context or section in case of activity stream posts.
            return ""

        title = ""
        for parent in aq_chain(content_context):
            # Get a news or library section.
            if INewsSection.providedBy(parent) or ILibrarySection.providedBy(parent):
                title = parent.Title()
                break

        if not title:
            # Generic fallback
            title = aq_parent(content_context).Title()

        return title

    def get_message_for(self, notification, with_text=True, highlight=False):
        status_update = self.context
        recipient_id = notification.recipient_id
        logged_event = notification.logged_event

        in_workspace = bool(status_update.microblog_context)
        in_private_workspace = (
            in_workspace
            and not status_update.microblog_context.accessible_to_intranet_users
        )
        enhanced_security = (
            api.portal.get_registry_record(
                name="ploneintranet.notifications.enhanced_security", default=False
            )
            and in_private_workspace
            and notification.channel
            in {
                "email",
                "digest_daily",
                "digest_weekly",
            }
        )
        if enhanced_security:
            with_text = False

        # NB notifications about documents should use with_text, not show_post_title
        show_post_title = bool(with_text and status_update.thread_parent.Title())

        # If ploneintranet.microblog.extract.auto==True and a workspace
        # post has an attachment, the attachment is stored in the workspace
        # and we now have a workspace post with a content context.
        is_autoextracted_post = bool(
            status_update.is_post
            and in_workspace
            and status_update.is_content_update
            and status_update.thread_parent.Title()
        )
        is_comment_or_reply_on_autoextracted_post = bool(
            (status_update.is_comment or status_update.is_reply)
            and in_workspace
            and status_update.is_content_update
            and status_update.thread_parent.Title()
        )
        is_comment_or_reply_on_document = bool(
            (status_update.is_comment or status_update.is_reply)
            and status_update.is_content_update
            and not is_comment_or_reply_on_autoextracted_post
        )

        # build up the mapping outside of main logic, for readability
        proto = api.content.get_view("proto", api.portal.get())
        mapping = {
            "actor": logged_event.actor_fullname,
            "author": self.thread_creator_name,
            "object_creation_date": proto.format_date(status_update.thread_parent.date),
        }

        # For autoextracted posts and comments/replies on those, we show the
        # post text as title.
        if status_update.thread_parent.Title():
            mapping["object"] = self.maybe_process_title(
                status_update.thread_parent.Title(), highlight
            )
        elif status_update.is_content_update:
            mapping["object"] = self.maybe_process_title(
                status_update.content_context.Title(), highlight
            )

        if enhanced_security:
            mapping["location"] = _(
                "label_a_private_workspace", default="a private workspace"
            )
        elif in_workspace:
            mapping["location"] = status_update.microblog_context.Title()
        else:
            mapping["location"] = self.get_section_name(status_update.content_context)

        # mentions
        if recipient_id in status_update.mentions:
            if status_update.is_post:
                return messages.variant_mentioned_you_in_a_post(
                    mapping, show_post_title
                )
            if in_workspace:
                if is_comment_or_reply_on_document:
                    return messages.variant_mentioned_you_in_a_comment_on_a_document_in_workspace(  # noqa: E501
                        mapping, with_text
                    )
                return messages.variant_mentioned_you_in_a_comment_in_workspace(
                    mapping, show_post_title
                )
            elif is_comment_or_reply_on_document:  # news or library
                return messages.variant_mentioned_you_in_a_comment_on_a_document(
                    mapping, with_text
                )
            return messages.variant_mentioned_you_in_a_comment(mapping, show_post_title)

        # creator: these are always comments

        # a content item may be published by a different user than the creator
        # in that case we inform the content creator as "your document"
        # and the reviewer who is the thread creator as "the document"
        if (
            is_comment_or_reply_on_document
            and recipient_id == status_update.content_context.Creator()
        ):
            # creator
            return messages.variant_commented_on_a_document_you_created(
                mapping, with_text
            )
        if is_comment_or_reply_on_document and recipient_id == self.thread_creator_id:
            # reviewer
            return messages.variant_commented_on_a_document(mapping, with_text)

        # regular comments on a normal thread go to the original post author
        if recipient_id == self.thread_creator_id:
            if in_workspace:
                return messages.variant_user_commented_on_your_post_in_workspace(
                    mapping, show_post_title
                )
            # must be a comment outside of a workspace or document
            return messages.variant_commented_on_your_post(mapping, show_post_title)

        # other participants: not in mentions, nor thread creator

        # content posts for created/published are not handled via statusupdates
        # However we do handle autoextracted posts here
        if is_autoextracted_post:
            return messages.variant_created_a_new_post_in_location(
                mapping, show_post_title
            )

        # handle comments on autoextracted posts separately
        if is_comment_or_reply_on_autoextracted_post:
            return messages.variant_user_commented_on_post_in_workspace(
                mapping, show_post_title
            )

        # content comment
        if is_comment_or_reply_on_document:
            return messages.variant_commented_on_a_document(mapping, with_text)

        if in_workspace:
            # post in workspace
            if status_update.is_post:
                return messages.variant_created_a_new_post_in_location(
                    mapping, show_post_title
                )
            # comment in workspace, not on a document
            return messages.variant_user_commented_on_post_in_workspace(
                mapping, show_post_title
            )

        # other participants: outside of workspace or document
        #
        # dashboard post
        if status_update.is_post:
            return messages.variant_created_a_new_post(mapping, show_post_title)
        # dashboard comment/reply, news or library comment/reply
        return messages.variant_commented_on_a_post_from(mapping, show_post_title)


# adapts StatusUpdate
class StatusUpdateInteractionsCategory(StatusUpdateMessages, BaseCategory):
    """Mentions, and replies to creator and thread participants."""

    name = "interactions"
    default_channels = {"email", "desktop"}
    title = _(
        "label_interactions_with_other_people",
        default="Interactions with other people",
    )
    description = _(
        "message_interactions_with_other_people",
        default=(
            "You will be notified as soon as someone replies "
            "to a document created by you or in a discussion "
            "in which you also participate. "
            "You will also be notified as soon as someone "
            "mentions you."
        ),
    )

    @property
    def recipient_ids(self):
        ids = self.thread_participant_ids
        if self.context.content_context:
            # in case the thread was started by a reviewer publishing a document
            ids.add(self.context.content_context.Creator())
        return ids


class StatusUpdateFollowingCategory(StatusUpdateMessages, BaseFollowingCategory):
    """Updates:
    - by people you're following
    - with tags you're following
    - from workspaces you're following
    - on content you're following.
    """

    def content_context_accessible_for(self, username: str) -> bool:
        """Check if the user can see the microblog context."""
        try:
            return api.user.has_permission(
                "View",
                obj=self.context.content_context,
                username=username,
            )
        except api.exc.UserNotFoundError:
            logger.warning("Cannot find user: %r", username)
        except Exception:
            logger.exception("Error while resolving username: %r", username)
        return False

    @property
    def recipient_ids(self):
        ploneintranet_network = api.portal.get_tool("ploneintranet_network")
        followers = set(
            ploneintranet_network.get_followers("user", self.context.creator)
        )
        if self.context.tags:
            # add tag watchers
            for tag in self.context.tags:
                followers.update(set(ploneintranet_network.get_followers("tag", tag)))
        if self.context.microblog_context:
            # add workspace watchers
            followers.update(
                set(
                    ploneintranet_network.get_watchers(
                        "workspace", self.context.microblog_context.UID()
                    )
                )
            )
        if self.context.content_context:
            # add content watchers
            followers.update(
                set(
                    ploneintranet_network.get_followers(
                        "content", self.context.content_context.UID()
                    )
                )
            )

        if (
            followers
            and self.context.microblog_context
            and not self.context.microblog_context.accessible_to_intranet_users
        ):
            # remove non-members from private workspace updates
            followers = followers.intersection(
                self.expand_principal_ids(
                    IWorkspace(self.context.microblog_context).members
                )
            )

        if followers and self.context.content_context:
            # Constrain to followers who have access to the content context.
            # While this is a costly check, it's better to perform it here,
            # before dispatch. Any notification not scheduled here, will not hit
            # the same check again at dispatch/render time. Note that we do
            # perform this security check again on dispatch for all channels (in
            # the aggregator), and on render in the web/record channel (because
            # the security may have changed between dispatch and display
            # rendering).
            followers = {
                follower
                for follower in followers
                if self.content_context_accessible_for(follower)
            }

        return followers


class StatusUpdateActivityPostsCategory(StatusUpdateMessages, BaseCategory):
    """Posts on the dashboard, i.e. outside of workspaces."""

    name = "activity_posts"
    default_channels = {"desktop"}
    title = _(
        "label_new_posts",
        default="New posts",
    )
    description = ""

    @property
    def recipient_ids(self):
        if self.context.microblog_context:
            # We do not notify workspace status updates
            return set()
        if self.context.is_comment or self.context.is_reply:
            # We do not notify comments or replies in this category
            return set()
        # Only notify all for toplevel dashboard posts
        return self.all_intranet_user_ids


class StatusUpdateActivityCommentsCategory(StatusUpdateMessages, BaseCategory):
    """Comments on the dashboard, i.e. outside of workspaces."""

    name = "activity_comments"
    default_channels = set()  # only record
    title = _(
        "label_new_comments",
        default="New comments",
    )
    description = ""

    @property
    def recipient_ids(self):
        if self.context.microblog_context:
            # We do not notify workspace status updates
            return set()
        if self.context.is_comment or self.context.is_reply:
            # Dashboard comments
            return self.all_intranet_user_ids
        # We do not notify posts in this category
        return set()


class StatusUpdateWorkspaceMemberCategory(
    StatusUpdateMessages, BaseWorkspaceMemberCategory
):
    """Posts and comments to users that are explicit members, i.e. whose access
    to a workspace is not via a group or via accessible_to_all_users."""

    @property
    def recipient_ids(self):
        if not self.context.microblog_context:
            return set()
        return IWorkspace(self.context.microblog_context).members


class StatusUpdateWorkspaceOpenCategory(
    StatusUpdateMessages, BaseWorkspaceOpenCategory
):
    """Posts and comments in open workspaces.

    This excludes explicit members, who will receive a workspace_member
    notification instead.
    """

    @property
    def recipient_ids(self):
        if not self.context.microblog_context:
            return set()
        workspace = self.context.microblog_context
        if not workspace.accessible_to_intranet_users:
            return set()
        return self.all_intranet_user_ids - self.expand_principal_ids(
            IWorkspace(self.context.microblog_context).members
        )


class NewsEditorsCommentCategory(StatusUpdateMessages, BaseCategory):
    """Inform the news editors group of a comment on a news item."""

    name = "news_comment_moderate"
    default_channels = {"desktop", "email"}
    title = _("label_new_comments_news_item", default="New comments on news items")
    description = ""
    _app_id = "news"

    @property
    def recipient_ids(self):
        if not (
            self.context.is_human_update
            and self.context.is_content_update
            and self.context.content_context.portal_type == "News Item"
        ):
            return set()
        news_moderators_group_id = api.portal.get_registry_record(
            "ploneintranet.notifications.news_moderators", default=None
        )
        if not news_moderators_group_id:
            return set()
        return {news_moderators_group_id}


class StatusUpdateCategoryProvider(BaseCategoryProvider):
    """Provide the appropriate status update notification categories,
    depending on where the status update was made (dash, workspace)."""

    @property
    def category_factories(self):
        if not self.context:
            # an old-style comment-reply that cannot be resolved
            return []

        return [
            StatusUpdateInteractionsCategory,
            StatusUpdateFollowingCategory,
            StatusUpdateActivityPostsCategory,
            StatusUpdateActivityCommentsCategory,
            StatusUpdateWorkspaceMemberCategory,
            StatusUpdateWorkspaceOpenCategory,
            NewsEditorsCommentCategory,
        ]
