from datetime import datetime
from plone import api
from plone.protect.interfaces import IDisableCSRFProtection
from Products.Five.browser import BrowserView
from zope.interface import alsoProvides
from zope.interface import implementer
from zope.publisher.interfaces import IPublishTraverse

import logging

logger = logging.getLogger("ploneintranet.utils.browser.cron")

# in-memory datastructure provides security:
# * if the key is not listed, it's not accessible
# * if the value timestamp is too recent, access is denied
# structure is {URL:timestamp}
ALLOWED_VIEWS = {
    "generate-pdf-for-recent-contents-async": 0,
    "sync-users": 0,
    "sync-user-properties": 0,
}

MIN_DELAY = 5 * 60  # seconds: allow max 1 hit per 5 mins per view


@implementer(IPublishTraverse)
class CronView(BrowserView):
    """Enable anonymous access on protected maintenance views, without
    compromising security.

    How this works:
    - Anon hits <plone_site>/@@cron/sync_users
    - Cron view errors out if `sync_users` is not in `ALLOWED_VIEWS`
    - Cron view errors out if `sync_users` whas hit < MIN_DELAY ago
    - Cron view executes `sync_users` on proper context, hides output,
      updates timestamp for last access.
    """

    view_name = ""

    def publishTraverse(self, request, name):
        self.view_name = name
        # stop traversing, we have arrived
        request["TraversalRequestNameStack"] = []
        # return self so the publisher calls this view
        return self

    def __call__(self):
        # first, security checks
        if not self.view_name:
            msg = "Nothing to traverse"
            logger.warning(msg)
            return msg
        if self.view_name not in ALLOWED_VIEWS:
            msg = f"disallowed {self.view_name}"
            logger.warning(msg)
            return msg
        now = int(datetime.utcnow().strftime("%s"))
        if now < ALLOWED_VIEWS[self.view_name] + MIN_DELAY:
            msg = f"rate limit exceeded for {self.view_name}"
            logger.warning(msg)
            return msg

        # setup the context and request
        view_context = portal = api.portal.get()
        if self.view_name in ("sync-users", "sync-user-properties"):
            view_context = portal.profiles
        view_request = self.request.clone()

        alsoProvides(self.request, IDisableCSRFProtection)

        # execute the maintenance view
        with api.env.adopt_roles(["Authenticated", "Member", "Manager"]):
            try:
                view = api.content.get_view(self.view_name, view_context, view_request)
            except api.exc.InvalidParameterError as error:
                # That we are allowing traversal to the view, doesn't mean
                # it is allowed on the context
                logger.warning(error)
                return f"Could NOT execute {self.view_name}. See server logs."
            view()  # the view itself typically provides lots of logging

        ALLOWED_VIEWS[self.view_name] = now
        logger.info(f"executed {self.view_name} OK")
        return "OK."
