from collective.workspace.interfaces import IWorkspace
from datetime import datetime
from lxml import etree
from plone import api
from plone.memoize.view import memoize
from plone.memoize.view import memoize_contextless
from plone.registry.interfaces import IRegistry
from ploneintranet import api as piapi
from ploneintranet.attachments.attachments import IAttachmentStoragable
from ploneintranet.attachments.utils import extract_and_add_attachments
from ploneintranet.core import _
from ploneintranet.layout.interfaces import IDiazoNoTemplate
from ploneintranet.microblog.browser.extract import StatusupdateExtractAttachments
from ploneintranet.microblog.statusupdate import StatusUpdate
from ploneintranet.workspace.config import INTRANET_USERS_GROUP_ID
from ploneintranet.workspace.workspacefolder import IWorkspaceFolder
from Products.Five.browser import BrowserView
from zope.component import queryUtility
from zope.interface import implementer

import logging

logger = logging.getLogger(__name__)


@implementer(IDiazoNoTemplate)
class UpdateSocialBase(BrowserView):
    """Shared base class for:

    - update-social.html
    - post-well-done.html
    - comment-well-said.html
    - panel-users
    """

    input_prefix = "form.widgets."
    button_prefix = "form.buttons."

    placeholder_i18nmessage = _("add_statusupdate_button", default="Post a message…")
    should_autofocus = False

    @property
    @memoize
    def placeholder(self):
        return api.portal.translate(self.placeholder_i18nmessage)

    @property
    @memoize
    def is_posting(self):
        """Check if the form was submitted."""
        return ("%sstatusupdate" % self.button_prefix) in self.request.form

    @property
    @memoize
    def microblog_context(self):
        """The context of this microblog post (the portal, a workspace, and so
        on...)"""
        if isinstance(self.context, StatusUpdate):
            microblog_context = self.context.microblog_context
        else:
            microblog_context = piapi.microblog.get_microblog_context(self.context)
        if microblog_context:
            return microblog_context
        else:
            return api.portal.get()

    @property
    def microblog_context_url(self):
        return self.microblog_context.absolute_url()

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

    @property
    @memoize_contextless
    def portal(self):
        return api.portal.get()

    @property
    @memoize_contextless
    def portal_url(self):
        return self.portal.absolute_url()


class UpdateSocialHandler(UpdateSocialBase):
    """Shared HTTP POST handler for:

    - post-well-done.html
    - comment-well-said.html

    So excluding update-social.html.
    """

    @property
    @memoize
    def post_text(self):
        """The text that was posted."""
        text = self.request.form.get(("%stext" % self.input_prefix), "")

        # The pat-tiptap pattern's suggestion feature adds a lot of attributes
        # by purpose (href, class, data-pat-*, etc) so that this feature is
        # usable in prototype and can be universally integrated without much
        # backend logic. It's actually a tool to help to add links to existing
        # items to the text.
        # However, in Quaive and probably other CMS we do not want to store
        # these attributes directly in the text. The ``href`` can change, as
        # well as ``data-pat-inject`` and other attributes.
        # Therefore we strip everything except the target id (from
        # `data-mention`` and ``data-tag``) out of the text, just to
        # dynamically add them when displaying.
        #
        # Also see: ploneintranet.activitystream.browser.statusupdate.decorated_text
        # And:      ploneintranet.activitystream.browser.utils

        # Note: Using the HTMLParser always wraps the markup in ``<html><body>``
        #       structure.
        #       Using no explicit parser fails with multiple top-level nodes, which
        #       is a common case for a inline text editor.
        #       Therefore we're using the HTMLParser and afterwards removing the
        #       ``<html><body>`` structure again.
        parser = etree.HTMLParser()
        parsed = etree.fromstring(text, parser)

        if parsed is None:
            return

        # Strip out all attributes except data-mention
        for el in parsed.xpath("//a[@data-mention]"):
            for attr in el.attrib:
                if attr == "data-mention":
                    continue
                del el.attrib[attr]

        # Strip out all attributes except data-tag
        for el in parsed.xpath("//a[@data-tag]"):
            for attr in el.attrib:
                if attr == "data-tag":
                    continue
                del el.attrib[attr]

        text = etree.tostring(parsed)
        # Now removing the ``<html><body>`` wrapper
        text = (
            text.decode("utf-8")
            .replace("<html>", "")
            .replace("<body>", "")
            .replace("</body>", "")
            .replace("</html>", "")
        )

        return text

    @property
    @memoize
    def parsed_text(self):
        """HTML-parsed text."""
        parsed = []
        if self.post_text:
            parser = etree.HTMLParser()
            parsed = etree.fromstring(self.post_text, parser)
        return parsed

    @property
    @memoize
    def pas_view(self):
        """The PAS view."""
        return api.content.get_view("pas_view", self.context, self.request)

    @property
    def post_tags(self):
        """The tags that were added.

        Parse the comment text for any data-tag attributes. The value is
        the id/title of the tag itself. Use this to resolve the
        mentioned users without having any duplicates.
        """
        if not len(self.parsed_text):
            return {}
        tags = {
            it.attrib["data-tag"] for it in self.parsed_text.xpath("//a[@data-tag]")
        }
        return tags

    @property
    def post_mentions(self):
        """The mentions that were added.

        Parse the comment text for any data-mention attributes. The
        value is a UID with a prefix. Use this to resolve the mentioned
        users without having any duplicates.
        """
        users = set()
        if not len(self.parsed_text):
            return users
        # iterate over all mentions
        for el in self.parsed_text.xpath("//a[@data-mention]"):
            mention = el.attrib["data-mention"]

            # prefix:uid, e.g. ``workgroup:1234`` or ``package:category:uid``
            sep = ":"
            mention_type, sep, mention_uid = mention.partition(sep)

            user_uids = set()

            if mention_type == "userprofile":
                # Mention a single user.
                user_uids.add(mention)

            if mention_type == "workgroup":
                # Mention a whole group.
                user_uids.update(self.pas_view.descendants(mention))

            if mention_type == "workspace":
                # Mention a whole workspace but fake the intranet users are not there.
                member_ids = set(IWorkspace(self.microblog_context).members)
                member_ids.discard(INTRANET_USERS_GROUP_ID)
                for member_id in member_ids:
                    pas_uid = self.pas_view.principalname2uid(member_id)
                    if pas_uid.startswith("userprofile:"):
                        users.add(member_id)
                    else:
                        try:
                            user_uids.update(self.pas_view.descendants(pas_uid))
                        except KeyError:
                            pass
            users.update(self.pas_view.uids2principalnames(user_uids))

        return users

    @property
    @memoize
    def post_attachment(self):
        """The attachmented that was posted (if any)."""
        return self.request.form.get(("%sattachments" % self.input_prefix), None)

    def create_post_attachment(self, post):
        """Check:
         - if the post supports attachments
         - if we have an attachment posted

        If both are True attach the data to the post.
        """
        if not IAttachmentStoragable.providedBy(post):
            return
        if not self.post_attachment:
            return
        token = self.request.get("attachment-form-token")
        extract_and_add_attachments(
            self.post_attachment, post, workspace=self.microblog_context, token=token
        )

    def maybe_extract_post_attachment(self, post):
        """If auto-extract is enabled, move the attachment into the extraction
        target folder."""
        registry = queryUtility(IRegistry)
        if registry["ploneintranet.microblog.extract.auto"] is True:
            StatusupdateExtractAttachments(post, self.request).extract()

    def init_post(self) -> StatusUpdate:
        """This base class method should be overridden by subclasses to do
        something meaningfull."""
        raise NotImplementedError

    def create_post(self):
        """Return the post object or None.

        To really proceed in the status update creation we require at least
        one of these two properties to evaluate to True
         - post_text
         - post_attachment

        If not of them evaluates to True we return None
        """
        if not (self.post_text or self.post_attachment):
            return

        # This is customized by the subclasses
        post = self.init_post()

        self.create_post_attachment(post)
        self.maybe_extract_post_attachment(post)
        return post

    def update(self):
        """When updating we may want to process the form data and, if needed,
        create a post."""
        if self.is_posting:
            self.post = self.create_post()

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.post = None

    def __call__(self, *args, **kwargs):
        """Call the multiadapter update.

        We need to call the update manually (Tile instances don't do it)
        """
        self.update()
        return super().__call__(*args, **kwargs)


class UpdateSocialView(UpdateSocialBase):
    """Show the form for creating a post or reply.

    Does not actually handle the HTTP POST.
    """

    post_id = ""
    container_id = ""

    @property
    def form_id(self):
        return f"post-box{self.id_suffix}"

    @property
    def form_data_pat_inject(self):
        return (
            f"source: #activity-stream{self.id_suffix}; "
            f"target: #stream-items{self.id_suffix}::before && "
            f"#post-box{self.id_suffix}"
        )

    @property
    def form_action(self):
        return f"{self.microblog_context_url}/@@post-well-done.html"

    @property
    @memoize
    def attachment_form_token(self):
        """Set up a token used in the attachment form."""
        member = api.user.get_current()
        userid = member.getId()
        return "{}-{}".format(userid, datetime.utcnow().strftime("%Y%m%d%H%M%S%f"))


class StatusUpdatePostBoxView(UpdateSocialView):
    placeholder_i18nmessage = _("leave_a_comment", default="Leave a comment…")

    @property
    def should_autofocus(self):
        return self.context.thread_id

    @property
    def post_id(self):
        return self.context.id

    @property
    def form_action(self):
        return f"{self.microblog_context_url}/@@comment-well-said.html"

    @property
    def form_id(self):
        return f"comment_box_{self.context.id}{self.id_suffix}"

    @property
    def container_id(self):
        """the container id will be the comments attribute if the post is
        already a comment.

        Otherwise it will be the post id
        """
        return self.context.thread_id

    @property
    def form_data_pat_inject(self):
        """This will return the a string to fill the data-pat-inject attribute.

        It varies according to the fact we are creating:
         - a standalone post (`parent_child_ids` will be `None`)
         - a comment to a post (`parent_child_ids` will start with the string `"None"`)
         - a reply to a comment
        """
        template = (
            "source: #comment-trail; "
            "target: #new-comment-%(post_id)s ::element::before "
        )
        if not self.context.thread_id:
            # For comments to posts we need to reinject the postbox.
            # This is not needed for comment replies because the postbox is
            # hidden using pat-switch
            template += (
                "&& source: #microblog-%(post_id)s; target: #microblog-%(post_id)s "
            )
        return template % {"post_id": self.post_id}


class AddReplyToReplyView(StatusUpdatePostBoxView):
    @property
    def container_id(self):
        return self.context.thread_id

    @property
    def form_data_pat_inject(self):
        """This will return the a string to fill the data-pat-inject attribute.

        It varies according to the fact we are creating:
         - a standalone post (`parent_child_ids` will be `None`)
         - a comment to a post (`parent_child_ids` will start with the string `"None"`)
         - a reply to a comment
        """
        return (
            "source: #comment-trail; "
            "target: #new-comment-{container_id} ::element::before "
        ).format(container_id=self.container_id)


class FirstCommentView(UpdateSocialHandler):
    """This view initializes the comment stream on contexts that still have no
    comments."""

    direct = False
    placeholder_i18nmessage = _("leave_a_comment", default="Leave a comment…")
    form_data_pat_inject = "#comments-document-comments"
    container_id = ""

    @property
    def post_id(self):
        return self.context.id

    @property
    def form_id(self):
        return f"comment_box_{self.context.UID()[:6]}{self.id_suffix}"

    @property
    @memoize
    def attachment_form_token(self):
        """Set up a token used in the attachment form."""
        member = api.user.get_current()
        userid = member.getId()
        return "{}-{}".format(userid, datetime.utcnow().strftime("%Y%m%d%H%M%S%f"))

    @memoize
    def form_action(self):
        """Submit to itself."""
        return "/".join((self.context.absolute_url(), self.__name__))

    def init_post(self):
        """We need first to initialize the content stream, then we can
        comment."""
        if not (self.post_text or self.post_attachment):
            return
        parent = piapi.microblog.statusupdate.create(
            content_context=self.context,
            action_verb="created",
            tags=self.context.Subject() or None,
            userid=self.context.Creator(),
            time=self.context.created().asdatetime(),
        )
        post = piapi.microblog.statusupdate.create(
            text=self.post_text,
            microblog_context=self.microblog_context,
            thread_id=parent.id,
            mention_ids=self.post_mentions,
            tags=self.post_tags,
        )
        return post

    def __call__(self):
        """Call update and see if a post was created.

        If yes return the content stream that will contain the newly
        created post and a commentbox.
        """
        self.update()
        if not self.post:
            return super().__call__()
        return self.request.response.redirect(
            "%s/@@content-stream.html" % self.context.absolute_url()
        )
