"""
Modified version of django.contrib.auth module (https://docs.djangoproject.com/en/2.0/_modules/django/contrib/auth/).
"""
from django.apps import apps as django_apps
from django.conf import settings
from django.contrib.auth import _get_backends, load_backend
from django.contrib.auth.signals import user_logged_in
from django.middleware.csrf import rotate_token
from django.utils.crypto import constant_time_compare

BACKEND_SESSION_KEY = '_auth_user_backend'
CLASS_SESSION_KEY = '_auth_user_class'
HASH_SESSION_KEY = '_auth_user_hash'
SESSION_KEY = '_auth_user_id'

def get_user_model(user_class):
    """
    Return the user model (User/Admin from usermerge.models - see models.py) that is active in the session.
    """
    # user_class is either 'usermerge.User' or 'usermerge.Admin' (the format should necessarily be 'app_label.model_name').
    return django_apps.get_model(user_class, require_ready = False)

def _get_user_session_key(request):
    # This value in the session is always serialized to a string, so we need
    # to convert it back to Python whenever we access it.
    return get_user_model(request.session[CLASS_SESSION_KEY])._meta.pk.to_python(request.session[SESSION_KEY])

def login(request, user, backend = None):
    """
    Persist a user id and a backend in the request. This way a user doesn't
    have to reauthenticate on every request. Note that data set during
    the anonymous session is retained when the user logs in.
    """
    session_auth_hash = ''
    if user is None:
        user = request.user
    if hasattr(user, 'get_session_auth_hash'):
        session_auth_hash = user.get_session_auth_hash()
    if SESSION_KEY in request.session:
        if _get_user_session_key(request) != user.pk or (
                session_auth_hash and
                not constant_time_compare(request.session.get(HASH_SESSION_KEY, ''), session_auth_hash)):
            # To avoid reusing another user's session, create a new, empty
            # session if the existing session corresponds to a different
            # authenticated user.
            request.session.flush()
    else:
        request.session.cycle_key()
    try:
        backend = backend or user.backend
    except AttributeError:
        backends = _get_backends(return_tuples = True)
        if len(backends) == 1:
            _, backend = backends[0]
        else:
            raise ValueError(
                'You have multiple authentication backends configured and '
                'therefore must provide the `backend` argument or set the '
                '`backend` attribute on the user.'
            )
    request.session[SESSION_KEY] = user._meta.pk.value_to_string(user)
    # The value of CLASS_SESSION_KEY is needed to clarify if the authenticated user
    # is a User ('usermerge.User') or an Admin ('usermerge.Admin').
    request.session[CLASS_SESSION_KEY] = str(user.__class__).replace('.models', '').split("'")[1]
    request.session[BACKEND_SESSION_KEY] = backend
    request.session[HASH_SESSION_KEY] = session_auth_hash
    if hasattr(request, 'user'):
        request.user = user
    rotate_token(request)
    user_logged_in.send(sender = user.__class__, request = request, user = user)

def get_user(request):
    """
    Return the user model instance associated with the given request session.
    If no user is retrieved, return an instance of `AnonymousUser`.
    """
    from django.contrib.auth.models import AnonymousUser
    user = None
    try:
        user_id = _get_user_session_key(request)
        backend_path = request.session[BACKEND_SESSION_KEY]
    except KeyError:
        pass
    else:
        if backend_path in settings.AUTHENTICATION_BACKENDS:
            backend = load_backend(backend_path)
            user = backend.get_user(request.session[CLASS_SESSION_KEY], user_id)
            # Verify the session.
            if hasattr(user, 'get_session_auth_hash'):
                session_hash = request.session.get(HASH_SESSION_KEY)
                session_hash_verified = session_hash and constant_time_compare(
                    session_hash,
                    user.get_session_auth_hash()
                )
                if not session_hash_verified:
                    request.session.flush()
                    user = None
    return user or AnonymousUser()