from AccessControl import getSecurityManager
from Acquisition import aq_inner
from DateTime import DateTime
from plone.app.content.browser.interfaces import IFolderContentsView
from plone.app.layout.globals.interfaces import IViewView
from plone.app.layout.viewlets import ViewletBase
from plone.app.multilingual.browser.vocabularies import translated_languages
from plone.app.multilingual.interfaces import ITranslatable
from plone.app.multilingual.interfaces import ITranslationManager
from plone.base.interfaces import ISecuritySchema
from plone.base.interfaces import ISiteSchema
from plone.base.utils import base_hasattr
from plone.memoize.instance import memoize
from plone.memoize.view import memoize_contextless
from plone.protect.authenticator import createToken
from plone.registry.interfaces import IRegistry
from Products.CMFCore.utils import _checkPermission
from Products.CMFCore.utils import getToolByName
from Products.CMFCore.WorkflowCore import WorkflowException
from Products.CMFEditions.Permissions import AccessPreviousVersions
from Products.CMFPlone import PloneMessageFactory as _
from Products.CMFPlone.utils import log
from Products.Five.browser import BrowserView
from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
from urllib.parse import urlencode
from zope.component import getMultiAdapter
from zope.component import getUtility
from zope.component import queryMultiAdapter
from zope.deprecation import deprecation

import logging
import pkg_resources


try:
    pkg_resources.get_distribution("plone.app.relationfield")
except pkg_resources.DistributionNotFound:
    HAS_RELATIONFIELD = False
else:
    from plone.app.relationfield.behavior import IRelatedItems

    HAS_RELATIONFIELD = True

# XXX needs refactoring, since Plone 5 we have PAM in core.
HAS_PAM = True


class DocumentActionsViewlet(ViewletBase):

    index = ViewPageTemplateFile("document_actions.pt")

    def update(self):
        super().update()

        self.context_state = getMultiAdapter(
            (self.context, self.request), name="plone_context_state"
        )
        self.actions = self.context_state.actions("document_actions")


class DocumentBylineViewlet(ViewletBase):

    index = ViewPageTemplateFile("document_byline.pt")

    def update(self):
        super().update()
        self.anonymous = self.portal_state.anonymous()

    @property
    @deprecation.deprecate("The context_state property is unused and will be removed in Plone 7")
    def context_state(self):
        return getMultiAdapter((self.context, self.request), name="plone_context_state")

    @property
    @deprecation.deprecate("The has_pam property is unused and will be removed in Plone 7")
    def has_pam(self):
        return HAS_PAM

    @property
    @memoize_contextless
    def portal_membership(self):
        return getToolByName(self.context, "portal_membership")

    def show(self):
        registry = getUtility(IRegistry)
        settings = registry.forInterface(
            ISiteSchema,
            prefix="plone",
        )
        return not self.anonymous or settings.display_publication_date_in_byline

    def show_about(self):
        registry = getUtility(IRegistry)
        settings = registry.forInterface(
            ISecuritySchema,
            prefix="plone",
        )
        return not self.anonymous or settings.allow_anon_views_about

    @deprecation.deprecate("The creator method is unused and will be removed in Plone 7")
    def creator(self):
        return self.context.Creator()

    @deprecation.deprecate("The author method is unused and will be removed in Plone 7")
    def author(self):
        membership = getToolByName(self.context, "portal_membership")
        return membership.getMemberInfo(self.creator())

    @deprecation.deprecate("The authorname method is unused and will be removed in Plone 7")
    def authorname(self):
        author = self.author()
        return author and author["fullname"] or self.creator()

    @memoize
    def get_member_info(self, user_id):
        return self.portal_membership.getMemberInfo(user_id)

    def get_url_path(self, user_id):
        if self.get_member_info(user_id) is None:
            return ""
        if "/" in user_id:
            qs = urlencode({"author": user_id})
            return f"author/?{qs}"
        return f"author/{user_id}"

    def get_fullname(self, user_id):
        info = self.get_member_info(user_id)
        if info is None:
            return user_id
        return info.get("fullname") or user_id

    def show_modification_date(self):
        return not self.context.effective_date or (
            self.context.effective_date.Date() < self.context.modification_date.Date()
        )

    def isExpired(self):
        if base_hasattr(self.context, "expires"):
            return self.context.expires().isPast()
        return False

    @deprecation.deprecate("The toLocalizedTime method is unused and will be removed in Plone 7")
    def toLocalizedTime(self, time, long_format=None, time_only=None):
        """Convert time to localized time"""
        util = getToolByName(self.context, "translation_service")
        return util.ulocalized_time(
            time, long_format, time_only, self.context, domain="plonelocales"
        )

    def pub_date(self):
        """Return object effective date.

        Return None if publication date is switched off in global site settings
        or if Effective Date is not set on object.
        """
        # check if we are allowed to display publication date
        registry = getUtility(IRegistry)
        settings = registry.forInterface(ISiteSchema, prefix="plone")

        if not settings.display_publication_date_in_byline:
            return None

        # check if we have Effective Date set
        date = self.context.EffectiveDate()
        if not date or date == "None":
            return None

        return DateTime(date)

    @deprecation.deprecate("The get_translations method is unused and will be removed in Plone 7")
    def get_translations(self):
        cts = []
        if ITranslatable.providedBy(self.context):
            t_langs = translated_languages(self.context)
            context_translations = ITranslationManager(self.context).get_translations()
            for lang in t_langs:
                cts.append(
                    dict(
                        lang_native=lang.title,
                        url=context_translations[lang.value].absolute_url(),
                    )
                )
        return cts


class HistoryByLineView(BrowserView):
    """DocumentByLine information for content history view"""

    index = ViewPageTemplateFile("history_view.pt")

    def update(self):
        context = self.context
        self.portal_state = getMultiAdapter(
            (context, self.request), name="plone_portal_state"
        )
        self.context_state = getMultiAdapter(
            (self.context, self.request), name="plone_context_state"
        )
        self.anonymous = self.portal_state.anonymous()
        self.has_pam = HAS_PAM

    def __call__(self):
        self.update()

        return self.index()

    def show(self):
        registry = getUtility(IRegistry)
        settings = registry.forInterface(
            ISecuritySchema,
            prefix="plone",
        )
        return not self.anonymous or settings.allow_anon_views_about

    def show_history(self):
        has_access_preview_versions_permission = _checkPermission(
            "CMFEditions: Access previous versions", self.context
        )
        if not has_access_preview_versions_permission:
            return False
        if IViewView.providedBy(self.__parent__):
            return True
        if IFolderContentsView.providedBy(self.__parent__):
            return True
        return False

    def locked_icon(self):
        if not getSecurityManager().checkPermission(
            "Modify portal content", self.context
        ):
            return ""

        locked = False
        lock_info = queryMultiAdapter(
            (self.context, self.request), name="plone_lock_info"
        )
        if lock_info is not None:
            locked = lock_info.is_locked()
        else:
            context = aq_inner(self.context)
            lockable = getattr(context.aq_explicit, "wl_isLocked", None) is not None
            locked = lockable and context.wl_isLocked()

        if not locked:
            return ""

        portal = self.portal_state.portal()
        icon = portal.restrictedTraverse("lock_icon.png")
        return icon.tag(title="Locked")

    def creator(self):
        return self.context.Creator()

    def author(self):
        membership = getToolByName(self.context, "portal_membership")
        return membership.getMemberInfo(self.creator())

    def authorname(self):
        author = self.author()
        return author and author["fullname"] or self.creator()

    def isExpired(self):
        if base_hasattr(self.context, "expires"):
            return self.context.expires().isPast()
        return False

    def toLocalizedTime(self, time, long_format=None, time_only=None):
        """Convert time to localized time"""
        util = getToolByName(self.context, "translation_service")
        return util.ulocalized_time(
            time, long_format, time_only, self.context, domain="plonelocales"
        )

    def pub_date(self):
        """Return object effective date.

        Return None if publication date is switched off in global site settings
        or if Effective Date is not set on object.
        """
        # check if we are allowed to display publication date
        registry = getUtility(IRegistry)
        settings = registry.forInterface(ISiteSchema, prefix="plone")

        if not settings.display_publication_date_in_byline:
            return None

        # check if we have Effective Date set
        date = self.context.EffectiveDate()
        if not date or date == "None":
            return None

        return DateTime(date)

    def get_translations(self):
        cts = []
        if ITranslatable.providedBy(self.context):
            t_langs = translated_languages(self.context)
            context_translations = ITranslationManager(self.context).get_translations()
            for lang in t_langs:
                cts.append(
                    dict(
                        lang_native=lang.title,
                        url=context_translations[lang.value].absolute_url(),
                    )
                )

        return cts


class ContentRelatedItems(ViewletBase):

    index = ViewPageTemplateFile("document_relateditems.pt")

    def related_items(self):
        context = aq_inner(self.context)
        res = ()

        # Archetypes
        if base_hasattr(context, "getRawRelatedItems"):
            catalog = getToolByName(context, "portal_catalog")
            related = context.getRawRelatedItems()
            if not related:
                return ()
            brains = catalog(UID=related)
            if brains:
                # build a position dict by iterating over the items once
                positions = {v: i for (i, v) in enumerate(related)}
                # We need to keep the ordering intact
                res = list(brains)

                def _key(brain):
                    return positions.get(brain.UID, -1)

                res.sort(key=_key)

        # Dexterity
        if HAS_RELATIONFIELD and IRelatedItems.providedBy(context):
            related = context.relatedItems
            if not related:
                return ()
            res = self.related2brains(related)

        return res

    def related2brains(self, related):
        """Return a list of brains based on a list of relations. Will filter
        relations if the user has no permission to access the content.

        :param related: related items
        :type related: list of relations
        :return: list of catalog brains
        """
        catalog = getToolByName(self.context, "portal_catalog")
        brains = []
        for r in related:
            path = r.to_path
            if path is None:
                # Item was deleted.  The related item should have been cleaned
                # up, but apparently this does not happen.
                continue
            # the query will return an empty list if the user
            # has no permission to see the target object
            brains.extend(catalog(path=dict(query=path, depth=0)))
        return brains


class WorkflowHistoryViewlet(ViewletBase):

    index = ViewPageTemplateFile("review_history.pt")

    @memoize
    def getUserInfo(self, userid):
        actor = dict(fullname=userid)
        mt = getToolByName(self.context, "portal_membership")
        info = mt.getMemberInfo(userid)
        if info is None:
            return dict(actor_home="", actor=actor)

        fullname = info.get("fullname", None)
        if fullname:
            actor["fullname"] = fullname

        return dict(actor=actor, actor_home=f"{self.site_url}/author/{userid}")

    def workflowHistory(self, complete=True):
        """Return workflow history of this context.

        Taken from plone_scripts/getWorkflowHistory.py
        """
        context = aq_inner(self.context)
        # check if the current user has the proper permissions
        if not (
            _checkPermission("Request review", context)
            or _checkPermission("Review portal content", context)
        ):
            return []

        workflow = getToolByName(context, "portal_workflow")
        review_history = []

        try:
            # get total history
            review_history = workflow.getInfoFor(context, "review_history")

            if not complete:
                # filter out automatic transitions.
                review_history = [r for r in review_history if r["action"]]
            else:
                review_history = list(review_history)

            portal_type = context.portal_type
            anon = _("label_anonymous_user", default="Anonymous User")

            for r in review_history:
                r["type"] = "workflow"
                r["transition_title"] = workflow.getTitleForTransitionOnType(
                    r["action"], portal_type
                ) or _("Create")
                r["state_title"] = workflow.getTitleForStateOnType(
                    r["review_state"], portal_type
                )
                actorid = r["actor"]
                r["actorid"] = actorid
                if actorid is None:
                    # action performed by an anonymous user
                    r["actor"] = {"username": anon, "fullname": anon}
                    r["actor_home"] = ""
                else:
                    r.update(self.getUserInfo(actorid))
            review_history.reverse()

        except WorkflowException:
            log(
                "plone.app.layout.viewlets.content: "
                "%s has no associated workflow" % context.absolute_url(),
                severity=logging.DEBUG,
            )

        return review_history


class ContentHistoryViewlet(WorkflowHistoryViewlet):

    index = ViewPageTemplateFile("content_history.pt")

    def revisionHistory(self):
        context = aq_inner(self.context)
        if not _checkPermission(AccessPreviousVersions, context):
            return []

        rt = getToolByName(context, "portal_repository", None)
        if rt is None or not rt.isVersionable(context):
            return []

        context_url = context.absolute_url()
        history = rt.getHistoryMetadata(context)
        portal_diff = getToolByName(context, "portal_diff", None)
        can_diff = (
            portal_diff is not None
            and len(portal_diff.getDiffForPortalType(context.portal_type)) > 0
        )
        can_revert = _checkPermission(
            "CMFEditions: Revert to previous versions", context
        )

        def morphVersionDataToHistoryFormat(vdata, version_id):
            meta = vdata["metadata"]["sys_metadata"]
            userid = meta["principal"]
            token = createToken()
            preview_url = (
                "%s/versions_history_form?version_id=%s&_authenticator=%s#version_preview"
                % (context_url, version_id, token)  # noqa
            )
            info = dict(
                type="versioning",
                action=_("Edited"),
                transition_title=_("Edited"),
                actorid=userid,
                time=meta["timestamp"],
                comments=meta["comment"],
                version_id=version_id,
                preview_url=preview_url,
            )
            if can_diff:
                if version_id > 0:
                    info[
                        "diff_previous_url"
                    ] = "{}/@@history?one={}&two={}&_authenticator={}".format(
                        context_url,
                        version_id,
                        version_id - 1,
                        token,
                    )
                if not rt.isUpToDate(context, version_id):
                    info[
                        "diff_current_url"
                    ] = "{}/@@history?one=current&two={}&_authenticator={}".format(
                        context_url,
                        version_id,
                        token,
                    )
            if can_revert:
                info["revert_url"] = "%s/revertversion" % context_url
            else:
                info["revert_url"] = None
            info.update(self.getUserInfo(userid))
            return info

        # History may be an empty list
        if not history:
            return history

        version_history = []
        retrieve = history.retrieve
        getId = history.getVersionId
        # Count backwards from most recent to least recent
        for i in range(history.getLength(countPurged=False) - 1, -1, -1):
            version_history.append(
                morphVersionDataToHistoryFormat(
                    retrieve(i, countPurged=False), getId(i, countPurged=False)
                )
            )

        return version_history

    def fullHistory(self):
        history = self.workflowHistory() + self.revisionHistory()
        if len(history) == 0:
            return None
        history.sort(key=lambda x: x["time"], reverse=True)
        return history

    def toLocalizedTime(self, time, long_format=None, time_only=None):
        """Convert time to localized time"""
        util = getToolByName(self.context, "translation_service")
        return util.ulocalized_time(
            time, long_format, time_only, self.context, domain="plonelocales"
        )


class ContentHistoryView(ContentHistoryViewlet):
    def __init__(self, context, request):
        super().__init__(context, request, None, None)
        self.update()

    def __call__(self):
        return self.index()
