from collections import OrderedDict
from io import BytesIO
from io import StringIO
from json import dumps
from plone import api
from plone.app.blocks.interfaces import IBlocksTransformEnabled
from plone.memoize.view import memoize
from plone.memoize.view import memoize_contextless
from ploneintranet.core import _
from ploneintranet.layout.browser.actions import PIDeleteConfirmationForm
from ploneintranet.layout.interfaces import IDiazoAppTemplate
from ploneintranet.search.interfaces import ISiteSearch
from ploneintranet.userprofile.events import MemberGroupMembershipChangedEvent
from ploneintranet.userprofile.interfaces import IFunctionalPrincipal
from ploneintranet.workspace.basecontent.utils import dexterity_update
from Products.CMFCore.permissions import ModifyPortalContent
from Products.Five import BrowserView
from zope.component import getMultiAdapter
from zope.component import getUtility
from zope.deprecation import deprecate
from zope.event import notify
from zope.i18n import translate
from zope.interface import implementer
from zope.lifecycleevent import Attributes
from zope.lifecycleevent import ObjectModifiedEvent
from ZTUtils import make_query

import csv
import openpyxl

_marker = object()


class GroupBase(BrowserView):
    # I suggest a multiple of 6
    _batch_size = 48
    userprofile_portal_types = ("ploneintranet.userprofile.userprofile",)
    show_more_menu = False

    @property
    @memoize_contextless
    def app(self):
        return api.portal.get().apps["contacts"]

    def page_number(self):
        """Get current page number from the request."""
        try:
            page = int(self.request.form.get("page", 1))
        except ValueError:
            page = 1
        return page

    def get_start(self):
        """Fills the start parameter of the search query, i.e. the first
        element of the batch."""
        return (self.page_number() - 1) * self._batch_size

    def next_page_number(self):
        """Get page number for next page of search results."""
        page = self.page_number()
        item_length = self.num_non_functional_principals
        if page * self._batch_size < item_length:
            return page + 1
        else:
            return None

    def next_page_url(self):
        """Get url for the next page of results."""
        next_page_number = self.next_page_number()
        if not next_page_number:
            return ""
        new_query = make_query(self.request.form.copy(), {"page": next_page_number})
        return f"{self.request.ACTUAL_URL}?{new_query}"

    def batch(self):
        """"""
        principals = self.non_functional_principals
        start = self.get_start()
        if start >= len(principals):
            return []
        end = start + self._batch_size
        return principals[start:end]

    @property
    @memoize
    def groups_container(self):
        """Returns the group container (if found) or an empty dictionary."""
        portal = api.portal.get()
        return portal.get("groups", {})

    @property
    @memoize
    def users_container(self):
        """Returns the group container (if found) or an empty dictionary."""
        portal = api.portal.get()
        return portal.get("profiles", {})

    @property
    @memoize
    def users(self):
        """Return the user that are in the user profile container."""
        container = self.users_container
        keys = self.userids
        pc = api.portal.get_tool("portal_catalog")
        return sorted(
            (container[key] for key in keys),
            key=lambda x: getMultiAdapter((x, pc), name="sortable_title")(),
        )

    @property
    @memoize
    def groups(self):
        """Return the groups that are in the group container."""
        container = self.groups_container
        keys = self.groupids
        return sorted((container[key] for key in keys), key=lambda x: x.title)

    @property
    @memoize
    def principals(self):
        """Return all the groups and users related to this group.

        If a search filter is applied, filter the results
        """
        principals = self.groups + self.users
        searchable_text = self.request.get("SearchableText", "")
        if searchable_text:
            searchable_text = (searchable_text or "").lower()
            principals = [
                principal
                for principal in principals
                if searchable_text in principal.Title().lower()
            ]
        return principals

    @property
    @memoize
    def non_functional_principals(self):
        """Filter out the principals that have a functional role."""
        return [
            principal
            for principal in self.principals
            if not IFunctionalPrincipal.providedBy(principal)
        ]

    @deprecate("Deprecated in 6.3.1.dev0. Use filter_by_review_state instead.")
    def is_enabled(self, principal):
        return self.filter_by_review_state(principal)

    @property
    @memoize_contextless
    def excluded_review_states(self) -> list:
        """Return the list of review states that are filtered out."""
        review_states = api.portal.get_registry_record(
            "ploneintranet.userprofile.ui.excluded_review_states", default=_marker
        )
        if review_states is _marker:
            return ["disabled", "pending"]
        return list(review_states)  # type: ignore

    def filter_by_review_state(self, principal):
        """Filter out principals that are in a disabled review state."""
        return (
            principal
            and api.content.get_state(obj=principal) not in self.excluded_review_states
        )


@implementer(IBlocksTransformEnabled, IDiazoAppTemplate)
class WorkGroupView(GroupBase):
    """View for a user workgroup."""

    _default_tabs = ("view",)

    @property
    @memoize
    def title(self):
        return self.context.title

    @property
    @memoize
    def groupid(self):
        return self.context.getId()

    @property
    @memoize
    def can_edit(self):
        return api.user.has_permission(ModifyPortalContent, obj=self.context)

    @property
    @memoize
    def can_delete(self):
        return api.user.has_permission("Delete objects", obj=self.context)

    @property
    def show_more_menu(self):
        return self.can_edit or self.can_delete

    @property
    @memoize
    def userids(self):
        """Return the member ids that are in the user profile container."""
        container = self.users_container
        return set(self.context.members) & set(container)

    @property
    @memoize
    def num_users(self):
        return len(self.userids)

    @property
    @memoize
    def num_non_functional_principals(self):
        return len(self.non_functional_principals)

    @property
    @memoize
    def groupids(self):
        """Return the group ids that are in the group container."""
        container = self.groups_container
        return set(self.context.members) & set(container)

    def members_field_prefill(self):
        """Return JSON for pre-filling a pat-autosuggest field with the values
        for that field."""
        prefill = {
            principal.getId(): principal.Title() or principal.getId()
            for principal in self.principals
        }
        if prefill:
            return dumps(prefill)
        return ""

    @property
    @memoize
    def owner_id(self):
        return self.context.getOwnerTuple()[1]

    @property
    @memoize
    def maintainers(self):
        """Return the maintainers of this group as found in the
        __ac_local_roles__"""
        local_roles = self.context.__ac_local_roles__
        return [key for key in local_roles if "Editor" in local_roles[key]]

    @property
    @memoize
    def maintainer_profiles(self):
        container = self.users_container
        pc = api.portal.get_tool("portal_catalog")
        keys = set(self.maintainers)
        keys.add(self.owner_id)
        return sorted(
            filter(None, (container.get(key) for key in keys)),
            key=lambda x: getMultiAdapter((x, pc), name="sortable_title")(),
        )

    @property
    @memoize
    def maintainer_groups(self):
        mt = api.portal.get_tool("membrane_tool")
        return sorted(
            mt(exact_getGroupId=self.maintainers), key=lambda brain: brain.Title
        )

    def maintainer_field_prefill(self):
        """Return JSON for pre-filling a pat-autosuggest field with the values
        for that field."""
        prefill = {
            principal.getId(): principal.Title()
            for principal in self.maintainer_profiles
        }
        prefill.update(
            {
                principal.getGroupId: principal.Title
                for principal in self.maintainer_groups
            }
        )
        if prefill:
            return dumps(prefill)
        return ""

    def maybe_change_maintainers(self):
        local_roles = self.context.__ac_local_roles__
        maintainers = self.request.form.get("maintainers", "").split(",")
        new_maintainers = {principalid for principalid in maintainers if principalid}
        old_maintainers = {key for key in local_roles if "Editor" in local_roles[key]}
        for principalid in new_maintainers - old_maintainers:
            try:
                api.group.grant_roles(principalid, obj=self.context, roles=["Editor"])
            except api.exc.GroupNotFoundError:
                api.user.grant_roles(principalid, obj=self.context, roles=["Editor"])
        for principalid in old_maintainers - new_maintainers:
            try:
                api.group.revoke_roles(principalid, obj=self.context, roles=["Editor"])
            except api.exc.GroupNotFoundError:
                api.user.revoke_roles(principalid, obj=self.context, roles=["Editor"])

    def maybe_edit(self):
        """Fiddle with the request to properly adapt the data in the form
        before calling dexterity_update to set the appropriate field values."""
        # avoid removing the image if not file is passed
        form = self.request.form
        if form.get("image", None) == "":
            form.pop("image", None)
        form["members"] = "\n".join(form.get("members", "").split(","))
        old_members = self.context.members
        modified, errors = dexterity_update(self.context, self.request)
        # Change local roles
        old_local_roles = self.context.__ac_local_roles__.copy()
        self.maybe_change_maintainers()
        is_modified = modified or self.context.__ac_local_roles__ != old_local_roles
        if errors:
            api.portal.show_message(
                _("There was a problem: ${errors}.", mapping={"errors": errors}),
                request=self.request,
                type="error",
            )
        elif is_modified:
            api.portal.show_message(
                _("Item edited."), request=self.request, type="success"
            )
            descriptions = [
                Attributes(interface, *fields) for interface, fields in modified.items()
            ]
            notify(ObjectModifiedEvent(self.context, *descriptions))
            # zegg.quaive subscribes to this event
            if old_members != self.context.members:
                notify(
                    MemberGroupMembershipChangedEvent(
                        self.context, old_members, self.context.members
                    )
                )

    def maybe_edit_and_redirect(self):
        """Maybe edit content and return a url."""
        if "submit" in self.request.form:
            self.maybe_edit()
        return self.request.response.redirect(self.context.absolute_url())

    def __call__(self):
        """If we are posting modify and redirect to the view."""
        if self.request.method == "POST":
            return self.maybe_edit_and_redirect()
        return super().__call__()


class WorkGroupMembershipReportXLSX(WorkGroupView):
    @property
    def fields(self):
        fields = OrderedDict(
            [
                ("workgroup", "Workgroup"),
                ("first_name", "First Name"),
                ("last_name", "Last Name"),
                ("username", "Username"),
                ("email", "Email"),
                ("source", "Source"),
                ("department", "Department"),
                ("role_in_group", "Role in Group"),
            ]
        )
        return fields

    @property
    def rows(self):
        """Prepare user profiles to be exported to excel."""
        admins = self.maintainer_profiles

        data = []
        profiles = set(self.users + admins)

        def sort_key(profile):
            return (profile.last_name or "", profile.first_name or "", profile.getId())

        for profile in sorted(profiles, key=sort_key):
            if api.content.get_state(obj=profile) == "disabled":
                continue

            role_in_group = None
            if profile in admins:
                role_in_group = translate(
                    _("group_admin", default="Group administrator"),
                    context=self.request,
                )
            else:
                role_in_group = translate(
                    _("group_member", default="Group member"), context=self.request
                )

            item = {
                "workgroup": self.context.title,
                "first_name": profile.first_name,
                "last_name": profile.last_name,
                "username": profile.getId(),
                "email": profile.email,
                "source": profile.source,
                "department": profile.department,
                "role_in_group": role_in_group,
            }
            data.append(item)
        return data

    @property
    def payload(self):
        workbook = openpyxl.Workbook()
        sheet = workbook.active
        fields = self.fields
        # Header row
        for col_index, field in enumerate(fields, 1):
            sheet.cell(1, col_index, fields[field])

        for row_index, row in enumerate(self.rows, 2):
            for col_index, field in enumerate(fields, 1):
                sheet.cell(row_index, col_index, row.get(field, ""))
        output = BytesIO()
        workbook.save(output)
        return output.getvalue()

    def __call__(self):
        """Return a XLSX file with all members of the group."""
        self.request.response.setHeader(
            "Content-Type",
            "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
        )
        self.request.response.setHeader(
            "Content-Disposition",
            f"attachment; filename=workgroup-{self.context.id}-membership-report.xlsx",
        )
        return self.payload


class WorkGroupMembershipReportCSV(WorkGroupMembershipReportXLSX):
    @property
    def payload(self):
        output = StringIO()
        writer = csv.writer(output, dialect="excel")
        writer.writerow(self.fields)
        writer.writerows([row.values() for row in self.rows])
        return output.getvalue()

    def __call__(self):
        """Return a CSV file with all members of the group."""
        self.request.response.setHeader("Content-Type", "text/csv")
        self.request.response.setHeader(
            "Content-Disposition",
            f"attachment; filename=workgroup-{self.context.id}-membership-report.csv",
        )
        return self.payload


class AllUsersMembershipReportXLSX(WorkGroupMembershipReportXLSX):
    maintainer_profiles = []

    @property
    def fields(self):
        fields = OrderedDict(
            [
                ("first_name", "First Name"),
                ("last_name", "Last Name"),
                ("username", "Username"),
                ("email", "Email"),
                ("source", "Source"),
                ("department", "Department"),
            ]
        )
        return fields

    @property
    def users(self):
        users_container = api.portal.get().get("profiles")
        return list(users_container.objectValues()) if users_container else []


class GroupDeleteConfirmationForm(PIDeleteConfirmationForm):
    @property
    def post_delete_url(self):
        """Go to the contacts app after a group has been deleted"""
        portal = api.portal.get()
        return portal.apps["contacts"].app_url()


@implementer(IBlocksTransformEnabled, IDiazoAppTemplate)
class AllUsersView(GroupBase):
    can_edit = False
    groupid = None
    groupids = []
    title = _("label_all_users", default="All users")

    @property
    def show_more_menu(self):
        return api.user.has_permission(ModifyPortalContent, obj=self.context)

    @property
    @memoize
    def userids(self):
        """This is not actually used. Kept for API compatibility with base class."""
        return [
            user.id
            for user in self.users_container.objectValues()
            if self.filter_by_review_state(user)
        ]

    @property
    @memoize
    def groups(self):
        """The All Users listing has no groups as members"""
        return []

    @property
    @memoize
    def maintainers(self):
        """The All Users listing has no maintainers"""
        return []

    @property
    @memoize
    def maintainer_profiles(self):
        """The All Users listing has no maintainers"""
        return []

    @property
    @memoize
    def maintainer_groups(self):
        """The All Users listing has no maintainers"""
        return []

    @property
    @memoize
    def num_users(self):
        search = getUtility(ISiteSearch)
        filters = {
            "portal_type": [
                "ploneintranet.userprofile.userprofile",
            ],
            "-review_state": self.excluded_review_states,
        }
        response = search.query(
            filters=filters,
            start=0,
            step=0,
        )
        return response.total_results

    @property
    def num_non_functional_principals(self):
        return self.num_users

    def batch(self):
        """"""
        search = getUtility(ISiteSearch)
        filters = {
            "portal_type": [
                "ploneintranet.userprofile.userprofile",
            ],
            "-review_state": self.excluded_review_states,
        }
        response = search.query(
            filters=filters,
            sort="sortable_title",
            start=self.get_start(),
            step=self._batch_size,
        )
        return [result.getObject() for result in response]


class LogoJPGView(BrowserView):
    """Helper view to redirect to the group logo or the default group
    picture."""

    _fallback_url_template = (
        "{}/++theme++ploneintranet.theme/generated/media/icon-group.svg?t={}"
    )
    _url_template = "{}/@@images/image/mini?t={}"

    def get_logo_real_url(self):
        timestamp = self.request.get("t", self.context.modified().strftime("%s"))
        if self.context.image:
            template = self._url_template
            base_url = self.context.absolute_url()
        else:
            template = self._fallback_url_template
            base_url = api.portal.get().absolute_url()
        return template.format(base_url, timestamp)

    def __call__(self):
        return self.request.response.redirect(self.get_logo_real_url())
