from collections import defaultdict
from itertools import islice
from plone import api
from plone.batching import Batch
from plone.memoize.view import memoize
from plone.memoize.view import memoize_contextless
from plone.protect.auto import safeWrite
from ploneintranet import api as pi_api
from ploneintranet.core import _
from ploneintranet.layout.interfaces import IDiazoDashboardTemplate
from ploneintranet.search.interfaces import ISiteSearch
from ploneintranet.workspace.browser.tiles.workspaces import AdaptableWSDict
from ploneintranet.workspace.interfaces import IVisibleToIntranetUsersWorkspace
from ploneintranet.workspace.interfaces import IWorkspaceFolder
from ploneintranet.workspace.superspace import ISuperspaceFolder
from ploneintranet.workspace.utils import OpenEndedGraph
from urllib.parse import urlencode
from zope.component import getUtility
from zope.interface import implementer
from zope.publisher.browser import BrowserView
from zope.schema.interfaces import IVocabularyFactory

vocab = "ploneintranet.workspace.vocabularies.Divisions"


class WorkspacesOnNavigationRoot(BrowserView):
    """When calling the workspaces.html view on the site root will
    redirect to the canonical url
    """

    def __call__(self):
        return self.request.response.redirect(self.context.workspaces.absolute_url())


@implementer(IDiazoDashboardTemplate)
class Workspaces(BrowserView):
    """A view to serve as overview over workspaces"""

    _batch_size = 48
    section_class = "section-workspaces"

    def __call__(self):
        """Render the default template"""
        self.persist_sort_option()
        self.persist_grouping_option()
        return super().__call__()

    @property
    @memoize
    def user(self):
        return api.user.get_current()

    @property
    @memoize
    def userprofile(self):
        return pi_api.userprofile.get_current()

    # Workspaces that will always be hidden
    # if the user has no permission to access

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

    def get_solr_sort(self, request={}):
        sort_option = self.get_selected_sort_option()
        if sort_option == "activity":
            return "-modified"
        elif sort_option == "newest":
            return "-created"
        else:
            return "sortable_title"

    def get_selected_sort_option(self):
        """This will return the selected sort option

        The rules are:

        1. Check the request
        2. Check the user profile (unless viewing someone else's workspaces)
        3. Check the registry
        4. Default to "alphabet"
        """
        requested_sort = self.request.get("sort")
        if requested_sort:
            return requested_sort

        if not self.request.get("userid"):
            userprofile = self.userprofile
            requested_sort = getattr(userprofile, "my_workspace_sorting", None)

        if requested_sort:
            self.request.form["sort"] = requested_sort
            return requested_sort
        return api.portal.get_registry_record(
            "ploneintranet.workspace.my_workspace_sorting", default="alphabet"
        )

    @property
    @memoize
    def should_see_only_my_workspaces(self):
        """Check if the workspace overview should filter out the workspaces
        the user is not member of.
        Might also persist the preference on the userprofile.
        """
        userprofile = self.userprofile

        # By default only intranet users should persist the preference
        # and not intranet user should see only their workspaces
        intranet = api.portal.get().get("groups", {}).get("intranet")
        if intranet and userprofile and userprofile.getId() in intranet.members:
            preference = getattr(userprofile, "only_my_workspaces", False)
        else:
            preference = True

        # Check the marker present in the filter subform
        value_requested = self.request.get("only-my-workspaces-empty-marker")
        value = self.request.get("member")
        if not value_requested:
            if value is None:
                return preference
            return bool(value)

        value = bool(value)

        # Check if a change of preference should be recorded
        if userprofile and preference != value:
            userprofile.only_my_workspaces = value
            safeWrite(userprofile)

        return value

    @memoize
    def sort_options(self):
        selected_sort_option = self.get_selected_sort_option()
        options = api.portal.get_registry_record(
            "ploneintranet.workspace.sort_options",
            default={
                "alphabet": "Alphabetical",
                "newest": "Newest workspaces on top",
            },
        )
        options = (
            {
                "value": key,
                "content": _(value),
                "selected": key == selected_sort_option and "selected" or None,
            }
            for key, value in options.items()
        )
        return sorted(options, key=lambda x: x["content"])

    def persist_option(self, name, value, valid_options):
        """Persist the value under the given name in the user profile"""
        if api.user.is_anonymous() or self.request.get("userid"):
            # When viewing someone else's workspaces the grouping is forced to "no".
            # Don't persist that.
            return
        userprofile = self.userprofile
        preference = getattr(userprofile, name, False)

        if value == preference:
            return

        # otherwise check that it is a valid option
        for valid_option in valid_options:
            if valid_option["value"] == value:
                # if it is, update the user profile
                setattr(userprofile, name, value)
                safeWrite(userprofile)
                break

    def persist_sort_option(self):
        """This will persist the selected sort option in the user profile"""
        option = self.get_selected_sort_option()
        self.persist_option("my_workspace_sorting", option, self.sort_options())

    def persist_grouping_option(self):
        """This will persist the selected grouping option in the user profile"""
        option = self.get_selected_grouping_option()
        self.persist_option("my_workspace_grouping", option, self.grouping_options())

    def get_selected_grouping_option(self):
        """This will return the selected grouping option

        The rules are:

        1. Check the request
        2. Check the user profile (unless viewing someone else's workspaces)
        3. Default to "no" (no grouping)
        """
        requested_grouping = self.request.get("grouping")
        if requested_grouping:
            return requested_grouping

        if not self.request.get("userid"):
            userprofile = self.userprofile
            requested_grouping = getattr(userprofile, "my_workspace_grouping", None)

        if requested_grouping:
            self.request.form["grouping"] = requested_grouping
            return requested_grouping
        return "no"

    def grouping_options(self):
        selected_grouping_option = self.get_selected_grouping_option()
        options = [
            {"value": "no", "content": _("No grouping")},
            {"value": "division", "content": _("Group by division")},
            {"value": "tag", "content": _("Group by tag")},
            # Not yet implemented
            # {'value': 'workspace_type',
            #  'content': 'Group by workspace type'}
        ]
        for option in options:
            option["selected"] = (
                "selected" if selected_grouping_option == option["value"] else None
            )

        return options

    def workspace_types(self):
        options = [{"value": "", "content": _("All workspace types")}]
        translate = self.context.translate
        additional_filters = api.portal.get_registry_record(
            "ploneintranet.workspace.workspace_type_filters"
        )
        # Sort them by the translated values
        for portal_type in sorted(
            additional_filters, key=lambda x: translate(_(additional_filters.get(x)))
        ):
            label = additional_filters[portal_type]
            options.append({"value": portal_type, "content": _(label)})
        return options

    def workspaces_by_division(self):
        """returns workspaces grouped by division"""
        workspaces = self.workspaces(exclude_in_superspace=False)
        division_uids = [x.value for x in self.divisions]
        division_map = defaultdict(list)
        for workspace in workspaces:
            # Note: Already sorted as source list is sorted
            if workspace.uid in division_uids:
                division_map[workspace.uid].append(workspace)
            else:
                division_map[workspace.division].append(workspace)

        return division_map

    def workspaces_by_tag(self):
        """returns workspaces grouped by tag"""
        workspaces = self.workspaces(exclude_in_superspace=False)
        tag_map = defaultdict(list)
        for workspace in workspaces:
            brain = workspace.item
            subject = brain.Subject()
            if not subject:
                subject = ["untagged"]
            for tag in subject:
                tag_map[tag].append(workspace)

        return tag_map

    def _get_unsecure_workspace_query(self):
        """Check the request to compose a solr query that
        can return the requested workspaces.
        No security is applied to the query
        """
        sitesearch = getUtility(ISiteSearch)
        Q = sitesearch.Q
        query = Q(
            object_provides=IWorkspaceFolder.__identifier__,
            path_parents="/".join(self.context.getPhysicalPath()),
        )
        phrase = self.request.get("SearchableText", "").strip()
        if phrase:
            query &= sitesearch.create_lucene_query(phrase)
        workspace_type = self.request.get("workspace_type", [])
        if workspace_type:
            query &= Q(portal_type=workspace_type)
        include_archived = self.request.get("archived", False)
        if not include_archived:
            query &= Q(is_archived=False)

        if self.should_see_only_my_workspaces:
            userid = self.request.get("userid")
            if not userid:
                user = self.userprofile
            else:
                user = pi_api.userprofile.get(userid)
            if user:
                userprofile_workspaces = api.content.get_view(
                    "userprofile-workspaces", user, self.request
                )
                uids = [
                    uid.rpartition(":")[-1]
                    for uid in userprofile_workspaces.workspacepasuids
                ]
            else:
                uids = set()

            if not uids:
                # We do not have any associated UIDs,
                # do not even try to query solr
                return None
            query = query & sitesearch.lucene_or([Q(UID=u) for u in uids])
        return query

    def superspace_filter(self, workspaces):
        """
        If we are not in an expanded superspace the workspaces
        are filtered out:

        - if they are contained in a superspace
        - if they are empty supespaces and I am not the creator or the
          userid parameter is set (this comes from the userprofile view)
        """
        if self.request.get("workspace_type"):
            # Do nothing if another filtering is in process
            return workspaces

        # nodes are regular workspaces and superspaces I created
        nodes = set()
        # edges are [superspace, {work|super}space] lists
        edges = []
        superspace_uids = set()

        requested_user_id = self.request.get("userid")
        userid = requested_user_id or self.user.getId()
        superspace_uids = set()

        for workspace in workspaces:
            item = workspace["item"]
            uid = item.UID
            parent_superspace = item.context.get("superspace") or None
            edges.append([parent_superspace, uid])
            # keep track of the superspaces I created
            if "superspace" in item.portal_type:
                superspace_uids.add(uid)
                if not requested_user_id:
                    if item.context.get("Creator") == userid:
                        nodes.add(uid)
            else:
                nodes.add(uid)

        # We fix broken edges (parent superspace might not be available)
        for edge in edges:
            if edge[0] not in superspace_uids:
                edge[0] = None

        graph = OpenEndedGraph(nodes, edges)
        start_node = self.request.get("in_superspace") or None
        good_nodes = graph.get_meaningful_children(start_node)
        workspaces = [
            workspace for workspace in workspaces if workspace["item"].UID in good_nodes
        ]
        return workspaces

    @memoize
    def workspaces(self, exclude_in_superspace=True):
        """The list of my workspaces"""
        return list(map(AdaptableWSDict, self.get_workspaces(exclude_in_superspace)))

    @property
    @memoize
    def divisions(self):
        """return available divisions"""
        divisions = getUtility(IVocabularyFactory, vocab)(self.context)
        return divisions

    @property
    @memoize
    def superspaces(self):
        search_util = getUtility(ISiteSearch)
        response = search_util.query(
            filters={
                "path": "/".join(self.context.getPhysicalPath()),
                "object_provides": ISuperspaceFolder.__identifier__,
            },
            step=9999,
            sort="sortable_title",
        )
        return response

    @memoize_contextless
    def should_batch(self):
        """Do not batch results when grouping for something"""
        if self.request.get("in_superspace"):
            return False
        if self.get_selected_grouping_option() != "no":
            return False
        return True

    @property
    def _batch_start(self):
        """Try to get the batch start. If not possible return 0"""
        try:
            return int(self.request.get("b_start", 0))
        except Exception:
            return 0

    def next_batch_url(self, batch):
        """The next URL for batching"""
        if not self.should_batch():
            return ""
        next_start = self._batch_size + self._batch_start
        if len(batch) <= next_start:
            return ""
        qs = self.request.form.copy()
        qs.update({"b_start": next_start})
        return f"{self.request.getURL()}/?{urlencode(qs)}"

    def maybe_batch(self, workspaces):
        """Batch the given workspaces if no particular grouping is requested"""
        if not self.should_batch():
            return workspaces
        return Batch(workspaces, self._batch_size, self._batch_start, orphan=1)

    def get_workspaces(self, exclude_in_superspace=True):
        """The list of workspaces.

        To get the sorting method:

        1. Check the request
        2. Check the registry
        3. Default to "alphabet"
        """
        sitesearch = getUtility(ISiteSearch)

        unsecure_query = self._get_unsecure_workspace_query()
        if unsecure_query is None:
            return []
        # This will be used to make the query secure
        arau_query = sitesearch._get_arau_query()

        sort = self.get_solr_sort()

        accessible_workspaces = sitesearch.query(
            query=unsecure_query & arau_query, step=99999, secure=False, sort=sort
        )

        if self.should_see_only_my_workspaces:
            # this may include open spaces that I'm not a member of
            viewable_workspaces = accessible_workspaces
        else:
            # this additionally includes inaccessible secret workspaces
            # that are marked as visible for all intranet users
            additional_query = sitesearch.Q(
                object_provides=IVisibleToIntranetUsersWorkspace.__identifier__
            )
            viewable_workspaces = sitesearch.query(
                query=unsecure_query & (arau_query | additional_query),
                secure=False,
                step=99999,
                sort=sort,
            )

        workspaces = self.format_workspaces(accessible_workspaces, viewable_workspaces)
        if exclude_in_superspace:
            workspaces = self.superspace_filter(workspaces)
        return workspaces

    def format_workspaces(self, accessible_workspaces, viewable_workspaces):
        limit = self.request.get("limit", None)
        secure_uids = [r.UID for r in accessible_workspaces]
        workspaces = []
        try:
            limit = int(limit)
        except (ValueError, TypeError):
            limit = None
        good_items = islice(viewable_workspaces, limit)
        for item in good_items:
            workspace_item = self.format_item(item)
            if item.UID not in secure_uids:
                # reformat inaccessible private spaces
                workspace_item = self.reformat_inaccessible_item(workspace_item)
            workspaces.append(workspace_item)
        return workspaces

    def format_item(self, item):
        path_components = item.path.split("/")
        item_id = path_components[-1]
        return {
            "id": item_id,
            "uid": item.context["UID"],
            "title": item.title,
            "description": item.description,
            "url": item.url,
            "path": item.path,
            "url_fragment": "app-space",  # Star only
            "link_class": "link pat-inject pat-switch",
            "modified": item.modified,
            "created": item.context["created"],
            "is_division": item.context.get("is_division", False),
            "division": item.context.get("division", ""),
            "is_archived": item.is_archived,
            "archival_date": item.archival_date,
            "portal_type": item.portal_type,
            "item": item,
        }

    def reformat_inaccessible_item(self, workspace_item):
        """TODO: move this code code to the workspace tile template"""
        workspace_item["link_class"] = "link pat-modal"
        workspace_item["url"] += "/@@panel-request-access"
        workspace_item["url_fragment"] = "document-content"
        return workspace_item
