# ##############################################################################
#  This file is part of df_config                                              #
#                                                                              #
#  Copyright (C) 2020 Matthieu Gallet <df_config@19pouces.net>                    #
#  All Rights Reserved                                                         #
#                                                                              #
#  You may use, distribute and modify this code under the                      #
#  terms of the (BSD-like) CeCILL-B license.                                   #
#                                                                              #
#  You should have received a copy of the CeCILL-B license with                #
#  this file. If not, please visit:                                            #
#  https://cecill.info/licences/Licence_CeCILL-B_V1-en.txt (English)           #
#  or https://cecill.info/licences/Licence_CeCILL-B_V1-fr.txt (French)         #
#                                                                              #
# ##############################################################################
import base64
import binascii
import logging
from functools import lru_cache

from django.conf import settings
from django.contrib import auth
from django.contrib.auth.middleware import (
    RemoteUserMiddleware as BaseRemoteUserMiddleware,
)
from django.core.exceptions import ImproperlyConfigured
from django.http import HttpRequest
from django.utils.functional import cached_property

logger = logging.getLogger("django.request")


class DFConfigMiddleware(BaseRemoteUserMiddleware):
    """Like :class:`django.contrib.auth.middleware.RemoteUserMiddleware` but:

    * can use any header defined by the setting `DF_REMOTE_USER_HEADER`,
    * handle the HTTP_X_FORWARDED_FOR HTTP header (set the right client IP)
    * handle HTTP basic authentication
    * set response header for Internet Explorer (to use its most recent render engine)
    """

    @lru_cache()
    def get_remoteuser_header(self):
        # avoid cached_property to ease unittests
        header = settings.DF_REMOTE_USER_HEADER
        if header:
            header = header.upper().replace("-", "_")
        return header

    def process_request(self, request: HttpRequest):
        request.remote_username = None

        if settings.USE_X_FORWARDED_FOR and "HTTP_X_FORWARDED_FOR" in request.META:
            request.META["REMOTE_ADDR"] = (
                request.META["HTTP_X_FORWARDED_FOR"].split(",")[0].strip()
            )

        if settings.USE_HTTP_BASIC_AUTH and "HTTP_AUTHORIZATION" in request.META:
            authentication = request.META["HTTP_AUTHORIZATION"]
            authmeth, sep, auth_data = authentication.partition(" ")
            if sep == " " and authmeth.lower() == "basic":
                try:
                    auth_data = base64.b64decode(auth_data.strip()).decode("utf-8")
                except binascii.Error:
                    auth_data = ""
                except UnicodeDecodeError:
                    auth_data = ""
                username, sep, password = auth_data.partition(":")
                if sep == ":":
                    user = auth.authenticate(username=username, password=password)
                    if user:
                        request.user = user
                        auth.login(request, user)
        username = getattr(settings, "DF_FAKE_AUTHENTICATION_USERNAME", None)

        header = self.get_remoteuser_header()
        if header and username and settings.DEBUG:
            remote_addr = request.META.get("REMOTE_ADDR")
            if remote_addr in settings.INTERNAL_IPS:
                request.META[header] = username
            elif remote_addr:
                logger.warning(
                    "Unable to use `settings.DF_FAKE_AUTHENTICATION_USERNAME`. "
                    "You should add %s to the list `settings.INTERNAL_IPS`."
                    % remote_addr
                )

        if header and header in request.META:
            remote_username = request.META.get(header)
            if (
                not remote_username or remote_username == "(null)"
            ):  # special case due to apache2+auth_mod_kerb :-(
                return
            remote_username = self.format_remote_username(remote_username)
            # noinspection PyTypeChecker
            self.remote_user_authentication(request, remote_username)

    # noinspection PyUnusedLocal,PyMethodMayBeStatic
    def process_response(self, request, response):
        response["X-UA-Compatible"] = "IE=edge,chrome=1"
        return response

    def remote_user_authentication(self, request, username):
        # AuthenticationMiddleware is required so that request.user exists.
        # noinspection PyTypeChecker
        if not hasattr(request, "user"):
            raise ImproperlyConfigured(
                "The Django remote user auth middleware requires the"
                " authentication middleware to be installed.  Edit your"
                " MIDDLEWARE_CLASSES setting to insert"
                " 'django.contrib.auth.middleware.AuthenticationMiddleware'"
                " before the RemoteUserMiddleware class."
            )
        # If the user is already authenticated and that user is the user we are
        # getting passed in the headers, then the correct user is already
        # persisted in the session and we don't need to continue.
        if request.user.is_authenticated:
            cleaned_username = self.clean_username(username, request)
            if request.user.get_username() == cleaned_username:
                request.remote_username = cleaned_username
                return
            else:
                self._remove_invalid_user(request)
        # We are seeing this user for the first time in this session, attempt
        # to authenticate the user.
        user = auth.authenticate(remote_user=username)
        if user:
            # User is valid.  Set request.user and persist user in the session
            # by logging the user in.
            request.user = user
            auth.login(request, user)
            request.remote_username = user.username

    # noinspection PyMethodMayBeStatic
    def format_remote_username(self, remote_username):
        return remote_username.partition("@")[0]
