from Acquisition import aq_inner
from Acquisition import aq_parent
from datetime import datetime
from logging import getLogger
from plone import api
from plone.app.content.browser.actions import DeleteConfirmationForm
from plone.base import PloneMessageFactory as __
from plone.memoize.view import memoize
from plone.registry.interfaces import IRegistry
from ploneintranet import api as pi_api
from ploneintranet.core import _
from ploneintranet.layout.browser.base import BaseView
from ploneintranet.layout.interfaces import IDiazoNoTemplate
from ploneintranet.layout.interfaces import IModalPanel
from ploneintranet.notifications.mails.base import BaseEmail
from ploneintranet.search.interfaces import ISiteSearch
from ploneintranet.workspace.interfaces import ITrashable
from ploneintranet.workspace.interfaces import ITrashed
from Products.CMFPlone.browser.login.password_reset import PasswordResetToolView
from Products.CMFPlone.browser.login.password_reset import PasswordResetView
from Products.CMFPlone.PasswordResetTool import ExpiredRequestError
from Products.CMFPlone.PasswordResetTool import InvalidRequestError
from Products.Five.browser import BrowserView
from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
from urllib.parse import urlencode
from z3c.form import button
from zope.component import getUtility
from zope.interface import implementer

logger = getLogger(__name__)


@implementer(IModalPanel)
class PIDeleteConfirmationForm(DeleteConfirmationForm):
    """We need to override this
    because of some problems with the original delete_confirmation form
    See: https://github.com/plone/plone.app.content/issues/38

    Caveat: we need to reimplement all the actions while overriding,
    because of the button.buttonAndHandler implementation
    """

    template = ViewPageTemplateFile("templates/delete_confirmation.pt")

    is_modal_panel = True
    panel_style = "panel"
    panel_size = "medium"
    show_default_cancel_button = False
    title = _("Delete confirmation")

    @property
    @memoize
    def form_action(self):
        """Return the z3c.form action"""
        return self.action

    @property
    @memoize
    def form_data_pat_inject(self):
        """Merge the data inject parts to populate
        the form data-pat-inject attribute
        """
        return self.request.get("pat-inject")

    @memoize
    def is_ajax(self):
        """Check if we have an ajax call"""
        requested_with = self.request.environ.get("HTTP_X_REQUESTED_WITH")
        return requested_with == "XMLHttpRequest"

    def updateActions(self):
        """This method updates the actions to enable some dynamic behaviours

        In particular it understands the request parameters:
         - pat-modal
         - pat-inject

        If pat-modal is truish we add to the action buttons the class
        'close-panel'
        If pat-inject is truish the pat-inject class is added to the form
        and a data attribute data-pat-inject is filled we the value
        read from the request
        """
        super().updateActions()

        self.actions["Delete"].klass = "pat-button default icon-ok-circle"
        if self.request.get("pat-modal"):
            self.actions["Cancel"].klass = "pat-button close-panel icon-cancel-circle"
            self.actions["Cancel"].type = "button"
        else:
            self.actions["Cancel"].klass = "pat-button icon-cancel-circle close-panel"
        if self.form_data_pat_inject:
            self.actions["Delete"].klass += " close-panel"

    def additional_hidden_inputs(self):
        """Additional hidden inputs that may be used to control the form
        behavior (e.g. injection)
        """
        inputs = []
        if "app" in self.request.form:
            inputs.append({"name": "app", "value": "1"})
        return inputs

    @property
    def post_delete_url(self):
        """Go here after an object has been deleted"""
        return aq_parent(aq_inner(self.context)).absolute_url()

    @button.buttonAndHandler(_("I am sure, delete now"), name="Delete")
    def handle_delete(self, action):
        title = self.context.Title()
        parent = aq_parent(aq_inner(self.context))
        # Compute this now before changes to the DB are made
        post_delete_url = self.post_delete_url
        if self.context.aq_chain == self.context.aq_inner.aq_chain:
            parent.manage_delObjects(self.context.getId(), self.request)
            msg = __("${title} has been deleted.", mapping={"title": title})
        else:
            msg = __('"${title}" has already been deleted', mapping={"title": title})
        api.portal.show_message(msg, self.request)
        self.request.response.redirect(post_delete_url)

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


@implementer(IDiazoNoTemplate)
class PIDeleteLink(BaseView):
    """View that renders the delete/trash button"""

    fragment = "#document-content"
    css_class = "pat-modal icon-trash icon close-panel"
    data_pat_modal = "class: panel medium"

    @property
    @memoize
    def title(self):
        if self.is_trashable and not self.is_trashed:
            return _("Move this document to the trash")
        return _("Delete this document permanently")

    @property
    @memoize
    def label(self):
        if self.is_trashable and not self.is_trashed:
            return _("Move to trash")
        return _("Delete this document")

    @property
    @memoize
    def is_trashed(self):
        """Check if the current document is in the trash can"""
        return ITrashed.providedBy(self.context)

    @property
    @memoize
    def is_trashable(self):
        """Check if the current document is a candidate for the trach can"""
        return ITrashable.providedBy(self.context)

    @property
    @memoize
    def delete_link_options(self):
        """Return the link to the delete confirmation panel"""
        options = {}
        if "app" in self.request.form:
            options["app"] = 1
        return options

    @property
    @memoize
    def delete_link(self):
        """Return the link to the delete confirmation panel"""
        return "{url}/@@delete_confirmation?{qs}{fragment}".format(
            url=self.context.absolute_url(),
            qs=urlencode(self.delete_link_options),
            fragment=self.fragment,
        )


class RefreshCompilationsView(BaseView):
    """When called reset the last_compilation time for the static resources"""

    _last_compilation_keys = (
        "plone.bundles/ploneintranet.last_compilation",
        "plone.bundles/ploneintranet-workspace-styles.last_compilation",
    )

    def refresh_compilation(self):
        """This registry record is used to append a timestamp to the URLs
        of the CSS and JS compilation.
        """
        self.disable_csrf_protection()
        now = datetime.now()
        for key in self._last_compilation_keys:
            api.portal.set_registry_record(key, now)

    def __call__(self):
        self.refresh_compilation()
        return "OK"


class PasswordHelper(BrowserView):
    def find_profiles(self, userid):
        if not userid:
            return []
        profile = pi_api.userprofile.get(userid)
        if profile is not None:
            profiles = [profile]
        else:
            search_util = getUtility(ISiteSearch)
            profiles = [
                pi_api.userprofile.get(result.getId)
                for result in search_util.query(
                    filters={
                        "email": userid,
                        "portal_type": "ploneintranet.userprofile.userprofile",
                    },
                    secure=False,
                )
            ]
            profiles = [profile for profile in profiles if profile is not None]
            if not profiles:
                profiles_no_emails = [
                    pi_api.userprofile.get(result.getId)
                    for result in search_util.query(
                        step=5000,
                        filters={
                            "email": "",
                            "portal_type": "ploneintranet.userprofile.userprofile",
                        },
                        secure=False,
                    )
                ]
                profiles = [
                    profile
                    for profile in profiles_no_emails
                    if profile is not None
                    and (getattr(profile, "email", "") or "") == userid
                ]
        return profiles


class MailPassword(BaseView):
    """
    Provide our own view for sending a password reminder e-mail, rather than
    relying on the skin script of CMFPlone/skins/plone_scripts/mail_password.py
    """

    def can_reset(self, user):
        check_view = api.content.get_view(
            "verify_password_reset_allowed", self.context, self.request
        )
        return check_view.is_allowed(user)

    def warn_cannot_reset(self, user):
        email_view = api.content.get_view(
            context=user, request=self.request, name="mail_password_external"
        )
        email_view.send_email()

    def warn_no_account(self):
        email_view = api.content.get_view(
            context=self.context, request=self.request, name="mail_password_not_found"
        )
        try:
            email_view.send_email()
        except UnicodeEncodeError as e:
            logger.info(
                "Could not send mail to %s: %s",
                self.request.get("userid", "").strip(),
                e,
            )

    def __call__(self):
        tool = api.portal.get_tool("portal_registration")
        userid = self.request.get("userid", "").strip()
        helper_view = api.content.get_view(
            context=self.context, request=self.request, name="password-helper"
        )
        profiles = helper_view.find_profiles(userid)

        if len(profiles) == 0:
            logger.warning("Passwordreset cannot find account for userid %s", userid)
        if len(profiles) == 0 and "@" in userid:
            self.warn_no_account()
        else:
            for profile in profiles:
                logger.debug("Trying to reset password for %s", profile.username)
                if not self.can_reset(profile):
                    self.warn_cannot_reset(profile)
                    logger.warning("Password reset is disallowed for %s", userid)
                elif (
                    profile is not None
                    and api.content.get_state(obj=profile) != "enabled"
                ):
                    logger.warning(
                        "Skipping password reset for disabled userprofile %s",
                        profile.username,
                    )
                elif (
                    profile is not None
                    and api.content.get_state(obj=profile) == "enabled"
                ):
                    logger.debug("Sending password reset for %s", profile.username)
                    try:
                        tool.mailPassword(profile.username, self.request)
                        logger.info("Password reset sent for %s", profile.username)
                    except ValueError:
                        # User name could not be found
                        # Swallow this error! We do not want to expose whether a user
                        # account exists or not
                        logger.exception(
                            "Password reset failed. Invalid userid? %s", userid
                        )

        return self.request.RESPONSE.redirect(
            self.context.absolute_url() + "/mail_password_response"
        )


class MailPasswordNotFound(BaseEmail):
    """ """

    translatable_subject = __("mailtemplate_subject_resetpasswordrequest")

    @property
    @memoize
    def recipient(self):
        return self.request.get("userid", "")


class MailPasswordExternal(BaseEmail):
    """ """

    translatable_subject = __("mailtemplate_subject_resetpasswordrequest")

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


class MailPasswordTemplate(PasswordResetToolView):
    def localized_date(self, expires):
        return api.portal.get_localized_time(expires)

    def construct_url(self, randomstring, userid=None):
        return "{root_url}/passwordreset/{randomstring}?userid={userid}".format(
            root_url=self.portal_state().navigation_root_url(),
            randomstring=randomstring,
            userid=userid or self.request.form.get("userid"),
        )


class PIPasswordResetView(PasswordResetView):
    def _reset_password(self, pw_tool, randomstring):
        state = self.getErrors()
        if state:
            return self.form()
        userid = self.request.form.get("userid")
        password = self.request.form.get("password")
        helper_view = api.content.get_view(
            context=self.context, request=self.request, name="password-helper"
        )
        profiles = helper_view.find_profiles(userid)
        if len(profiles) > 1:
            # not dealing with this edge case, bail out
            return self.invalid()
        if len(profiles) == 1:
            userid = profiles[0].username
        try:
            pw_tool.resetPassword(userid, randomstring, password)
        except ExpiredRequestError:
            return self.expired()
        except InvalidRequestError:
            return self.invalid()
        except RuntimeError:
            return self.invalid()
        registry = getUtility(IRegistry)
        if registry.get("plone.autologin_after_password_reset", False):
            return self._auto_login(userid, password)
        return self.finish()
