from Acquisition import aq_base
from collections import OrderedDict
from logging import getLogger
from plone import api
from plone.app.contenttypes.interfaces import IFile
from plone.app.layout.globals.interfaces import IViewView
from plone.memoize import instance
from plone.memoize import view
from ploneintranet import api as pi_api
from ploneintranet.core import _
from ploneintranet.layout.browser.download_pdf import DownloadAsMixin
from ploneintranet.layout.interfaces import IDiazoDashboardTemplate
from ploneintranet.library.browser import utils
from Products.Five import BrowserView
from urllib.parse import parse_qs
from urllib.parse import urlencode
from urllib.parse import urlparse
from zope.component import queryMultiAdapter
from zope.interface import implementer
from zope.publisher.interfaces import IPublishTraverse

import six
import six.moves.urllib.error
import six.moves.urllib.parse
import six.moves.urllib.request

log = getLogger(__name__)


@implementer(IDiazoDashboardTemplate)
class LibraryHomeView(BrowserView):
    """
    The '/library' link in the portal tabs is coded as a CMF action
    and not derived from the actual library app URL.
    This view redirects to the, or a, actual LibraryApp/view.
    """

    def __call__(self):
        catalog = api.portal.get_tool("portal_catalog")
        results = catalog(portal_type="ploneintranet.library.app")
        if not results:
            msg = "Somebody removed the last library app and broke the site."
            log.error(msg)
            return msg
        target = results[0].getURL()

        if len(results) > 1:
            # pick the first one, unless there is one actually called 'libary'
            for brain in results:
                if brain.id == "library":
                    url = brain.getURL()
                    qs = six.moves.urllib.parse.urlencode(self.request.form)
                    target = f"{url}?{qs}"

        # get us to an actual LibraryAppView
        self.request.response.redirect(target)


class LibraryBaseView(BrowserView, DownloadAsMixin):
    groupby = "section"
    show_back_button = False
    groupby_menu_enabled = False
    enabled = True
    folderish_portal_types = (
        "ploneintranet.library.section",
        "ploneintranet.library.folder",
    )
    rich_portal_types = ("Document",)
    pageish_portal_types = rich_portal_types + ("File", "News Item")
    linkish_portal_types = ("Link",)
    contentish_portal_types = pageish_portal_types + linkish_portal_types
    batch_size = 10
    section_class = "section-library"

    def maybe_disable_diazo(self):
        return False

    @view.memoize
    def is_file(self, context=None):
        """
        Are we handling a File object?
        """
        return IFile.providedBy(context)

    @view.memoize
    def context_content_type(self, obj):
        if getattr(aq_base(obj), "content_type", None):
            return obj.content_type() or ""
        return ""

    @view.memoize
    def is_audio_file(self, context=None):
        """
        Are we handling an audio File?
        """
        if context is None:
            context = self.context
        return self.is_file(context) and self.context_content_type(context).startswith(
            "audio/"
        )

    @view.memoize
    def is_video_file(self, context=None):
        """
        Are we handling a video File?
        """
        if context is None:
            context = self.context
        return self.is_file(context) and self.context_content_type(context).startswith(
            "video/"
        )

    @view.memoize
    def is_office_file(self, context=None):
        """
        Are we handling an office file?
        """
        # Note: Currently, we do not distinguish if a File object is an
        # office file (PDF, Word, Spreadsheet, Presentation, etc) or not.
        # In a future implementation, we might also look at the mime-type, so
        # that we can distinguish them from files that will not have a proper
        # preview, like Zip files.
        if context is None:
            context = self.context
        if not self.is_file(context):
            return False
        return not (self.is_audio_file(context) or self.is_video_file(context))

    @view.memoize
    def is_youtube(self, context=None):
        if context is None:
            context = self.context
        return self.context_content_type(context) == "text/x-uri-youtube"

    @view.memoize
    def is_link(self, context=None):
        """
        Are we handling a (real) link=
        """
        if context is None:
            context = self.context
        return context.portal_type in self.linkish_portal_types and not self.is_youtube(
            context
        )

    def make_safe_url(self, context):
        if getattr(aq_base(context), "getURL", None):
            return context.getURL()
        return f"{context.absolute_url()}/view"

    def next_page_url(self):
        """Generate the url for the next page"""
        form = self.request.form.copy()
        form["b_start:int"] = form.get("b_start", 0) + self.batch_size
        if form["b_start:int"] > len(self.children()):
            return ""
        for key in ("_", "_authenticator", "b_start"):
            form.pop(key, None)
        return "?".join((self.request.getURL(), urlencode(form)))

    @property
    def show_sections_navigation(self):
        return len(self.sections()) > 1

    @view.memoize
    def is_allowed_document_type(self):
        return pi_api.previews.is_allowed_document_type(self.context)

    @property
    @view.memoize_contextless
    def pdf_enabled(self):
        return api.portal.get_registry_record(
            "ploneintranet.library.enable_pdf_download_ui", default=False
        )

    def __call__(self):
        groupby = self.request.form.pop("groupby", None)
        if groupby != "tag":
            return super().__call__()

        target = self.context.absolute_url() + "/tag"
        if self.request.form:
            target += "?" + six.moves.urllib.parse.urlencode(self.request.form)
        self.request.response.redirect(target)

    @view.memoize
    def chain(self, getapp=False):
        _chain = []
        obj = self.context
        while (
            obj.portal_type != "ploneintranet.library.app"
            and obj.portal_type != "Plone Site"
        ):
            _chain.insert(0, dict(title=obj.Title, absolute_url=obj.absolute_url()))
            obj = obj.aq_inner.aq_parent

        if obj.portal_type == "Plone Site":
            raise AttributeError("Cannot find parent Library app!")

        if getapp:
            return obj
        return _chain

    @view.memoize
    def app(self):
        return self.chain(getapp=True)

    def can_add(self):
        return api.user.has_permission("Add portal content", obj=self.context)

    def can_edit(self):
        return api.user.has_permission("Modify portal content", obj=self.context)

    def can_delete(self):
        return api.user.has_permission(
            "Delete objects", obj=self.context
        ) and api.user.has_permission("Delete objects", obj=self.context.aq_parent)

    def can_download_library_content(self):
        return (
            self.context.portal_type == "ploneintranet.library.app" and self.pdf_enabled
        )

    def show_options_menu(self):
        """Display the three dots menu to access actions like:

        - add content
        - go to the edit -panel
        - download content
        """
        return (
            self.can_add()
            or self.can_edit()
            or self.can_delete()
            or self.can_download_library_content()
        )

    @view.memoize
    def sections(self):
        """Return toplevel section navigation"""
        app = self.app()
        # Then we look for the contained sections
        # and create the other navigation items
        sections = utils.query(
            "/".join(app.getPhysicalPath()),
            portal_types=["ploneintranet.library.section"],
            sort_by="getObjPositionInParent",
        )
        context_url = self.context.absolute_url()
        menu = []
        menu.extend(
            [
                dict(
                    title=section.title,
                    absolute_url=section.url,
                    current="current" if section.url in context_url else None,
                    cls="",
                )
                for section in sections
            ]
        )
        if len(menu) > 1:
            # The first navigation entry is always the app, but only if there
            # is more than one entry to show.
            # It is highlighted with the 'current class'
            # if the context is equal to that app
            menu.insert(
                0,
                dict(
                    title=_("All topics"),
                    absolute_url=app.absolute_url(),
                    current="current" if self.context == app else None,
                    cls="icon icon-library",
                ),
            )
        return menu

    def get_child_class(self, child):
        """Get the class for the chiod elements"""
        if child.portal_type in self.contentish_portal_types:
            return "type-document"
        return f"group-by-{self.groupby}"

    @property
    def solr_params(self):
        """Return the parameters for the solr query"""
        searchable_text = self.request.get("SearchableText", "").lower()
        path = "/".join(self.context.getPhysicalPath())
        params = {
            "path": path,
            "searchable_text": searchable_text,
            "sort_by": "getObjPositionInParent",
        }
        if searchable_text:
            # We want all the contentish types under this context
            params["portal_types"] = self.contentish_portal_types
        else:
            # Otherwise we want all the children and nephews of this page
            params["path_depth"] = [path.count("/") + 2, path.count("/") + 3]
            params["portal_types"] = (
                self.folderish_portal_types + self.contentish_portal_types
            )
        return params

    @view.memoize
    def get_logical_parent(self, parent_path):
        """Memoized version of restrictedTraverse

        We expect the path to be the absolute path,
        so we need to strip out this context's path
        """
        context_path = "/".join(self.context.getPhysicalPath())
        if context_path == parent_path:
            # We want to return None in this case because
            # the children does not need to be group
            return
        # Get the relative path
        parent_path = parent_path[len(context_path) + 1 :]
        return self.context.restrictedTraverse(parent_path, None)

    def structure_solr_results(self, results):
        """Take the solr results and create a nested structure
        to fill the page template tiles
        """
        children = {}
        for result in results:
            parent_path = result.path.rpartition("/")[0]
            parent = self.get_logical_parent(parent_path)
            # If there is no logical parent
            # consider the result as a child of this folder
            child = parent or result.getObject()
            if not child:
                # skip private section with published content and log warning
                # see https://github.com/quaive/ploneintranet/issues/1759
                log.warning(
                    (
                        "library view: private section/folder with published "
                        "content omitted: {}"
                    ).format(result.path)
                )
                continue
            if child not in children:
                children[child] = []
            if parent:
                children[child].append(result)

        pc = api.portal.get_tool("portal_catalog")

        def _sorting_key(obj):
            adapter = queryMultiAdapter(
                (obj, pc), name="getObjPositionInParent", default=None
            )
            return callable(adapter) and adapter()

        children_keys = sorted(children, key=_sorting_key)
        return OrderedDict((key, children[key]) for key in children_keys)

    @view.memoize
    def children(self):
        """Return children and nephews of current context"""
        results = utils.query(**self.solr_params)
        return self.structure_solr_results(results)

    @view.memoize
    def tile_class(self, child=None):
        no_children = len(self.children())
        if no_children == 2:
            return "span-2"
        elif no_children == 1:
            # A document must never span full width, only a listing tile
            # can do that
            c_type = getattr(child, "portal_type", "")
            if c_type in ("File",) or c_type in self.contentish_portal_types:
                return "span-2"
            else:
                return "span-full"
        return "span-1"

    @instance.memoize
    def preview_image_path(self, obj):
        """Lazy load preview urls.
        Taken from ploneintranet.search.base, used here for objects.
        """
        content_type = self.context_content_type(obj)

        if content_type.startswith("video/"):
            return "{}/@@poster.jpg?t={}".format(
                obj.absolute_url(), obj.file._blob._p_mtime
            )

        if content_type == "text/x-uri-youtube":
            parsed_url = urlparse(obj.remoteUrl)
            v = next(
                iter(parse_qs(parsed_url.query).get("v", [])), ""
            ) or parsed_url.path.lstrip("/")
            return f"https://img.youtube.com/vi/{v}/mqdefault.jpg"

        path = ""
        if getattr(obj, "preview_image_path", None):
            path = obj.preview_image_path
        elif obj.portal_type == "Image":
            path = f"{obj.absolute_url(1)}/@@images/image/large"
        elif obj.portal_type == "ploneintranet.userprofile.userprofile":
            path = f"{obj.absolute_url(1)}/@@avatar.jpg"
        elif obj.portal_type in ("Document", "News Item", "Link"):
            # For doc and news we look for lead images
            if hasattr(obj, "image") and obj.image is not None:
                path = f"{obj.absolute_url(1)}/@@images/image/large"
        elif self.is_file(obj):
            path = self.large_preview_image_path(obj)
        if path and not path.startswith("http"):
            path = self.request.physicalPathToURL(path)
        return path

    @instance.memoize
    def large_preview_image_path(self, obj):
        images = pi_api.previews.get_preview_urls(obj)
        if len(images):
            return images[0]
        return ""

    def icon(self, portal_type):
        if portal_type in self.pageish_portal_types:
            return "icon-doc-text"
        elif portal_type in self.linkish_portal_types:
            return "icon-link"
        else:
            return "icon-squares"

    def icon_label(self, obj):
        if self.is_youtube(obj):
            return "icon-youtube"
        if self.is_video_file(obj):
            return "icon-file-video"
        if self.is_audio_file(obj):
            return "icon-file-audio"
        if self.is_link(obj):
            return "icon-link-alternative"
        return "icon-doc-text"


@implementer(IDiazoDashboardTemplate)
class LibraryAppView(LibraryBaseView):
    groupby_menu_enabled = True

    def info(self):
        return {}


@implementer(IDiazoDashboardTemplate)
class LibrarySectionView(LibraryBaseView):
    groupby_menu_enabled = True

    def info(self):
        return dict(title=self.context.Title, description=self.context.Description)


@implementer(IDiazoDashboardTemplate)
class LibraryFolderView(LibraryBaseView):
    show_back_button = True
    show_sections_navigation = True
    groupby_menu_enabled = False

    @view.memoize
    def parent(self):
        return self.context.aq_parent

    def info(self):
        return dict(chain=self.chain(), description=self.context.Description)


@implementer(IDiazoDashboardTemplate, IPublishTraverse, IViewView)
class LibraryTagView(LibraryBaseView):
    groupby = "tag"
    groupby_menu_enabled = True
    pdf_enabled = False

    def __init__(self, context, request):
        super().__init__(context, request)
        self.request_tag = None
        self.enabled = True  # only in solr

    def publishTraverse(self, request, name):
        """Extract self.request_tag from URL /tag/foobar"""
        self.request_tag = six.moves.urllib.parse.unquote(name)
        return self

    def info(self):
        if self.request_tag:
            return dict(title=self.request_tag, klass="icon-tag")
        else:
            return {}

    def sections(self):
        """Toplevel section navigation, targets tag facet"""
        menu = super().sections()
        for section in menu:
            # This can get called repeatedly, and in some cases, /tag is
            # already appended. Don't append a second time
            if not section["absolute_url"].endswith("/tag"):
                section["absolute_url"] += "/tag"
        return menu

    @property
    def solr_params(self):
        """Return the parameters for the solr query"""
        searchable_text = self.request.get("SearchableText", "").lower()
        params = {
            "facet_by": "tags",
            "path": "/".join(self.context.getPhysicalPath()),
            "portal_types": self.contentish_portal_types,
            "searchable_text": searchable_text,
            "tags": self.request_tag or self.request.get("Subject"),
            "sort_by": "Title",
        }
        return params

    @property
    @view.memoize
    def tag_factory(self):
        class Tag:
            description = ""
            portal_type = ""
            url_template = "/".join((self.context.absolute_url(), "tag", "{}"))

            def content_type(self):
                return ""

            def __init__(self, tag):
                self.title = tag

            def getURL(self):
                return self.url_template.format(
                    six.moves.urllib.parse.quote(self.title)
                )

        return Tag

    def maybe_taggify(self, obj):
        """If obj is a string (a tag) we call the tag_factory
        to have an object that can be easily rendered by our template
        """
        if not isinstance(obj, str):
            return obj
        return self.tag_factory(obj)

    def structure_solr_results(self, results):
        """Take the solr results and create a nested structure
        to fill the page template tiles
        """
        # We take all the tags in the query
        # different from the currently requested one
        tags = [
            tag["name"]
            for tag in results.facets.get("tags", [])
            if tag["name"] != self.request_tag
        ]
        # We initialize our structure
        # It is temporary because we want to sort it later
        tmp_children = {tag: set() for tag in tags}
        # Finally we can fill our structure
        # For each result we want to update the structure
        # with the additional tags and maybe the document
        # if we are already filtering
        # If filtering is in place we just show the nested tags
        include_results = self.request_tag or self.request.get("SearchableText")
        tags_set = set(tags)
        for result in results:
            result_tags = tags_set.intersection(result.Subject())
            if not result_tags:
                # No relevant tags for this document, it will be a child
                # But we do not display any children if no search filter is set
                if include_results:
                    tmp_children[result] = set()
            else:
                has_only_this_tag = len(result_tags) == 1 and result_tags == set(
                    result.Subject()
                )
                for result_tag in result_tags:
                    tmp_children[result_tag].update(result_tags)
                    if include_results:
                        tmp_children[result_tag].add(result)
                    elif has_only_this_tag:
                        # Also include the results if the document has just one tag,
                        # otherwise it will not be displayed anywhere
                        tmp_children[result_tag].add(result)

        # Now we sort the data structure
        # First all the tags and then all the documents
        children = OrderedDict()
        tmp_child_keys = sorted(
            tmp_children,
            key=lambda x: (
                hasattr(x, "portal_type"),
                x.title.lower() if hasattr(x, "portal_type") else x.lower(),
            ),
        )
        for tmp_child in tmp_child_keys:
            tmp_values = tmp_children[tmp_child]
            child = self.maybe_taggify(tmp_child)
            if child not in children:
                children[child] = []
            for tmp_value in tmp_values:
                # Skip tags that happen to be nested
                if tmp_value != tmp_child:
                    children[child].append(self.maybe_taggify(tmp_value))
            # Sort the children, first by portal_type and the by title
            children[child].sort(key=lambda x: (x.portal_type, x.title))
        return children

    def __call__(self):
        groupby = self.request.form.pop("groupby", None)
        if groupby != "section":
            return super(LibraryBaseView, self).__call__()

        target = self.context.absolute_url()
        if self.request.form:
            target += "?" + six.moves.urllib.parse.urlencode(self.request.form)
        self.request.response.redirect(target)
