from BTrees.OOBTree import OOTreeSet
from collections import OrderedDict
from io import BytesIO
from logging import getLogger
from pdfrw import PdfReader
from pdfrw import PdfWriter
from plone import api
from plone.memoize.view import memoize
from ploneintranet import api as pi_api
from ploneintranet.core import _
from ploneintranet.layout.browser.base import BasePanel
from ploneintranet.search.interfaces import ISiteSearch
from time import time
from zope.annotation import IAnnotations
from zope.component import getUtility

logger = getLogger(__name__)


class PickerView(BasePanel):
    """The Panel through which I can delete the selected library contents"""

    form_data_pat_inject = ""
    _folderish_types = [
        "Folder",
        "ploneintranet.library.folder",
        "ploneintranet.library.section",
    ]
    _types_not_to_search_for = {"Image", "ploneintranet.library.app"}
    form_data_pat_validation = "disable-selector: #form-buttons-download"

    cart_key = "ploneintranet.library.cart"

    @property
    @memoize
    def allowed_portal_types(self):
        """All the types except the _types_not_to_search_for
        (tipically the library)
        """
        pt = api.portal.get_tool("portal_types")
        types = [t for t in pt.keys() if t not in self._types_not_to_search_for]
        return types

    @property
    @memoize
    def cart(self):
        """The cart of this user"""
        return IAnnotations(self.user, {}).get(self.cart_key, set())

    def update_cart(self, response):
        """Take a search response and update the items in the cart.
        We need to remove the obsolete ones and to add the new ones.
        """
        annotations = IAnnotations(self.user, {})
        if self.cart_key not in annotations:
            annotations[self.cart_key] = OOTreeSet()
        cart = self.cart
        old_keys = set(self.cart.keys())
        new_keys = {result.UID for result in response}
        if old_keys == new_keys:
            return
        list(map(cart.add, new_keys - old_keys))
        list(map(cart.remove, old_keys - new_keys))

    def query_items(self, additional_filters={}):
        """Search under the current context for the allowed portal types.
        Additional filters can be passed to extend or override
        the default query
        """
        sitesearch = getUtility(ISiteSearch)
        filters = {
            "path": "/".join(self.context.getPhysicalPath()),
            "portal_type": self.allowed_portal_types,
        }
        filters.update(additional_filters)
        sort = ["path_depth", "getObjPositionInParent"]
        response = sitesearch.query(filters=filters, sort=sort, step=9999)
        return response

    def structure_items_by_container_path(self, response):
        """Take a Solr response and return an ordered dict where
        the keys are item container path and the values are a list
        of the contained items. Items are SearchResults.

        This is an example of the returned structure:
        {
            '/a': [<item /a/0>, <item /a/1>,...],
            '/a/c': [<item /a/c/0>, <item /a/c/1>,...],
            '/b': [<item /b/0>, <item /b/1>,...],
        }
        """
        items_by_container_path = OrderedDict()
        for item in response:
            container_path = item.path.rpartition("/")[0]
            if container_path not in items_by_container_path:
                items_by_container_path[container_path] = []
            items_by_container_path[container_path].append(item)
        return items_by_container_path

    @property
    @memoize
    def items(self):
        """The items we can pick in the selector.

        We want them in a nice tree structure and sorted by position

        It returns a list of SearchResults with an additional
        children attribute that contains the contained elements
        """
        response = self.query_items()
        items_by_container_path = self.structure_items_by_container_path(response)

        def make_item(item):
            item.children = list(
                map(make_item, items_by_container_path.get(item.path, []))
            )
            return item

        items = list(
            map(
                make_item,
                items_by_container_path["/".join(self.context.getPhysicalPath())],
            )
        )
        return items

    @property
    @memoize
    def filename(self):
        """Return a dynamic filename for the downloaded pdf based on this time"""
        return str(time()).replace(".", "") + ".pdf"

    def item2pdf(self, item):
        """Tries to get a pdf representation of the object when it make sense"""
        obj = item.getObject()
        pdf = pi_api.previews.get_pdf(obj)
        payload = None
        if pdf:
            with pdf.open() as fh:
                payload = fh.read()
        try:
            if not payload:
                payload = obj.file.data
            return PdfReader(fdata=payload)
        except BaseException:
            logger.error("No PDF for %r" % obj)

    @property
    def download_many_query(self):
        """Look for the items we want to download in the query"""
        return {"UID": self.request.get("uid", [])}

    def get_candidate_pdf_items(self):
        """Returns the search results that match the query,
        sorted by their position in the uid parameter in the request
        """
        response = self.query_items(self.download_many_query)
        uids = self.request.get("uid", [])

        def _sort_key(result):
            try:
                return uids.index(result.UID)
            except ValueError:
                # Put this at the end
                return len(uids)

        return sorted(response, key=_sort_key)

    def download_many(self):
        """Get a list of the items we want to download, convert them in PDF
        and join them.
        """
        candidates = self.get_candidate_pdf_items()
        self.update_cart(candidates)
        writer = PdfWriter()
        for candidate in candidates:
            pdf = self.item2pdf(candidate)
            if pdf:
                writer.addpages(pdf.pages)

        ou = BytesIO()
        writer.write(ou)
        ou.seek(0)
        response = ou.read()
        setHeader = self.request.response.setHeader
        setHeader("Content-Type", "application/pdf")
        setHeader("Content-Length", len(response))
        setHeader("Content-Disposition", f'attachment;filename="{self.filename}"')
        return response

    def __call__(self):
        if self.is_posting():
            if not self.request.get("uid"):
                msg = _("No item selected")
                api.portal.show_message(msg, self.request, "warning")
                return self.request.response.redirect(self.context.absolute_url())
            return self.download_many()
        return super().__call__()


class DownloadTopicView(PickerView):
    """Download this topic as pdf"""

    @property
    def download_many_query(self):
        """We want all the stuff under this path"""
        return {"path": "/".join(self.context.getPhysicalPath())}

    def get_candidate_pdf_items(self):
        """Returns the search results that match the query,
        sorted by their position in the tree
        """
        response = self.query_items(self.download_many_query)
        items_by_container_path = self.structure_items_by_container_path(response)

        def _make_list(path):
            list_ = []
            for item in items_by_container_path.get(path, []):
                list_.append(item)
                list_.extend(_make_list(item.path))
            return list_

        path = "/".join(self.context.getPhysicalPath())
        items = _make_list(path)

        # We need to filter here and not in solr in order to get proper sorting
        return [item for item in items if item.portal_type not in self._folderish_types]

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