# Copyright (c) 2010 Agendaless Consulting and Contributors.
# (http://www.agendaless.com), All Rights Reserved
# License: BSD-derived (http://www.repoze.org/LICENSE.txt)
# With Patches from RhodeCode GmBH

import os

from beaker import cache
from beaker.session import SessionObject, Session
from beaker.util import coerce_cache_params
from beaker.util import coerce_session_params

from pyramid.interfaces import ISession
from pyramid.settings import asbool
from zope.interface import implementer

from binascii import hexlify


class CustomSession(Session):
    pass


def BeakerSessionFactoryConfig(**options):
    """Return a Pyramid session factory using Beaker session settings
    supplied directly as ``**options``"""

    class PyramidBeakerSessionObject(SessionObject):
        _options = options
        _cookie_on_exception = _options.pop("cookie_on_exception", True)
        _constant_csrf_token = _options.pop("constant_csrf_token", False)
        _sa_opts = _options.pop("sa_opts", {})

        def __init__(self, request):
            self._options["session_class"] = CustomSession
            self._options["sa_opts"] = self._sa_opts
            SessionObject.__init__(self, request.environ, **self._options)

            def session_callback(_request, _response):
                exception = getattr(_request, "exception", None)
                file_response = getattr(_request, "_file_response", None)
                api_call = getattr(_request, "rpc_method", None)

                if file_response is not None:
                    return
                if api_call is not None:
                    return

                if exception is not None and not self._cookie_on_exception:
                    return

                if self.accessed():
                    self.persist()
                    headers = self.__dict__["_headers"]
                    if headers.get("set_cookie") and headers.get("cookie_out"):
                        _response.headerlist.append(("Set-Cookie", headers["cookie_out"]))

            request.add_response_callback(session_callback)

        # ISession API

        @property
        def id(self):
            # this is as inspected in SessionObject.__init__
            if self.__dict__["_params"].get("type") != "cookie":
                return self._session().id
            return None

        @property
        def new(self):
            return self.last_accessed is None

        changed = SessionObject.save

        # modifying dictionary methods

        @call_save
        def clear(self):
            return self._session().clear()

        @call_save
        def update(self, d, **kw):
            return self._session().update(d, **kw)

        @call_save
        def setdefault(self, k, d=None):
            return self._session().setdefault(k, d)

        @call_save
        def pop(self, k, d=None):
            return self._session().pop(k, d)

        @call_save
        def popitem(self):
            return self._session().popitem()

        __setitem__ = call_save(SessionObject.__setitem__)
        __delitem__ = call_save(SessionObject.__delitem__)

        # Flash API methods
        def flash(self, msg, queue="", allow_duplicate=True):
            storage = self.setdefault(f"_f_{queue}", [])
            if allow_duplicate or (msg not in storage):
                storage.append(msg)

        def pop_flash(self, queue=""):
            storage = self.pop(f"_f_{queue}", [])
            return storage

        def peek_flash(self, queue=""):
            storage = self.get(f"_f_{queue}", [])
            return storage

        # CSRF API methods
        def new_csrf_token(self):
            token = self._constant_csrf_token or hexlify(os.urandom(20)).decode("ascii")
            self["_csrft_"] = token
            return token

        def get_csrf_token(self):
            token = self.get("_csrft_", None)
            if token is None:
                token = self.new_csrf_token()
            return token

    return implementer(ISession)(PyramidBeakerSessionObject)


def call_save(wrapped):
    """By default, in non-auto-mode beaker badly wants people to
    call save even though it should know something has changed when
    a mutating method is called.  This hack should be removed if
    Beaker ever starts to do this by default."""

    def save(session, *arg, **kw):
        value = wrapped(session, *arg, **kw)
        session.save()
        return value

    save.__doc__ = wrapped.__doc__
    return save


def session_factory_from_settings(settings):
    """Return a Pyramid session factory using Beaker session settings
    supplied from a Paste configuration file"""
    prefixes = ("session.", "beaker.session.")
    options = {}

    # custom gather of our specific sqlalchemy session db configuration we need to translate this into a single entry
    # dict because this is how beaker expects that.
    sa_opts = {}

    # Pull out any config args meant for beaker session. if there are any
    for k, v in settings.items():
        for prefix in prefixes:
            if k.startswith(prefix):
                option_name = k[len(prefix) :]
                if option_name == "cookie_on_exception":
                    v = asbool(v)
                if option_name.startswith("sa."):
                    sa_opts[option_name] = v
                options[option_name] = v

    options = coerce_session_params(options)
    options["sa_opts"] = sa_opts
    return BeakerSessionFactoryConfig(**options)


def includeme(config):
    session_factory = session_factory_from_settings(config.registry.settings)
    config.set_session_factory(session_factory)
