from AccessControl import Unauthorized
from Acquisition import aq_chain
from collections import defaultdict
from plone import api
from plone.app.uuid.utils import uuidToObject
from plone.dexterity.interfaces import IDexterityContent
from plone.memoize import view
from plone.uuid.interfaces import IUUID
from ploneintranet import api as pi_api
from ploneintranet.core import _
from ploneintranet.docconv.client.handlers import handle_file_creation
from ploneintranet.layout.browser.base import BasePanel
from ploneintranet.layout.interfaces import IAppContent
from ploneintranet.library.browser.utils import folderish
from ploneintranet.library.browser.utils import query
from ploneintranet.library.content import ILibraryApp
from Products.CMFCore.indexing import processQueue
from Products.CMFCore.WorkflowCore import WorkflowException
from Products.Five import BrowserView
from zope.component import adapter
from zope.interface import implementer
from zope.interface import Interface

import logging

log = logging.getLogger(__name__)


class IPublishWidely(Interface):
    """
    A behavior that allows an object to place a copy
    of itself in the Library.

    Note that not every object with this behavior necessarily
    is published widely. It's just enabled to do so if needed.
    """

    def can_publish_widely():
        """
        Is the context object eligible to be published widely?
        Checks the following conditions:
        - context is not contained in Library app
        - context state is 'published'
        - user has Reviewer permissions
        - context is not already widely published

        :rtype: bool
        """

    def can_update_widely():
        """
        Can the target object be updated with a copy of the source?
        Checks the following conditions:
        - context is not contained in Library app
        - context state is 'published'
        - user has Reviewer permissions
        - context is already widely published
        - user has permission to modify the target
        :rtype: bool
        """

    def can_retract_widely():
        """
        Is the context object's target eligible to be retracted from publication?
        Checks the following conditions:
        - context is not contained in Library app
        - context is already widely published
        - user has permission to retract the target

        :rtype: bool
        """

    def has_updated_source():
        """
        Checks if the source is newer than the target.
        :rtype: bool
        """

    def copy_to(target):
        """
        Copies the context object to the target.
        Submits copy for review.

        :param target: Target folder to copy to.
        :type target: ILibraryFolder.
        :returns: The newly created copy object.
        :rtype: IDexterityContent (same as self.context)
        """

    def retract():
        """
        Retracts (deletes) the target.

        :returns: The target path of the deleted target, or None
        :rtype: string or None

        """

    def source():
        """
        Get the original content object, if the current context
        is a widely published copy of an original.

        :returns: Original for this context, or None
        :rtype: IDexterityContent or None
        """

    def target():
        """
        Get the copied content object, if the current context
        is the original for a widely published copy.

        :returns: Target for this context, or None
        :rtype: IDexterityContent or None
        """


@implementer(IPublishWidely)
@adapter(IDexterityContent)
class PublishWidely:
    def __init__(self, context):
        self.context = context

    def _basic_publishing_permissions(self):
        app = IAppContent(self.context).get_app()
        # don't allow copying FROM library
        if ILibraryApp.providedBy(app):
            return False
        # only locally published content may be widely published
        try:
            if api.content.get_state(self.context) not in ("published",):
                return False
        except WorkflowException:
            # no workflow, e.g. images
            pass
        # only reviewers may publish widely
        return api.user.has_permission("Review portal content", obj=self.context)

    def can_publish_widely(self):
        # may only publish widely once
        if self.target():
            return False
        return self._basic_publishing_permissions()

    def can_update_widely(self):
        if not self._basic_publishing_permissions():
            return False
        # may only update if target exists
        target = self.target()
        if not target:
            return False
        return api.user.has_permission("Modify portal content", obj=target)

    def can_retract_widely(self):
        app = IAppContent(self.context).get_app()
        # don't allow retracting FROM library
        if ILibraryApp.providedBy(app):
            return False
        # Can only retract if target exists
        target = self.target()
        if not target:
            return False
        return api.user.has_permission("Delete objects", obj=target)

    def has_updated_source(self):
        target = self.target()
        if not target:
            return False
        source = IPublishWidely(target).source()
        if not source:
            return False

        return source.modified() > target.modified()

    @view.memoize
    def get_library(self):
        # cannot use object_provides query for some reason
        portal = api.portal.get()
        try:
            if ILibraryApp.providedBy(portal.library):
                return portal.library
        except (AttributeError, TypeError):
            pass
        for section in portal.objectValues():
            if ILibraryApp.providedBy(section):
                return section
        raise ValueError("No library app")

    def copy_to(self, target):
        if not self.can_publish_widely():
            log.error(f"Cannot publish widely: {self.context}")
            return None
        if not self.context.getTypeInfo() in target.allowedContentTypes():
            log.error(f"Invalid target: {target}")
            return None
        pi_api.events.disable_previews()

        user = api.user.get_current()
        wf_tool = api.portal.get_tool("portal_workflow")

        with api.env.adopt_roles(["Manager"]):
            new = api.content.copy(self.context, target)
            if api.content.get_state(target) != "private":
                for workflow in wf_tool.getWorkflowsFor(new):
                    if "submit" in workflow.transitions:
                        api.content.transition(new, "submit")
                        break
            new.changeOwnership(user)
            # list publishing user as first creator = owner
            creators = [user.id] + [x for x in new.creators if x != user.id]
            new.setCreators(creators)

            # If the required registry setting is present, order the items in
            # the target folder in reverse chronological order.
            if api.portal.get_registry_record(
                "ploneintranet.library.order_by_modified", default=False
            ):
                ordering = target.getOrdering()
                catalog = api.portal.get_tool("portal_catalog")
                query = {
                    "path": {"query": "/".join(target.getPhysicalPath()), "depth": 1},
                    "sort_on": "modified",
                    "sort_order": "descending",
                }
                brains = catalog(**query)
                for idx, brain in enumerate(brains):
                    ordering.moveObjectToPosition(brain.id, idx)

        log.info(
            "{} copied {} --> {}".format(
                user.id, self.context.absolute_url(), new.absolute_url()
            )
        )
        setattr(self.context, "publish_widely_target_uuid", IUUID(new))
        setattr(new, "publish_widely_source_uuid", IUUID(self.context))
        pi_api.events.enable_previews()
        handle_file_creation(new, None, asynchronous=True)
        return new

    def retract(self):
        if not self.can_retract_widely():
            return None
        target = self.target()
        path_elems = []
        for item in aq_chain(target)[1:]:
            if ILibraryApp.providedBy(item):
                break
            path_elems.append(item.getId())
        path_elems.reverse()
        target_path = "/".join(path_elems)
        try:
            api.content.delete(target, check_linkintegrity=False)
        except ValueError:
            return None
        return target_path

    def source(self):
        return self._safe_ref("publish_widely_source_uuid")

    def target(self):
        return self._safe_ref("publish_widely_target_uuid")

    def _safe_ref(self, uuid_attr):
        uuid = getattr(self.context, uuid_attr, None)
        if uuid:
            try:
                # Since plone.app.uuid 2.1 we need to call processQueue ourselves
                # because uuidToObject does not query the catalog anymore
                # but gets the path from the index and then traverses to the object.
                # Querying the catalog was causing the queue to be processed.
                processQueue()
                return uuidToObject(uuid)
            except Unauthorized:
                return None


class PublishActionView(BasePanel):

    title = _("Publish to library")
    _form_data_pat_inject_parts = (
        "source: #document-alert-bar::element; target: #workspaces-toolbar::element::after",  # noqa: E501
    )
    copy_obj = None

    @property
    def form_action(self):
        """The handler for this form"""
        return "{url}/@@feedback-library-copy-updated".format(
            url=self.context.absolute_url()
        )

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

    def update(self):
        if self.request.get("update_library_copy"):
            target_path = IPublishWidely(self.context).retract()
        else:
            target_path = self.request.get("target_path")
        if target_path:
            path = target_path.split("/")
            target = self.library.restrictedTraverse(path)
            self.copy_obj = IPublishWidely(self.context).copy_to(target)

    def can_retract_widely(self):
        try:
            return IPublishWidely(self.context).can_retract_widely()
        except TypeError:
            return False

    @property
    @view.memoize_contextless
    def library(self):
        return IPublishWidely(self.context).get_library()

    def library_url(self):
        return self.library.absolute_url()

    def copied_to_url(self):
        if not self.copy_obj:
            log.error("No copy found")
            return "#"
        return self.copy_obj.absolute_url()

    def allow_adding_to_section(self):
        ttool = api.portal.get_tool("portal_types")
        type_info_section = getattr(ttool, "ploneintranet.library.section")
        return self.context.portal_type in type_info_section.allowed_content_types

    def target_tree(self):
        """Return the complete library structure, flattened and divided by sections.

        We want to fill the drop-down of the publish-to-library panel with that structure.
        Every section becomes an optgroup, and all folders and sub-folders of each
        section are listed below that section, each as a separate option.
        In case of deeply nested folder structures, the titles of all parents up to the
        section are concatenated using a slash. The same is true for the value of each
        option, which is built from each folder's id, with the difference that it also
        contains the id of the section.

        Imagine we have the following structure in our Library
        Library
        |__Human resources  (section)
          |__Holidays  (folder)
             |__Bank holidays  (sub-folder)
          |__Sick leave  (folder)
        |__Manuals  (section)
           |__Security manuals  (folder)
           |__Electrical appliances  (folder)

        Then we want to create the following markup for a <select> out of it:
        <optgroup label="Human Resources">
          <option value="human-resources/holidays">Holidays</option>
          <option value="human-resources/holidays/bank-holidays">Holidays/Bank holidays</option>  # noqa: E501
          <option value="human-resources/sick-leave">Sick leave</option>
        </optgroup>
        <optgroup label="Manuals>
          <option value="manuals/security-manuals">Security manuals</option>
          <option value="manuals/electical-applicances">Electrical appliances</option>
        </optgroup>

        To make building this markup as simple as possible, we return the following:
        struct = [
            {
                "id": "human-resources",
                "title": "Human resources",
                "content": [
                    {"id": "human-resources/holidays", "title": "Holidays"},
                    {"id": "human-resources/holidays/bank-holidays", "title": "Holidays/Bank holidays"},  # noqa: E501
                    {"id": "human-resources/sick-leave", "title": "Sick leave"},
                ]
            },
            {
                "id": "manuals",
                "title": "Manuals",
                "content": [
                    {"id": "manuals/security-manuals", "title": "Security manuals"},
                    {"id": "manuals/electical-applicances", "title": "Electrical appliances"},  # noqa: E501
                ]
            }
        ]

        By default, library sections only allow library folders as content. In case our
        site is configured for library sections to allow the same content as library folders
        (flat structure), we add the section itself as potential target as first element
        in each optgroup.

        To build the data structure, we fetch all folderish contents of the library.
        Then we sort them by path, so that we get the contents into the order that reflects
        their parent-child relationship.
        """
        allow_sections = self.allow_adding_to_section()
        path_elements = self.library.getPhysicalPath()
        path = "/".join(path_elements)
        # Save the path level of the library's root
        path_depth = len(path_elements)
        results = query(path=path, portal_types=folderish)
        by_path = {item.getPath(): item for item in results}
        struct = []
        # Each top-level section will serve as a "buffer" that contains the paths
        # and titles of all sub-sections and sub-folders.
        section = None

        # This data structure will hold the last-seen tuple of (id, title) for each
        # level of folder depth. It is used as a temporary storage that allows to look
        # up the id and title of all parents of the current item.
        def _missing_tuple():
            """Return a tuple that indicates a missing item in the path.

            Happens when we can traverse to a folder but
            we do not have permission to the parent.
            """
            return ("missing", "Missing")

        memory = defaultdict(_missing_tuple)
        # Iterate over the results, sorted by their path
        # Sorting by path allows us to easily construct a parent-child relationship.
        for item_path in sorted(by_path, key=lambda s: s.split("/")):
            item_path_depth = item_path.count("/")
            item = by_path[item_path]
            item_id = item.getId
            item_title = item.title
            memory[item_path_depth] = (item_id, item_title)
            # If we are on the library top level, start a new section
            if item_path_depth == path_depth:
                # Flush the "buffer" if a section is already present
                if section:
                    struct.append(section)
                section = {"title": item_title, "id": item_id, "content": []}
                if allow_sections:
                    section["content"].append({"title": item_title, "id": item_id})
            else:
                # The id contains all parents, including the current section
                full_id = "/".join(
                    [memory[i][0] for i in range(path_depth, item_path_depth + 1)]
                )
                # The title contains all parents, starting below the current section
                full_title = "/".join(
                    [memory[i][1] for i in range(path_depth + 1, item_path_depth + 1)]
                )
                section["content"].append({"title": full_title, "id": full_id})
        # Last flush
        if section:
            struct.append(section)
        return struct


class RetractActionView(BrowserView):
    def can_publish_widely(self):
        try:
            return IPublishWidely(self.context).can_publish_widely()
        except TypeError:
            return False

    def __call__(self):
        if "form.buttons.retract" in self.request:
            target_path = IPublishWidely(self.context).retract()
            if not target_path:
                api.portal.show_message(
                    "Retracting document failed", self.request, "error"
                )
                return self.request.RESPONSE.redirect(
                    f"{self.context.absolute_url()}/view"
                )
            self.target_path = target_path
        return self.index()
