# Calendar.social
# Copyright (C) 2018 Gergely Polonkai
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.

"""Caching functionality for Calendar.social
"""

from datetime import timedelta
import pickle
from uuid import uuid4

from flask import has_request_context, request as flask_request, session as flask_session
from flask.sessions import SessionInterface, SessionMixin
from flask_caching import Cache
from werkzeug.datastructures import CallbackDict

cache = Cache()  # pylint: disable=invalid-name


class CachedSession(CallbackDict, SessionMixin):  # pylint: disable=too-many-ancestors
    """Object for session data saved in the cache
    """

    def __init__(self, initial=None, sid=None, new=False):
        self.__modifying = False

        def on_update(self):
            """Function to call when session data is updated
            """

            if self.__modifying:
                return

            self.__modifying = True

            if has_request_context():
                self['ip'] = flask_request.remote_addr

            self.modified = True

            self.__modifying = False

        CallbackDict.__init__(self, initial, on_update)
        self.sid = sid
        self.new = new
        self.modified = False

    @property
    def user(self):
        """The user this session belongs to
        """

        from calsocial.models import User

        if 'user_id' not in self:
            return None

        return User.query.get(self['user_id'])


class CachedSessionInterface(SessionInterface):
    """A session interface that loads/saves session data from the cache
    """

    serializer = pickle
    session_class = CachedSession
    global_cache = cache

    def __init__(self, prefix='session:'):
        self.cache = cache
        self.prefix = prefix

    @staticmethod
    def generate_sid():
        """Generade a new session ID
        """

        return str(uuid4())

    @staticmethod
    def get_cache_expiration_time(app, sess):
        """Get the expiration time of the cache entry
        """

        if sess.permanent:
            return app.permanent_session_lifetime

        return timedelta(days=1)

    def open_session(self, app, request):
        sid = request.cookies.get(app.session_cookie_name)

        if not sid:
            sid = self.generate_sid()

            return self.session_class(sid=sid, new=True)

        session = self.load_session(sid)

        if session is None:
            return self.session_class(sid=sid, new=True)

        return session

    def load_session(self, sid):
        """Load a specific session from the cache
        """

        val = self.cache.get(self.prefix + sid)

        if val is None:
            return None

        data = self.serializer.loads(val)

        return self.session_class(data, sid=sid)

    def save_session(self, app, session, response):
        domain = self.get_cookie_domain(app)

        if not session:
            self.cache.delete(self.prefix + session.sid)

            if session.modified:
                response.delete_cookie(app.session_cookie_name, domain=domain)

            return

        cache_exp = self.get_cache_expiration_time(app, session)
        cookie_exp = self.get_expiration_time(app, session)
        val = self.serializer.dumps(dict(session))
        self.cache.set(self.prefix + session.sid, val, int(cache_exp.total_seconds()))

        response.set_cookie(app.session_cookie_name,
                            session.sid,
                            expires=cookie_exp,
                            httponly=True,
                            domain=domain)

    def delete_session(self, sid):
        """Delete the session with ``sid`` as its session ID
        """

        if has_request_context() and flask_session.sid == sid:
            raise ValueError('Will not delete the current session')

        cache.delete(self.prefix + sid)