from BTrees.OOBTree import OOBTree
from datetime import datetime
from datetime import timedelta
from plone import api
from plone.protect import CheckAuthenticator
from plone.protect import PostOnly
from ploneintranet.core import _
from ploneintranet.invitations.interfaces import ITokenUtility
from Products.CMFCore.interfaces import ISiteRoot
from Products.Five import BrowserView
from zope.annotation import IAnnotations
from zope.component import getUtility
from zope.globalrequest import getRequest

ANNOTATION_KEY = "ploneintranet.invitations.invitation_storage"


class InviteUser(BrowserView):
    """
    Control panel view for sending invitation emails to
    potential new users of the Plone site

    THIS IS EXAMPLE CODE AND NEEDS ATTENTION.
    """

    def __call__(self):
        email = self.request.form.get("email")
        if email is not None:
            CheckAuthenticator(self.request)
            PostOnly(self.request)
            self.invite_user(email)

        return self.index()

    def invite_user(self, email):
        """
        Get new token, build email and send it to the given email address

        :param email: Email address to send invitation to, and for new user
        :type email: str
        :return: Tuple of token id and token url
        """
        # TODO - check that this email address is not already registered
        # TODO - Body and subject to become content
        portal = api.portal.get()
        token_util = getUtility(ITokenUtility)
        days = 3
        valid_until, expire_seconds = _ceil_datetime(days=days)
        token_id, token_url = token_util.generate_new_token(
            expire_seconds=expire_seconds
        )
        _store_invite(token_id, email)
        proto = api.content.get_view("proto", portal, self.request)
        message = """You've been invited to join %s.

The following is a unique URL tied to your email address (%s).
Clicking the link will create you an account and log in you automatically.

%s

This token will be valid %d days and expire on %s.

Once your account has been created, you can also set up a password
to access the site here:

%s/mail_password_form?userid=%s
""" % (
            proto.site_title,
            email,
            token_url,
            days,
            valid_until,
            portal.absolute_url(),
            email,
        )
        api.portal.send_email(
            recipient=email,
            subject="Your invitation to %s" % proto.site_title,
            body=message,
        )

        api.portal.show_message(_("Invitation sent to %s") % email, self.request)
        return token_id, token_url


def accept_invitation(event):
    """
    Event handler for :class:`AcceptToken` event fired by invitation framework

    FIXME #1047: this creates a acl user, not a ploneintranet userprofile

    Additionally, this code makes the heroic assumption that
    email == username == userid which is very brittle, see #1043.

    :param event: The event object
    :type event: :class:`AcceptToken`
    :return:
    """
    request = getRequest()
    email = _get_storage().get(event.token_id)
    if email is None:
        api.portal.show_message(_("The token has expired."), request)
        return
    acl_users = api.portal.get_tool("acl_users")
    if api.user.get(username=email) is None:
        api.user.create(email=email, username=email)
    with api.env.adopt_user(username=email):
        acl_users.updateCredentials(request, request.response, email, None)
        membership_tool = api.portal.get_tool("portal_membership")
        membership_tool.loginUser(request)


def _get_storage(clear=False):
    portal = getUtility(ISiteRoot)
    annotations = IAnnotations(portal)
    if ANNOTATION_KEY not in annotations or clear:
        annotations[ANNOTATION_KEY] = OOBTree()
    return annotations[ANNOTATION_KEY]


def _store_invite(token_id, email):
    storage = _get_storage()
    storage[token_id] = email


def _ceil_datetime(days=3, now=datetime.now()):
    """
    Helper method to ceil the datetime to a quarter of a hour and calculate
    the total_seconds until that moment.

    :param days: the amount of days from now.
    :type days: int
    :param now: The current moment. This is a parameter so we can test.
    :type now: datetime
    :return: (datetime and int)
    """
    valid_until = now + timedelta(days=days)
    valid_until += timedelta(minutes=15)
    valid_until.replace(minute=valid_until.minute % 15).replace(second=0).replace(
        microsecond=0
    )
    expire_seconds = (valid_until - now).total_seconds()
    return valid_until, expire_seconds
