from functools import cached_property
from logging import getLogger
from plone import api
from plone.app.z3cform.widgets.select import AjaxSelectFieldWidget
from plone.autoform import directives
from plone.dexterity.browser.edit import DefaultEditForm
from plone.dexterity.browser.edit import DefaultEditView
from plone.supermodel import model
from ploneintranet.core import _
from ploneintranet.layout.browser.actions import PIDeleteConfirmationForm
from ploneintranet.layout.browser.base import BasePanel
from ploneintranet.layout.interfaces import IModalPanel
from ploneintranet.layout.widgets.form import PIEditForm
from ploneintranet.layout.widgets.select import PISelectFromOptgroupsFieldWidget
from ploneintranet.userprofile.content.userprofile import IUserProfile
from ploneintranet.userprofile.vocabularies import PrincipalSource
from z3c.form import form
from zope import schema
from zope.interface import implementer

logger = getLogger(__name__)


class PanelImageBankImageView(BasePanel):
    panel_size = "full"
    show_default_cancel_button = False
    form_action = None
    body_child_class = "image-details-social"
    show_button_bar = False

    def get_url(self, data: dict | None) -> str | None:
        """Check the URL and return the appropriate panel image bank URL.

        The data argument comes from the @@plone_nextprevious_view.
        """
        if not data:
            return
        url: str = data.get("url") or ""
        if not url.endswith("/view"):
            return url
        return f"{url.rpartition('/view')[0]}/@@panel-image-bank-image"


class IPanelCreateCollectionViewSchema(model.Schema):
    """Schema for the panel-create-collection view"""

    title = schema.TextLine(title=_("label_name", default="Name"))

    description = schema.Text(
        title=_("label_description", default="Description"), required=False
    )

    directives.widget(
        "maintenance", AjaxSelectFieldWidget, placeholder=_("Select users and groups")
    )
    maintenance = schema.List(
        title=_("label_maintenance", default="Maintenance"),
        description=_("Select the users and groups that can maintain this collection"),
        value_type=schema.Choice(source=PrincipalSource()),
        required=False,
    )

    directives.widget(
        "visibility", AjaxSelectFieldWidget, placeholder=_("Select users and groups")
    )
    visibility = schema.List(
        title=_("label_visibility", default="Visibility"),
        description=_("Select the users and groups that can view this collection"),
        value_type=schema.Choice(source=PrincipalSource()),
        required=False,
    )

    directives.widget(
        "contribution", AjaxSelectFieldWidget, placeholder=_("Select users and groups")
    )
    contribution = schema.List(
        title=_("label_contributors", default="Contributors"),
        description=_(
            "Select the users and groups that can contribute to this collection"
        ),
        value_type=schema.Choice(source=PrincipalSource()),
        required=False,
    )


@implementer(IModalPanel)
class PanelCreateCollectionView(PIEditForm):
    panel_size = "medium"
    show_default_cancel_button = False
    data_pat_inject = (
        "source: #directory-image-bank; target: #directory-image-bank && "
        "source: #image-bank-toolbar; target: #image-bank-toolbar"
    )
    ignoreContext = True
    label = _("label_create_collection", default="Create a collection")
    schema = IPanelCreateCollectionViewSchema

    @cached_property
    def app(self):
        """Return the collections container."""
        return api.portal.get().apps.get("image-bank")

    @cached_property
    def app_view(self):
        app = self.app
        return api.content.get_view(app.lstrip("@"), app, self.request)

    def redirect(self, target=None, msg=None, msg_type="warning"):
        if not target:
            target = self.context

        if not isinstance(target, str):
            context_state = api.content.get_view(
                "plone_context_state", target, self.request
            )
            target = context_state.view_url()

        if msg:
            api.portal.show_message(msg, self.request, msg_type)
        return self.request.response.redirect(target)

    def do_create(self, data: dict):
        """Create the collection object and set the appropriate local roles."""
        obj = api.content.create(
            container=self.context,
            type="Folder",
            title=data.pop("title"),
        )

        # For the other fields we need to adapt the schema to the new object.
        adapted_context = self.schema(obj)
        for key, value in data.items():
            setattr(adapted_context, key, value)
        return obj

    def updateFields(self):
        super().updateFields()
        if IUserProfile.providedBy(self.context):
            self.fields = self.fields.omit("maintenance", "visibility", "contribution")

    @form.button.buttonAndHandler(_("label_create", default="Create"), name="create")
    def handle_create(self, action):
        data, errors = self.extractData()
        if errors:
            self.status = self.formErrorsMessage
            return self.redirect(
                target=self.app.app_url(), msg=self.status, msg_type="error"
            )

        obj = self.do_create(data)
        return self.redirect(
            target=f"{obj.absolute_url()}/@@app-image-bank-collection",
            msg=_("Collection created successfully"),
            msg_type="success",
        )

    @form.button.buttonAndHandler(_("label_cancel", default="Cancel"), name="cancel")
    def handle_cancel(self, action):
        """Cancel button"""
        return self.redirect(target=self.app.app_url())

    def updateActions(self):
        super().updateActions()
        for action in self.actions.values():
            action.addClass("close-panel")


@implementer(IPanelCreateCollectionViewSchema)
class CollectionSchemaAdapter:
    """Adapter to adapt the collection to the schema used in the edit form"""

    _maintenance_roles = {"Editor"}
    _visibility_roles = {"Reader"}
    _contribution_roles = {"Contributor"}

    def __init__(self, context):
        self.context = context

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

    @title.setter
    def title(self, value):
        self.context.title = value

    @property
    def description(self):
        return self.context.description

    @description.setter
    def description(self, value):
        self.context.description = value

    def _get_principal_ids_with_roles(self, roles: set) -> list:
        """Get the principal ids that have the given roles on the collection."""
        local_roles: dict = self.context.__ac_local_roles__
        principal_ids = []
        for principal_id, assigned_roles in local_roles.items():
            if roles.issubset(set(assigned_roles)):
                principal_ids.append(principal_id)
        return sorted(principal_ids)

    def _ensure_only_principal_ids_have_roles(
        self, principal_ids: list | None, roles: set
    ):
        """Ensure that the given principal ids have the given roles on the collection.
        No other principal ids should have these roles on the collection.
        """
        if principal_ids is None:
            principal_ids = []

        local_roles: dict = self.context.__ac_local_roles__

        existing_principal_with_roles = set()
        for principal_id, assigned_roles in local_roles.items():
            if roles.issubset(set(assigned_roles)):
                existing_principal_with_roles.add(principal_id)

        exceeding_principal_ids = existing_principal_with_roles - set(principal_ids)
        missing_principal_ids = set(principal_ids) - existing_principal_with_roles

        for principal_id in exceeding_principal_ids:
            existing_roles = set(local_roles.get(principal_id, []))
            new_roles = existing_roles - roles
            if new_roles:
                self.context.manage_setLocalRoles(principal_id, sorted(new_roles))
            else:
                self.context.manage_delLocalRoles([principal_id])

        for principal_id in missing_principal_ids:
            existing_roles = set(local_roles.get(principal_id, []))
            new_roles = existing_roles | roles
            self.context.manage_setLocalRoles(principal_id, sorted(new_roles))

    @property
    def maintenance(self):
        return self._get_principal_ids_with_roles(self._maintenance_roles)

    @maintenance.setter
    def maintenance(self, value):
        value = value or []
        principal_ids = [token.rpartition(":")[-1] for token in value]
        self._ensure_only_principal_ids_have_roles(
            principal_ids, self._maintenance_roles
        )

    @property
    def visibility(self):
        return self._get_principal_ids_with_roles(self._visibility_roles)

    @visibility.setter
    def visibility(self, value):
        value = value or []
        principal_ids = [token.rpartition(":")[-1] for token in value]
        self._ensure_only_principal_ids_have_roles(
            principal_ids, self._visibility_roles
        )

    @property
    def contribution(self):
        return self._get_principal_ids_with_roles(self._contribution_roles)

    @contribution.setter
    def contribution(self, value):
        value = value or []
        principal_ids = [token.rpartition(":")[-1] for token in value]
        self._ensure_only_principal_ids_have_roles(
            principal_ids, self._contribution_roles
        )


@implementer(IModalPanel)
class PanelEditCollectionView(PIEditForm):
    panel_size = "medium"
    show_default_cancel_button = False
    data_pat_inject = (
        "source: #directory-image-bank; target: #directory-image-bank && "
        "source: #image-bank-toolbar; target: #image-bank-toolbar"
    )
    label = _("label_edit_collection", default="Edit collection")
    schema = IPanelCreateCollectionViewSchema

    def redirect(self, target=None, msg=None, msg_type="warning"):
        if not target:
            target = self.context

        if not isinstance(target, str):
            context_state = api.content.get_view(
                "plone_context_state", target, self.request
            )
            target = context_state.view_url()

        if msg:
            api.portal.show_message(msg, self.request, msg_type)
        return self.request.response.redirect(target)

    def updateFields(self):
        super().updateFields()
        if IUserProfile.providedBy(self.context.aq_parent):
            self.fields = self.fields.omit("maintenance", "visibility", "contribution")

    @form.button.buttonAndHandler(_("label_save", default="Save"), name="save")
    def handle_save(self, action):
        data, errors = self.extractData()
        if errors:
            self.status = self.formErrorsMessage
            return self.redirect(
                target=self.context.absolute_url(), msg=self.status, msg_type="error"
            )

        self.applyChanges(data)
        return self.redirect(
            target=f"{self.context.absolute_url()}/@@app-image-bank-collection",
            msg=_("Collection updated successfully"),
            msg_type="success",
        )

    @form.button.buttonAndHandler(_("label_cancel", default="Cancel"), name="cancel")
    def handle_cancel(self, action):
        """Cancel button"""
        return self.redirect(
            target=f"{self.context.absolute_url()}/@@app-image-bank-collection"
        )

    def updateActions(self):
        super().updateActions()
        for action in self.actions.values():
            action.addClass("close-panel")


class IPanelImageBankTransferSchema(model.Schema):
    """Schema for the panel-image-bank-transfer view"""

    directives.widget("target", PISelectFromOptgroupsFieldWidget)
    target = schema.Choice(
        title=_("label_target_collection", default="Target collection"),
        vocabulary="ploneintranet.imagebank.vocabularies.collections",
        required=True,
    )


class PanelImageBankTransferView(PIEditForm):
    panel_size = "small"
    label = _("label_transfer_image", default="Transfer image")
    description = _(
        "message_select_a_collection_to_transfer_this_image_to",
        default="Select a collection to transfer this image to",
    )
    ignoreContext = True
    schema = IPanelImageBankTransferSchema

    def redirect(self, target=None, msg=None, msg_type="warning"):
        if not target:
            target = self.context

        if not isinstance(target, str):
            context_state = api.content.get_view(
                "plone_context_state", target, self.request
            )
            target = context_state.view_url()

        if msg:
            api.portal.show_message(msg, self.request, msg_type)
        return self.request.response.redirect(target)

    def do_transfer(self, data):
        """Perform the transfer of the image to the target collection."""
        target = api.content.get(UID=data.get("target"))
        new_obj = api.content.move(source=self.context, target=target)
        return new_obj

    @form.button.buttonAndHandler(
        _("label_transfer", default="Transfer"), name="transfer"
    )
    def handle_transfer(self, action):
        data, errors = self.extractData()
        if errors:
            self.status = self.formErrorsMessage
            return self.redirect(msg=self.status, msg_type="error")

        try:
            new_obj = self.do_transfer(data)
        except Exception:
            self.status = _("An error occurred while transferring the image.")
            logger.exception(
                "An error occurred while transferring the image %r to collection with UID %r by user %r",  # noqa: E501
                self.context,
                data.get("target"),
                api.user.get_current().getId(),
            )
            return self.redirect(msg=self.status, msg_type="error")

        return self.redirect(target=new_obj)

    @form.button.buttonAndHandler(_("label_cancel", default="Cancel"), name="cancel")
    def handle_cancel(self, action):
        """Cancel button"""
        return self.redirect()

    def updateActions(self):
        super().updateActions()
        for action in self.actions.values():
            action.addClass("close-panel")
        self.actions["transfer"].addClass("icon-exchange")
        self.actions["transfer"].addClass("default")


class PanelImageBankDeleteConfirmationView(PIDeleteConfirmationForm):

    @property
    def template(self):
        return self.index


class PanelEditImageForm(DefaultEditForm):

    def updateFields(self):
        """Just display a subset of the dexterity fields.
        A full edit form is available in the regular @@edit view.
        """

        super().updateFields()

        self.fields = (
            self.fields.select("title", "description")
            # Tags
            + self.groups[0].fields.select("ICategorization.subjects")
            # Rights
            + self.groups[2].fields.select("IOwnership.rights")
        )
        # Remove the additional fieldsets
        self.groups = []

    def nextURL(self):
        return f"{self.context.absolute_url()}/@@panel-image-bank-image"


@implementer(IModalPanel)
class PanelEditImageView(DefaultEditView):
    form = PanelEditImageForm
    form_data_pat_inject = (
        "source: #image-details-social-sidebar; target: #image-details-social-sidebar"
    )
