from collections import defaultdict
from collections import OrderedDict
from collective.workspace.interfaces import IWorkspace
from datetime import datetime
from logging import getLogger
from plone import api
from plone.memoize import ram
from plone.memoize.view import memoize
from ploneintranet import api as pi_api
from ploneintranet.core import _
from ploneintranet.layout.browser.proto import _5min_cache_key
from ploneintranet.workspace.browser.add_content import AddBase
from ploneintranet.workspace.case import ICase
from ploneintranet.workspace.config import INTRANET_USERS_GROUP_ID
from ploneintranet.workspace.events import WorkspaceCreatedFromTemplateEvent
from ploneintranet.workspace.subscribers import _reset_security_context
from ploneintranet.workspace.superspace import ISuperspaceFolder
from ploneintranet.workspace.utils import purge_and_refresh_security_manager
from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
from zope.dottedname.resolve import resolve
from zope.event import notify

vocab = "ploneintranet.workspace.vocabularies.Divisions"
log = getLogger(__name__)


class AddWorkspace(AddBase):
    """
    Adds a workspace-ish. Supports multiple variants:
    1. policy 'packages' on the default workspacefolder
       (hardcoded)
    2. templates on any variant of workspace-ish
       (discovered from templates folder)
    3. any workspace-ish without a template
       (discovered from addable types)
    4. superspaces

    Templates are security filtered: you can only use a template if
    you have access to the template, typically because you're a member
    of the template, possibly via an indirect group membership.
    """

    suppress_messages = False
    title = _("Create workspace")
    _form_data_pat_inject_parts = None
    form_data_pat_inject = (
        "source: #workspaces ; target: #workspaces && "
        "target: #global-statusmessage; source: #global-statusmessage; "
        "loading-class: ''"
    )
    template = ViewPageTemplateFile("templates/add_workspace.pt")

    default_fti = "ploneintranet.workspace.workspacefolder"
    default_selected = "private"

    @property
    @ram.cache(_5min_cache_key)
    def policies(self):
        """The available workspace policies for a newly created workspace"""
        policies = OrderedDict()
        policies["secret"] = dict(
            join_policy="admin",
            participant_policy="producers",
            label=_("Locked down, secret workspace"),
            description=_(
                "The workspace will only be accessible by people you invite."
                " The workspace will not be visible on the workspaces overview."
                " Documents in this group are subject to review before they can"
                " be shared within the group. Nothing in this group will ever"
                " be visible to non-members."
            ),
        )
        policies["private"] = dict(
            visible_to_intranet_users=not (
                api.portal.get_registry_record(
                    "ploneintranet.workspace.hide_private_nonmember_workspaces",
                    default=False,
                )
            ),
            join_policy="team",
            participant_policy="moderators",
            label=_("Team-managed, private workspace"),
            description=_(
                "The workspace will only be accessible for team members."
                " Any member can add a new team member."
                " Once you're in the team, you can do pretty much anything."
                " Nothing in this group will ever be visible to non-members."
            ),
        )
        policies["open"] = dict(
            accessible_to_intranet_users=True,
            visible_to_intranet_users=True,
            join_policy="team",
            participant_policy="publishers",
            label=_("Open community"),
            description=_(
                "Any logged-in user is allowed to join the group and participate."
                " Anything in this group is visible for any logged-in user,"
                " even if they're not a member."
            ),
        )
        return policies

    # rendering helpers

    def workspace_templates(self):
        """Templates for normal workspaces"""
        return self.templates_by_type().get(self.default_fti, [])

    def fti_id2option(self, fti_id):
        """Given an FTI id, return it as an options for the add form"""
        pt = api.portal.get_tool("portal_types")
        fti = pt[fti_id]
        return {"value": fti_id, "label": fti.Title()}

    @memoize
    def special_templateless(self):
        """
        Addable but no template and not a default workspace.
        Uses unfiltered template list to ensure that only types that have
        no template at all are added to the menu.
        Also exclude superspace types
        """
        _blocked = {x.portal_type for x in self.templates_folder.objectValues()}
        _blocked.add(self.default_fti)
        # A workgroup will never be added inside the workspaces folder
        _blocked.add("ploneintranet.userprofile.workgroup")

        for fti in self.context.allowedContentTypes():
            klass = resolve(fti.klass)
            if ISuperspaceFolder.implementedBy(klass):
                _blocked.add(fti.getId())
        allowed = [fti_id for fti_id in self._addable_types() if fti_id not in _blocked]
        return list(map(self.fti_id2option, allowed))

    def all_templates(self):
        return self.workspace_templates() + self.workspace_type_special_options()

    @property
    def all_templates_dict(self):
        return {
            template["value"]: template
            for template in self.all_templates()
            if template not in self.special_templateless()
        }

    @memoize
    def _addable_types(self):
        return [fti.getId() for fti in self.context.allowedContentTypes()]

    @memoize
    def templates_by_type(self):
        """Gets the templates as a dictionary
        to fill a select or a radio group
        """
        allowed_types = self._addable_types()
        templates_by_type = defaultdict(list)
        for template in self.allowed_templates.values():
            if template.portal_type in allowed_types:
                templates_by_type[template.portal_type].append(
                    {
                        "value": template.getId(),
                        "label": template.Title(),
                        "description": template.Description(),
                    }
                )
        for key in templates_by_type:
            templates_by_type[key].sort(key=lambda x: x["label"])
        return templates_by_type

    def workspace_type_generic_options(self):
        policies = self.policies
        options = [
            {
                "value": key,
                "label": policies[key]["label"],
                "description": policies[key]["description"],
            }
            for key in policies
        ]

        options.extend(self.workspace_templates())
        return options

    def workspace_type_generic_depends_condition(self):
        policies = self.policies
        return " or ".join([f"workspace-type={key}" for key in policies])

    def workspace_type_special_options(self):
        """All the template options for non-default workspace types.
        Plus: an option for each type without a template
        """
        options = []
        templates_by_type = self.templates_by_type()
        [
            options.extend(templates_by_type[key])
            for key in templates_by_type
            if key != self.default_fti
        ]
        options.extend(self.special_templateless())
        return options

    @property
    @memoize
    def superspace_types(self):
        return [fti_id for fti_id in self._addable_types() if "superspace" in fti_id]

    def workspace_type_hierarchical_options(self):
        """The options to create a superspace"""
        return list(map(self.fti_id2option, self.superspace_types))

    def workspace_type_optgroups(self):
        """Return the optgroups for the workspace type selector"""
        optgroups = [
            {
                "label": _("Generic workspace"),
                "options": self.workspace_type_generic_options(),
            }
        ]
        special_options = self.workspace_type_special_options()
        if special_options:
            optgroups.append(
                {"label": _("Special workspace"), "options": special_options}
            )
        hierarchical_options = self.workspace_type_hierarchical_options()
        if hierarchical_options:
            optgroups.append(
                {"label": _("Hierarchical groupings"), "options": hierarchical_options}
            )
        return optgroups

    @property
    @memoize
    def allowed_templates(self):
        """A {id: template} dict containing only the templates
        which are accessible for the current user.
        """
        if not self.templates_folder:
            return {}
        brains = api.content.find(
            context=self.templates_folder,
            depth=1,
            sort_on="getObjPositionInParent",
        )
        return {
            brain.getId: brain.getObject()
            for brain in brains
            if brain.getId not in self.policies
        }

    @property
    @memoize
    def portal_type(self):
        """Check the portal type selected
        by the workspace-type request parameter
        """
        selected = self.request.form.get("workspace-type", None)
        if selected in self.policies:
            return self.default_fti
        templates = self.templates_folder
        if selected in templates:
            return templates[selected].portal_type
        if selected in self._addable_types():
            return selected
        raise KeyError("invalid workspace-type: %s" % selected)

    def __call__(self):
        """Render form, or handle POST and redirect"""
        title = self.request.form.get("title", None)
        selected = self.request.form.get("workspace-type", None)
        if not (title and selected):
            return self.template()
        url = self.create()
        return self.redirect(url)

    def redirect(self, url):
        """
        Has its own method to allow overriding
        """
        url = f"{url}#workspace-settings"
        return self.request.response.redirect(url)

    def get_new_object(self, container):
        """This will create a new object"""
        if not container:
            container = self.context

        if self.request.form.get("workspace-type") in self.all_templates_dict:
            return self.create_from_template(container)
        else:
            return super().get_new_object(container)

    def copy_team(self, new, old):
        """Copy the _team contents from the old to the new object"""
        new_ws = IWorkspace(new)
        old_ws = IWorkspace(old)
        members = new_ws.members
        for membership in old_ws:
            # we skip updating the current user (he is an admin by default)
            if membership.user not in members:
                new_ws.add_to_team(membership.user, groups=membership.groups)

    def maybe_preserve_ownership(self, new, template):
        """In some cases we want to preserve the template ownership
        on the new object
        """
        if not ICase.providedBy(new):
            # We do not want this to happen for workspaces...
            return
        if not api.portal.get_registry_record(
            "ploneintranet.workspace.preserve_template_ownership", default=False
        ):
            # ...or if it is not specified through a registry record
            return
        pu = api.portal.get_tool("plone_utils")
        pu.changeOwnershipOf(new, template.getOwner().getId(), recursive=True)

    def create_from_template(self, container=None):
        """Create an object with the given template"""
        if not container:
            container = self.context

        template = self.get_template()
        if not template:
            api.portal.show_message(
                _("Please specify which Case Template to use"),
                request=self.request,
                type="error",
            )
            return

        # Create neither previews for copied contents nor status updates.
        # Also, do not reindex copied content at this moment.
        pi_api.previews.events_disable(self.request)
        pi_api.microblog.events_disable(self.request)

        # avoid calling too many times an expensive event
        # the updates are anyway handled in the subscriber
        # handle_case_workflow_state_changed
        container._v_skip_update_todo_state = True

        self.request.environ["disable.auditlog"] = True

        # Needed for the custom _get_id method of the workspace container
        # this will skip a rename
        new_id = self.get_new_unique_id(container)
        self.request.set("_copy_move_suggested_id", new_id)

        new = api.content.copy(source=template, target=container, safe_id=False)
        new.accessible_to_intranet_users = template.accessible_to_intranet_users
        new.visible_to_intranet_users = template.visible_to_intranet_users

        userid = api.user.get_current().id
        _reset_security_context(userid, new.REQUEST, invalidate_cache=True)
        self.copy_team(new, template)
        self.maybe_preserve_ownership(new, template)
        new.creation_date = datetime.now()

        self.request.environ["disable.auditlog"] = False
        notify(WorkspaceCreatedFromTemplateEvent(new))

        # Now that the new workspace has been created, re-index it (async)
        # using the solr-maintenance convenience method.
        container._v_skip_update_todo_state = False
        return new

    def create(self, container=None):
        # The design currently forsees to go back to the workspace overview.
        super().create(container=container)
        return self.context.absolute_url()

    def get_template(self):
        """Get a template to copy"""
        template_id = self.request.get("workspace-type")
        if not template_id:
            return
        # anyone trying to bypass this deserves an uncaught KeyError
        return self.allowed_templates[template_id]

    def update(self, obj):
        """Update the object and returns the modified fields and errors"""
        self.set_workspace_policy(obj)
        return super().update(obj)

    def set_workspace_policy(self, obj):
        """Set's the workspace policy for the objects given a scenario"""
        workspace_type = self.request.form.get("workspace-type")

        if workspace_type in self.superspace_types:
            obj.manage_addLocalRoles(INTRANET_USERS_GROUP_ID, obj._superspace_roles)
            return

        try:
            policy = self.policies[workspace_type]
        except KeyError:
            return
        finally:
            purge_and_refresh_security_manager()

        for key in (
            "accessible_to_intranet_users",
            "join_policy",
            "participant_policy",
            "visible_to_intranet_users",
        ):
            value = policy.get(key)
            setattr(obj, key, value)
