from functools import partial

import django_otp
from django.conf import settings
from django.contrib.auth.views import redirect_to_login
from django.urls import NoReverseMatch, reverse
from django.utils.functional import SimpleLazyObject
from django_otp.middleware import OTPMiddleware as _OTPMiddleware


class VerifyUserMiddleware(_OTPMiddleware):
    _allowed_url_names = [
        "wagtail_2fa_auth",
        "wagtailadmin_login",
        "wagtailadmin_logout",
    ]

    # These URLs do not require verification if the user has no devices
    _allowed_url_names_no_device = [
        "wagtail_2fa_device_list",
        "wagtail_2fa_device_new",
        "wagtail_2fa_device_qrcode",
    ]

    def __call__(self, request):
        if hasattr(self, 'process_request'):
            response = self.process_request(request)
        if not response:
            response = self.get_response(request)
        if hasattr(self, 'process_response'):
            response = self.process_response(request, response)
        return response

    def process_request(self, request):
        if request.user:
            request.user = SimpleLazyObject(partial(self._verify_user, request, request.user))
        user = request.user
        if self._require_verified_user(request):
            user_has_device = django_otp.user_has_device(user, confirmed=True)

            if user_has_device and not user.is_verified():
                return redirect_to_login(
                    request.get_full_path(), login_url=reverse("wagtail_2fa_auth")
                )

            elif not user_has_device and settings.WAGTAIL_2FA_REQUIRED:
                # only allow the user to visit the admin index page and the
                # admin setup page
                return redirect_to_login(
                    request.get_full_path(), login_url=reverse("wagtail_2fa_device_new")
                )

    def _require_verified_user(self, request):
        user = request.user

        if not settings.WAGTAIL_2FA_REQUIRED:
            # If two factor authentication is disabled in the settings
            return False

        if not user.is_authenticated:
            return False

        # If the user has no access to the admin anyway then don't require a
        # verified user here
        if not (
            user.is_staff
            or user.is_superuser
            or user.has_perms(["wagtailadmin.access_admin"])
        ):
            return False

        # Don't require verification for specified paths
        if request.path in self._get_paths(self._allowed_url_names):
            return False

        # If the user does not have a device, don't require verification
        # for the specified paths
        allowed_no_device_paths = self._get_paths(self._allowed_url_names_no_device)
        if request.path in allowed_no_device_paths:
            user_has_device = django_otp.user_has_device(user, confirmed=True)
            if not user_has_device:
                return False

        # For all other cases require that the user is verfied via otp
        return True

    def _get_paths(self, route_names):
        results = []
        for route_name in route_names:
            try:
                results.append(settings.WAGTAIL_MOUNT_PATH + reverse(route_name))
            except NoReverseMatch:
                pass
        return results


class VerifyUserPermissionsMiddleware(VerifyUserMiddleware):
    """A variant of VerifyUserMiddleware which makes 2FA optional."""

    def process_request(self, request):
        result = super().process_request(request)

        # Add an attribute to the user so we can easily determine if 2FA should
        # be enabled for them.
        request.user.enable_2fa = request.user.has_perms(["wagtailadmin.enable_2fa"])

        return result

    def _require_verified_user(self, request):
        result = super()._require_verified_user(request)

        # Always require verification if the user has a device, even if they have
        # 2FA disabled.
        user_has_device = django_otp.user_has_device(request.user, confirmed=True)
        if not user_has_device and not request.user.has_perms(["wagtailadmin.enable_2fa"]):
            return False

        return result