from plone import api
from plone.app.contenttypes.behaviors.leadimage import ILeadImage
from plone.app.contenttypes.content import File
from plone.app.contenttypes.content import Image
from plone.app.contenttypes.content import NewsItem
from plone.memoize.view import memoize
from plone.memoize.view import memoize_contextless
from plone.registry.interfaces import IRegistry
from ploneintranet import api as pi_api
from ploneintranet.activitystream.browser.utils import link_tags
from ploneintranet.activitystream.browser.utils import link_urls
from ploneintranet.activitystream.browser.utils import link_users
from ploneintranet.activitystream.interfaces import IOmittedCommentTrail
from ploneintranet.activitystream.interfaces import ISimplifiedCommentTrail
from ploneintranet.attachments.utils import IAttachmentStorage
from ploneintranet.core import _
from ploneintranet.layout.interfaces import IDiazoNoTemplate
from ploneintranet.workspace.utils import in_workspace
from ploneintranet.workspace.workspacefolder import IWorkspaceFolder
from Products.CMFCore.WorkflowCore import WorkflowException
from Products.Five.browser import BrowserView
from zope.component import queryUtility
from zope.interface import implementer

import DateTime
import logging

logger = logging.getLogger("ploneintranet.activitystream")


@implementer(IDiazoNoTemplate)
class StatusUpdateView(BrowserView):
    """This view renders a status update

    See templates/post.html for an explanation of the various
    rendering modes.

    On top of that it also powers templates/comment.html

    The API could use some cleanup.
    """

    newpostbox_placeholder = _("leave_a_comment", default="Leave a comment…")

    @property
    def in_workspace(self):
        """statusupdate context is the portal, independently whether
        they are displayed on dashboard or workspace.
        Also content_context is the workspace, independently where the post
        is displayed. So we must rely on the request here to find out
        where the post is displayed, so that we can adjust injection params
        properly"""
        return in_workspace(self.request.PARENTS[0])

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

    @property
    @memoize_contextless
    def fresh_reply_limit(self):
        """How many replies are considered fresh?
        0 means "infinite"
        """
        if not self.request.get("all_comments"):
            return 3
        return 0

    @property
    @memoize
    def commentable(self):
        """
        Check whether the viewing user has the right to comment
        by resolving the containing workspace IMicroblogContext
        (falling back to None=ISiteRoot)
        """
        add = "Plone Social: Add Microblog Status Update"
        try:
            return api.user.has_permission(add, obj=self.context.microblog_context)
        except api.exc.UserNotFoundError:
            logger.error("UserNotFoundError while rendering a statusupdate.")
            return False

    @property
    @memoize
    def portal(self):
        """Return the portal object"""
        return api.portal.get()

    @property
    @memoize
    def portal_url(self):
        """Return the portal object url"""
        return self.portal.absolute_url()

    @property
    @memoize
    def context_url(self):
        """Return the context url"""
        return self.portal_url

    @property
    @memoize
    def toggle_like(self):
        """This is used to render the toggle like stuff"""
        toggle_like_base = api.content.get_view(
            "toggle_like_statusupdate", self.portal, self.request
        )
        toggle_like_view = toggle_like_base.publishTraverse(
            self.request, self.context.getId()
        )
        return toggle_like_view

    @property
    @memoize_contextless
    def pas_view(self):
        return api.content.get_view("pas_view", api.portal.get(), self.request)

    @property
    @memoize
    def decorated_text(self):
        """Use this method to enrich the status update text

        For example we can:
         - replace \n with <br />
         - add links
         - add mentions
         - add tags
        """
        text = ""
        if self.context.text:
            text = self.context.text.replace("\n", "<br />")
        text = link_urls(text)
        tags = getattr(self.context, "tags", None)
        mentions = getattr(self.context, "mentions", None)
        text = link_users(text, self.portal_url, mentions, self.pas_view)
        text = link_tags(text, self.portal_url, tags)
        return text

    @property
    @memoize
    def user(self):
        return pi_api.userprofile.get(self.context.userid) or api.user.get(
            self.context.userid
        )

    @property
    def fullname(self):
        fullname = ""
        if self.user:
            fullname = getattr(self.user, "fullname", self.user.getProperty("fullname"))
        fullname = fullname or self.context.userid
        if not self.is_user_enabled:
            fullname = api.portal.translate(
                _(
                    "fullname_user_inactive",
                    default="$fullname (inactive)",
                    mapping={"fullname": fullname},
                )
            )
        return fullname

    @property
    def fullname_title(self):
        if not self.is_user_enabled:
            return api.portal.translate(
                _("title_user_inactive", default="This user is no longer active.")
            )

    @property
    def is_user_enabled(self):
        try:
            return api.content.get_state(obj=self.user) == "enabled"
        except WorkflowException:
            # Mostly in tests, there may not be a workflow for profiles
            return True
        except api.exc.MissingParameterError:
            # Somebody deleted the user
            return False

    @property
    @memoize
    def attachment_base_url(self):
        """This will return the base_url for making attachments"""
        base_url = "{portal_url}/@@status-attachments/{thread_id}/{status_id}".format(
            portal_url=self.portal_url,
            thread_id=self.context.thread_id or 0,
            status_id=self.context.getId(),
        )
        return base_url

    def item2attachments(self, item):
        """Take the attachment storage item
        and transform it into an attachment
        """
        item_url = "/".join((self.attachment_base_url, item.getId()))
        is_image = False
        title = item.id
        if isinstance(item, Image):
            is_image = True
            if item.content_type() == "image/svg+xml":
                url = item_url
            else:
                url = "/".join((item_url, "@@images", "3col"))
            title = getattr(item.image, "filename", title)
        elif pi_api.previews.has_previews(item):
            url = "/".join((item_url, "@@render.svg"))
        else:
            url = None

        if isinstance(item, File):
            title = getattr(item.file, "filename", title)

        return {
            "is_image": is_image,
            "img_src": url,
            "link": item_url,
            "alt": item.id,
            "title": title,
        }

    @memoize
    def attachments(self):
        """Get preview images for status update attachments"""
        storage = IAttachmentStorage(self.context, {})
        return list(map(self.item2attachments, storage.values()))

    @property
    def image_attachments(self):
        return [item for item in self.attachments() if item["is_image"]]

    @property
    def file_attachments(self):
        return [item for item in self.attachments() if not item["is_image"]]

    @property
    def label_image_preview(self):
        num = len(self.image_attachments)
        if num == 1 or self.is_content_image_update:
            return api.portal.translate(_("Preview this image"))
        return api.portal.translate(
            _(
                "label_preview_images",
                default="Preview ${num} images",
                mapping={"num": num},
            )
        )

    @property
    @memoize
    def replies(self):
        """Get the replies for this statusupdate"""
        replies = list(self.context.replies())
        replies.reverse()
        return replies

    @property
    def has_older_replies(self):
        """Check if we have oilder replies that we may want to hide"""
        if not self.fresh_reply_limit:
            return False
        return len(self.replies) > self.fresh_reply_limit

    @memoize
    def comment_views(self):
        """Return the html views of the replies to this comment"""
        replies_rendered = [
            api.content.get_view("comment.html", reply, self.request)
            for reply in self.replies[-self.fresh_reply_limit :]
        ]
        return replies_rendered

    @memoize
    def sub_comment_views(self, obj):
        """Return the html views of the replies to this comment"""
        replies = list(obj.get_comments())
        replies_rendered = [
            api.content.get_view("subcomment", reply, self.request) for reply in replies
        ]
        return replies_rendered

    def use_relative_date(self):
        registry = queryUtility(IRegistry)
        absolute_after = registry.get(
            "ploneintranet.activitystream.absolute_dates_after", 0
        )
        if absolute_after == 0:
            return True
        # keep relative until {absolute_after} days
        return DateTime.DateTime() - self.context.date <= absolute_after

    # ----------- actions (edit, delete) ----------------
    # actual write/delete handling done in subclass below

    @property
    def traverse(self):
        """Base URL for traversal views"""
        microblog = pi_api.microblog.get_microblog()
        if self.context.id in microblog._status_mapping:
            return f"{self.portal_url}/@@statusupdate/{self.context.id}"
        return f"{self.portal_url}/@@statusupdate/{self.context.thread_id}/{self.context.id}"  # noqa: E501

    @property
    def actions(self):
        actions = []
        if self.context.can_delete:
            if self.context.thread_id:
                title = _("Delete comment")
            else:
                title = _("Delete post")
            actions.append(
                {
                    "icon": "trash",
                    "title": title,
                    "data_pat_modal": "class: panel medium",
                    "url": self.traverse + "/panel-delete-post.html",
                }
            )
        if self.context.can_edit:
            if self.context.thread_id:
                title = _("Edit comment")
            else:
                title = _("Edit post")
            actions.append(
                {
                    "icon": "edit",
                    "title": title,
                    "url": self.traverse + "/panel-edit-post.html",
                    "data_pat_modal": "class: panel medium",
                }
            )

        # edit_tags not implemented yet
        # edit_mentions not implemented yet
        return actions

    # ----------- content updates only ------------------

    @property
    @memoize
    def content_context(self):
        return self.context.content_context

    @property
    @memoize
    def is_content_update(self):
        return self.content_context is not None

    @property
    def omit_comment_trail(self):
        return IOmittedCommentTrail.providedBy(self.content_context)

    @property
    def omit_initial_status_update(self):
        return ISimplifiedCommentTrail.providedBy(self.content_context)

    @property
    @memoize
    def is_content_image_update(self):
        return self.is_content_update and isinstance(self.content_context, Image)

    @property
    @memoize
    def is_content_file_update(self):
        return self.is_content_update and isinstance(self.content_context, File)

    @property
    @memoize
    def is_content_news_update(self):
        return self.is_content_update and isinstance(self.content_context, NewsItem)

    @property
    def is_content_downloadable(self):
        return self.is_content_image_update or self.is_content_file_update

    @property
    def content_has_leadimage(self):
        return self.is_content_update and ILeadImage.providedBy(self.content_context)

    def content_has_previews(self):
        if not self.is_content_update:
            return False
        if self.is_content_image_update:
            return True
        return pi_api.previews.has_previews(self.content_context)

    @property
    @memoize
    def context_content_type(self):
        if not self.is_content_update:
            return ""
        return self.content_context.content_type() or ""

    @property
    @memoize
    def post_type(self):
        """We have two types of post:

        1. `stream` (The one added with the post box)
        2. `content_update` (The one referring to a content object)
        """
        if self.is_content_update:
            return "content_update"
        return "stream"

    @property
    @memoize
    def content_update_type(self):
        """This property is used to render different previews based on the
        content itself
        """
        if self.is_content_image_update:
            return "type-image"
        if self.is_content_news_update:
            return "type-news"
        content_type = self.context_content_type
        if content_type.startswith("audio/"):
            return "type-audio"
        if content_type.startswith("video/"):
            return "type-video"
        if content_type == "text/x-uri-video":
            return "type-video"
        if content_type == "text/x-uri-youtube":
            return "type-youtube"
        return "document"

    def content_preview_status_css(self):
        if not self.is_content_update:
            return "fixme"
        if self.content_update_type in (
            "type-image",
            "type-audio",
            "type-video",
            "type-youtube",
            "type-news",
        ):
            return f"{self.content_update_type} document-preview"

        base = "document document-preview"
        if self.is_content_image_update:
            return base
        if pi_api.previews.converting(
            self.content_context
        ) and pi_api.previews.is_allowed_document_type(self.content_context):
            return base + " not-generated"
        if not self.content_has_previews():
            return base + " not-possible"
        return base

    def content_preview_urls(self, small=True):
        if not self.is_content_update:
            return []
        if self.is_content_image_update:
            return [self.content_context.absolute_url()]
        return pi_api.previews.get_preview_urls(self.content_context, small=small)

    def content_url(self):
        if self.is_content_image_update or self.is_content_file_update:
            return f"{self.content_context.absolute_url()}/view"
        if self.is_content_update:
            return self.content_context.absolute_url()

    def get_video_source(self, obj):
        """
        Try to guess a URL that will allow to stream something from a video element
        """
        if obj.portal_type == "Link":
            return obj.remoteUrl
        if obj.portal_type == "File":
            return f"{obj.absolute_url()}/@@download/file/{obj.file.filename}"
        return ""

    def get_video_poster(self, obj):
        """Try to guess a URL that will allow to serve a poster image
        for a video element
        """
        if obj.portal_type == "Link":
            if getattr(obj, "image", None):
                return f"{obj.absolute_url()}/@@images/image/3col"
        elif obj.portal_type == "File":
            try:
                poster_view = api.content.get_view("poster.jpg", obj, self.request)
            except api.exc.InvalidParameterError:
                poster_view = None
            if poster_view and poster_view.image:
                return f"{obj.absolute_url()}/@@poster.jpg"

        return ""

    @property
    def injection_target(self):
        if self.in_workspace:
            return (
                "source: #application-body::element; target: #application-body::element"
            )
        if self.content_update_type in ("type-news",):
            return "source: #main; target: #main"
        return "source: #app-space; target: #app-space"

    @property
    def switch_target(self):
        if self.content_update_type in ("type-news",):
            return
        return "#app-space state-off state-on"


class StatusUpdateEditPanel(StatusUpdateView):
    """Render the edit panel for posts or comments"""

    @property
    @memoize
    def title(self):
        if self.context.thread_id:
            return _("Edit comment")
        return _("Edit post")

    @property
    @memoize
    def form_action(self):
        if self.context.thread_id:
            return self.traverse + "/comment-edited.html"
        return self.traverse + "/post-edited.html"

    @property
    @memoize
    def data_pat_inject(self):
        if self.context.thread_id:
            return f"#comment-{self.context.id}"
        return f"#post-{self.context.id}"


@implementer(IDiazoNoTemplate)
class StatusUpdateDeletePanel(StatusUpdateView):
    """Render the delete panel for posts or comments"""

    @property
    @memoize
    def title(self):
        if self.context.thread_id:
            return _("Delete comment")
        return _("Delete post")

    @property
    @memoize
    def description(self):
        if self.context.thread_id:
            return _("You are about to delete this comment. Are you sure?")
        return _("You are about to delete this post. Are you sure?")

    @property
    @memoize
    def form_action(self):
        if self.context.thread_id:
            return self.traverse + "/comment-deleted.html"
        return self.traverse + "/post-deleted.html"

    @property
    @memoize
    def data_pat_inject(self):
        if self.context.thread_id:
            return f"#document-content #comment-{self.context.id}"
        return f"#document-content #post-{self.context.id}"


class StatusUpdateModify(StatusUpdateView):
    """
    A shared view class for editing and deleting statusupdates.
    """

    def __call__(self):
        if self.request.method == "POST":
            self.handle_action()
        return super().__call__()

    def handle_action(self):
        """
        Handle edit/delete actions. Security is checked in backend.
        Takes care to handle any HTTP POST only once, even with
        a cloned request.
        """
        # pop() removes id to avoid multi-handling cloned POST request
        id = self.request.form.pop("statusupdate_id", None)
        if not id:
            return
        try:
            statusupdate = pi_api.microblog.statusupdate.get(id)
        except KeyError:
            # Try to see if this is a comment reply
            statusupdate = pi_api.microblog.statusupdate.get(
                self.context.thread_id
            ).comments[self.context.id]

        if self.request.form.get("delete", False):
            statusupdate.delete()
        elif self.request.form.get("text", None):
            statusupdate.edit(self.request.form.get("text"))
