from .utils import dexterity_update
from Acquisition import aq_inner
from Acquisition import aq_parent
from json import dumps
from logging import getLogger
from plone import api
from plone.app.blocks.interfaces import IBlocksTransformEnabled
from plone.app.event.base import default_timezone
from plone.app.iterate.interfaces import IBaseline
from plone.app.iterate.interfaces import IWorkingCopy
from plone.base.interfaces import INonStructuralFolder
from plone.memoize.view import memoize
from plone.memoize.view import memoize_contextless
from ploneintranet import api as pi_api
from ploneintranet.core import _
from ploneintranet.layout.browser.download_pdf import DownloadAsMixin
from ploneintranet.layout.interfaces import IDiazoAppTemplate
from ploneintranet.library.behaviors.publish import IPublishWidely
from ploneintranet.workspace.behaviors.file import IFileField
from ploneintranet.workspace.interfaces import ITrashed
from ploneintranet.workspace.utils import map_content_type
from ploneintranet.workspace.utils import parent_app
from ploneintranet.workspace.utils import parent_workspace
from Products.CMFEditions.interfaces.IModifier import FileTooLargeToVersionError
from Products.CMFEditions.utilities import isObjectChanged
from Products.CMFEditions.utilities import isObjectVersioned
from Products.CMFEditions.utilities import maybeSaveVersion
from Products.Five import BrowserView
from time import time
from z3c.relationfield import RelationValue
from zope.annotation import IAnnotations
from zope.component import getUtility
from zope.event import notify
from zope.interface import implementer
from zope.intid.interfaces import IIntIds
from zope.lifecycleevent import Attributes
from zope.lifecycleevent import ObjectModifiedEvent
from zope.schema.interfaces import IVocabularyFactory

logger = getLogger(__name__)


@implementer(IBlocksTransformEnabled, IDiazoAppTemplate)
class ContentView(BrowserView, DownloadAsMixin):
    """View and edit class/form for all default DX content-types."""

    suppress_messages = False

    _delete_permission = "Delete objects"
    _move_permission = "Copy or Move"
    _edit_permission = "Modify portal content"
    _untrash_permission = "Delete objects"

    @property
    @memoize
    def user(self):
        """The currently authenticated ploneintranet user profile (if any)"""
        return pi_api.userprofile.get_current()

    @property
    @memoize
    def has_permission(self):
        """Return the has_permission method of the authenticated user"""
        return api.user.get_current().has_permission

    @property
    @memoize
    def is_ajax(self):
        """Check if we have an ajax call"""
        requested_with = self.request.environ.get("HTTP_X_REQUESTED_WITH")
        return requested_with == "XMLHttpRequest"

    @property
    def allow_external_editor(self):
        """Check if external editor should be available on this content type."""
        mimetype = self._download_content_type
        return "audio" not in mimetype and "video" not in mimetype

    @property
    @memoize
    def show_sidebar(self):
        """Should we show the sidebar?"""
        form = self.request.form
        if "show_sidebar" in form:
            return True
        if "hide_sidebar" in form:
            return False
        if self.is_ajax:
            return False
        return True

    def make_sidebar_items(self):
        """Make the equivalent of the sidebar items"""
        # First we pretend to mimic a sidebar result
        sidebar = api.content.get_view("sidebar.documents", self.context, self.request)
        pc = api.portal.get_tool(name="portal_catalog")
        brains = pc(UID=self.context.UID())
        results = sidebar._extract_attrs(brains)
        return list(map(sidebar.result2item, results))

    @property
    @memoize_contextless
    def include_clicktracker(self):
        """Is inclusion of slcclicktracker element enabled in the registry?"""
        return api.content.get_view(
            "view", self.workspace, self.request
        ).include_clicktracker

    @property
    @memoize
    def autosave_enabled(self):
        """Look up the registry to check if autosave should be enabled
        for this portal_type
        """
        autosave_portal_types = (
            api.portal.get_registry_record(
                "ploneintranet.workspace.autosave_portal_types", default=[]
            )
            or ()
        )
        return self.context.portal_type in autosave_portal_types

    @property
    @memoize
    def autosave_delay(self):
        """The delay before triggering the autosave injection"""
        if self.autosave_enabled:
            return api.portal.get_registry_record(
                "ploneintranet.workspace.autosave_delay", default=2000
            )

    @property
    @memoize
    def workspace(self):
        """Return the parent workspace (if there is any)"""
        return parent_workspace(self.context)

    @property
    @memoize
    def can_cut(self):
        """Check if we can cut this document"""
        return self.has_permission(
            self._move_permission, self.context
        ) and self.has_permission(self._delete_permission, aq_parent(self.context))

    @memoize
    def is_baseline(self):
        return IBaseline.providedBy(self.context)

    @memoize
    def is_working_copy(self):
        return IWorkingCopy.providedBy(self.context)

    @property
    @memoize
    def can_delete(self):
        """Check if we can delete this document"""
        return self.has_permission(self._delete_permission, self.context)

    @property
    @memoize
    def can_edit(self):
        """Check if we have edit rights on this document"""
        if self.is_baseline():
            return False
        return self.has_permission(self._edit_permission, self.context)

    @property
    @memoize
    def can_untrash(self):
        """Check if we have untrash rights on this document"""
        return self.has_permission(self._untrash_permission, self.context)

    @property
    @memoize
    def is_trashed(self):
        """Check if the current document is in the trash can"""
        return ITrashed.providedBy(self.context)

    @property
    @memoize
    def trash_anchor_label(self):
        """"""
        if self.can_untrash:
            return _("Untrash")
        return _("Trashed")

    @property
    @memoize
    def read_only(self):
        return self.is_trashed or not self.can_edit

    def should_update(self):
        """Check if we should call the update method
        before rendering the template
        """
        return self.request.method == "POST"

    def __call__(self, title=None, description=None, tags=[], text=None):
        """Render the default template and evaluate the form when editing."""
        context = aq_inner(self.context)
        parent = aq_parent(context)
        if INonStructuralFolder.providedBy(parent):
            return self.request.response.redirect(parent.absolute_url())
        # When saving, force to POST
        if self.should_update():
            self.update()
        return super().__call__()

    def validate(self):
        """Return truish if valid"""
        return True

    @property
    @memoize
    def lock_info(self):
        """Return the lock information about this user"""
        view = api.content.get_view("toggle-lock", self.context, self.request)
        return view.lock_info

    @property
    @memoize
    def is_locked(self):
        """If the document is locked by another user do not update it"""
        info = self.lock_info
        if not info:
            return False
        user = self.user
        if not user or user.username == info.get("creator"):
            return False
        return True

    def get_lock_user_fullname(self):
        """Get the fullname of the user who has the lock"""
        info = self.lock_info or {}
        creator = info.get("creator")
        if not creator:
            return _("Anonymous User")
        user = pi_api.userprofile.get(creator)
        if not user:
            return creator
        return user.fullname or user.getId()

    @property
    @memoize
    def dx_modified(self):
        """Proxy the method from workflow_menu"""
        view = api.content.get_view("workflow_menu", self.context, self.request)
        return view.dx_modified

    def reset_dx_modified(self):
        """Reset the dx_modified attribute on the object and then refresh the
        memoize_cache
        """
        self.context.dx_modified = str(int(time()))
        self.purge_memoize()

    @property
    @memoize
    def is_old_version(self):
        """Prevent overwriting changes by checking
        that the current object modification time is equal to the one
        known when the document form was rendered
        """
        modified = self.dx_modified
        if not modified:
            return False
        if modified != self.context.dx_modified:
            return True
        self.request.form.pop("dx_modified", None)
        return False

    @property
    @memoize
    def is_audiotype(self):
        return IFileField(self.context, False) and IFileField(self.context).is_audiotype

    @property
    @memoize
    def pdf_view(self):
        try:
            return api.content.get_view("pdf", self.context, self.request)
        except api.exc.InvalidParameterError:
            return None

    @property
    @memoize
    def has_pdf(self):
        return self.pdf_view and self.pdf_view.has_pdf()

    @property
    @memoize
    def can_save(self):
        return self.context.portal_type in ("Document",) and not self.read_only

    @property
    def can_share_download(self):
        return self.context.portal_type in ("File", "Image")

    @property
    @memoize
    def can_share_pdf(self):
        if self.content_type_name == "PDF":
            return False
        return self.has_pdf

    @property
    @memoize
    def can_share_print(self):
        if self.request.get("fcm"):
            return False
        return self.content_type_name == "Image" or self.has_pdf

    @property
    @memoize
    def can_share(self):
        return (
            self.can_share_download
            or self.can_share_pdf
            or self.can_share_print
            or self.can_publish_widely()
        )

    @property
    @memoize
    def print_url(self):
        if not self.can_share_print or not self.pdf_view:
            return ""
        if self.can_share_pdf:
            return (
                f"{self.context.absolute_url()}"
                f"/@@pdf?for_print=1&amp;t={self.pdf_view.timestamp}"
            )
        return f"{self.context.absolute_url()}?for_print=1"

    def purge_memoize(self):
        """Purge the memoize cache, stored in the annotations.
        This is useful, e.g., after workflow state changes,
        to recalculate the can_* methods with the new security settings.
        """
        IAnnotations(self.request).pop("plone.memoize", None)

    def maybe_update_wf(self):
        """Check if we should update the workflow state.
        If yes update it and return a truish, otherwise return a falsish.
        """
        transition = self.request.get("workflow_action")
        if not transition:
            return
        # It may be that the transition is actually the current state
        # (see #1508)
        if transition == api.content.get_state(self.context):
            return
        # This is also tricky, we expect that if the whole form is submitted
        # no modification happened in the workflow subform
        if self.request.get("form.submitted"):
            return
        try:
            api.content.transition(obj=self.context, transition=transition)
        except api.exc.InvalidParameterError:
            logger.error(
                "Error updating the workflow state for %r, transition %r",
                self.context,
                transition,
            )
            api.portal.show_message(
                _("Error updating the workflow state"),
                request=self.request,
                type="error",
            )
            return
        self.reset_dx_modified()
        return True

    def maybe_add_related_item(self):
        uid = self.request.form.get("new-related-item")
        if not uid:
            return
        obj = api.content.get(UID=uid)
        if not obj:
            api.portal.show_message(
                _("Attachment cannot be added"), request=self.request, type="error"
            )
            return

        relatedItems = self.context.relatedItems
        for item in relatedItems:
            try:
                if item.to_object.UID() == uid:
                    return api.portal.show_message(
                        _("Attachment already present"),
                        request=self.request,
                        type="warning",
                    )
            except AttributeError:
                pass

        intids = getUtility(IIntIds)
        relation = RelationValue(intids.getId(obj))

        relatedItems.append(relation)
        if relatedItems != self.context.relatedItems:
            self.context.relatedItems = relatedItems

        api.portal.show_message(
            _("Attachment added"), request=self.request, type="success"
        )

    def maybe_remove_related_item(self):
        uid = self.request.form.get("obsolete-related-item")
        if not uid:
            return

        def _filter(relation):
            try:
                return relation.to_object.UID() != uid
            except AttributeError:
                pass

        new_relatedItems = list(filter(_filter, self.context.relatedItems))
        if new_relatedItems != self.context.relatedItems:
            self.context.relatedItems = new_relatedItems
            api.portal.show_message(
                _("Attachment removed"), request=self.request, type="success"
            )
        else:
            api.portal.show_message(
                _("No attachment was removed"), request=self.request, type="warning"
            )

    def update(self):
        """ """
        self.maybe_add_related_item()
        self.maybe_remove_related_item()
        context = aq_inner(self.context)
        fields_modified = {}
        errors = []
        messages = []
        if self.is_locked:
            api.portal.show_message(
                _(
                    "document_locked_by_error",
                    "The document is locked by ${user}.",
                    mapping={"user": self.get_lock_user_fullname()},
                ),
                self.request,
                "error",
            )
            return
        if self.is_old_version:
            msg = _(
                "document_modified_error",
                default=(
                    "It seems that this document was modified since "
                    "you loaded this form. "
                    "Your changes will be discarded. "
                    '<a href="${url}">'
                    "Please reload this page by clicking here</a>."
                ),
                mapping={"url": self.context.absolute_url()},
            )
            api.portal.show_message(msg, self.request, "error")
            return

        if self.maybe_update_wf():
            workflow_modified = True
            messages.append(
                api.portal.translate(_("The workflow state has been changed."))
            )
        else:
            workflow_modified = False

        # when creating the first version, autosave the previous state also
        versioning_errors = self.maybe_save_initial_version()
        errors.extend(versioning_errors)

        if self.can_edit:
            if self.validate():
                fields_modified, errors = dexterity_update(context, self.request)
                if fields_modified:
                    self.reset_dx_modified()
                    if not self.autosave_enabled:
                        messages.append(
                            api.portal.translate(_("Your changes have been saved."))
                        )

        versioning_errors = self.save_version()
        errors.extend(versioning_errors)

        if errors:
            for error in errors:
                api.portal.show_message(error, request=self.request, type="error")

        elif workflow_modified or fields_modified:
            if messages and not self.suppress_messages:
                api.portal.show_message(
                    " ".join(messages), request=self.request, type="success"
                )

            if fields_modified:
                descriptions = [
                    Attributes(interface, *fields)
                    for interface, fields in fields_modified.items()
                ]
                notify(ObjectModifiedEvent(context, *descriptions))
            else:
                context.reindexObject()

    @property
    def form_id(self):
        """The id to be used in the main form"""
        return self.context.UID()

    @property
    def sidebar_target(self):
        """When injecting the form we may want to reload some sidebar parts"""
        return f"item-{self.context.UID()}"

    def form_pat_inject_options(self):
        """Return the data-path-inject options we want to use"""
        parts = [
            "source: #{mainid}; target: #{mainid};",
            "source: #{sidebar_target}-replacement; target: #{sidebar_target}; loading-class: ''",  # noqa: E501
        ]
        if self.autosave_enabled:
            mainid = "saving-badge::element"
            parts.append("#workflow-state; loading-class: '';")
        else:
            mainid = "document-body"
        if self.copied_to_library_url:
            parts.append("source: #document-alert-bar; target: #document-alert-bar")
        parts.append(
            "source: #global-statusmessage; target: #global-statusmessage; "
            "loading-class: '';"
        )
        template = " && ".join(parts)
        return template.format(mainid=mainid, sidebar_target=self.sidebar_target)

    def get_preview_urls(self, obj):
        """Expose the homonymous pi_api method to get the list of preview_urls"""
        return pi_api.previews.get_preview_urls(obj)

    def image_url(self, obj=None):
        """The img-url used to construct the img-tag."""
        context = aq_inner(obj or self.context)
        if getattr(context, "image", None) is not None:
            return f"{context.absolute_url()}/@@images/image"

    def get_preview_or_image_urls(self, obj, scale="normal"):
        if obj.portal_type == "Image":
            image_url = self.image_url(obj=obj)
            if image_url:
                return [image_url]
        elif pi_api.previews.is_allowed_document_type(obj):
            return self.get_preview_urls(obj)
        return []

    def get_all_preview_or_image_urls(self, first_obj, all_objs, scale="normal"):
        """For email attachments we want to show *all* previews for
        *all* attachments in one go for *each* attachment, starting
        with the *current* attachment.
        """
        obj_seq = [first_obj] + [x for x in all_objs if x is not first_obj]
        previews = []
        for obj in obj_seq:
            previews.extend(self.get_preview_or_image_urls(obj, scale))
        return previews

    @property
    @memoize
    def friendly_type2type_class(self):
        """Reuse the method from the search view"""
        view = api.content.get_view("proto", api.portal.get(), self.request)
        return view.friendly_type2type_class

    def get_obj_icon_class(self, obj):
        """Return the best icon for this object"""
        if obj.portal_type == "Image":
            return "icon-file-image"
        if obj.portal_type == "File" and obj.file:
            content_type = obj.file.contentType
            if content_type == "message/rfc822":
                return "icon-mail"
            for word in (
                "pdf",
                "word",
                "excel",
                "powerpoint",
                "video",
                "audio",
                "archive",
                "image",
            ):
                if word in content_type:
                    return "icon-file-%s" % word
            return "icon-attach"

        icon_type = self.friendly_type2type_class(obj.portal_type)
        icon_file = icon_type.replace("type", "file")
        return "icon-%s" % icon_file

    def portal_type_to_class(self):
        pt = self.context.portal_type
        if pt == "File":
            if self.context.file is None:
                return "file"
            return map_content_type(self.context.file.contentType)
        elif pt == "Image":
            return "image"
        elif pt == "Link":
            return map_content_type(self.context.content_type())
        elif pt == "Event":
            return "event"
        elif pt == "Document":
            return "rich"
        elif pt == "Folder":
            return "folder"
        return "generic"

    def maybe_save_initial_version(self):
        """When creating the first version, save pre-modified state as version 0"""
        if isObjectVersioned(self.context):
            # we already have an version history, bail out
            return []
        comment = api.portal.translate(_("Autosave previous version"))
        return self.save_version(comment)

    def save_version(self, comment=""):
        errors = []
        comment = comment or self.request.get("cmfeditions_version_comment", "")
        save_new_version = self.request.get("cmfeditions_save_new_version", None)
        force = save_new_version is not None

        if not (isObjectChanged(self.context) or force):
            return errors

        try:
            maybeSaveVersion(self.context, comment=comment, force=force)
        except FileTooLargeToVersionError as e:
            errors.append(e)
        return errors

    def is_versionable(self):
        portal_repository = api.portal.get_tool("portal_repository")
        return portal_repository.isVersionable(self.context)

    @property
    @memoize
    def publish_adapter(self):
        try:
            return IPublishWidely(self.context)
        except TypeError:
            return None

    def can_publish_widely(self):
        adapted = self.publish_adapter
        return adapted and adapted.can_publish_widely()

    def can_update_widely(self):
        adapted = self.publish_adapter
        return adapted and adapted.can_update_widely()

    def can_retract_widely(self):
        adapted = self.publish_adapter
        return adapted and adapted.can_retract_widely()

    def has_updated_source(self):
        adapted = self.publish_adapter
        return adapted and adapted.has_updated_source()

    @property
    @memoize
    def copied_to_library_url(self):
        adapted = self.publish_adapter
        if not adapted:
            return False
        target = adapted.target()
        return target and target.absolute_url() or None


class ContainerView(ContentView):
    """For the container we always return the sidebar"""

    @property
    @memoize
    def show_sidebar(self):
        """Should we show the sidebar?"""
        form = self.request.form
        if "show_sidebar" in form:
            return True
        if "hide_sidebar" in form:
            return False
        return True


class HelperView(BrowserView):
    """Use this to provide helper methods"""

    def get_selected_tz(self):
        """Let's try to get this from the start attribute (if found).
        Otherwise default to the default timezone.
        """
        try:
            return str(self.context.start.tzinfo)
        except BaseException:
            return default_timezone()

    def get_tz_options(self):
        """Returns the timezone options to be used in a select"""
        selected_tz = self.get_selected_tz()
        plone_tzs = getUtility(
            IVocabularyFactory, "plone.app.vocabularies.CommonTimezones"
        )(self.context)
        # The offset and daylight depends on the date/time,
        # so it is not easy to set it up coherently
        return [
            {
                "id": x.token,
                "gmt_adjustment": "",  # "GMT+12:00",
                "use_daylight": "",  # 0
                "selected": x.token == selected_tz and x.token or None,
                "value": x.token,
                # '(GMT+12:00) Fiji, Kamchatka, Marshall Is.'
                "label": x.title,
            }
            for x in plone_tzs
        ]

    def safe_get_workspace(self):
        """This will safely return a sane element, either a workspace, or
        app or portal object to call helper methods on
        """
        portal = api.portal.get()
        return parent_workspace(self.context) or parent_app(self.context) or portal

    def safe_calendar_prefill(self, default=""):
        """Prefill for thecontainer field in event/edit forms"""
        ws = parent_workspace(self.context)
        if not ws:
            return ""
        return dumps({"id": "/".join(ws.getPhysicalPath()), "text": ws.title})

    def safe_member_prefill(self, context, name, default=""):
        """Tries safely to get a members prefill and returns nothing otherwise
        without failing. This way a form can be used for add and edit and
        within and outside a workspace
        """
        workspace = self.safe_get_workspace()
        if hasattr(workspace, "member_prefill"):
            return workspace.member_prefill(context, name, default)
        return default

    def safe_member_and_group_prefill(self, context, name, default=""):
        """Tries safely to get a members prefill and returns nothing otherwise
        without failing. This way a form can be used for add and edit and
        within and outside a workspace
        """
        workspace = self.safe_get_workspace()
        if hasattr(workspace, "member_and_group_prefill"):
            return workspace.member_and_group_prefill(context, name, default)
        return default
