"""
Role enforcement for the CMS.

The sidebar hides what a user cannot open, but hiding a link is decoration, not
security. This middleware is the part that actually refuses, so typing the URL
gets a person no further than clicking would have.

It runs on url names, not paths, so a route added later inherits the rules of
the area it belongs to without anyone remembering to decorate it.
"""
from django.contrib import messages
from django.http import JsonResponse
from django.shortcuts import redirect

from apps.accounts.permissions import can_access, is_read_only, section_for_url

SAFE_METHODS = {'GET', 'HEAD', 'OPTIONS', 'TRACE'}


class RolePermissionMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        return self.get_response(request)

    def process_view(self, request, view_func, view_args, view_kwargs):
        match = getattr(request, 'resolver_match', None)
        url_name = getattr(match, 'url_name', None)

        # Only the CMS is governed here. The public site is untouched.
        if not url_name or not url_name.startswith('cms_'):
            return None

        user = getattr(request, 'user', None)
        if not user or not user.is_authenticated:
            return None  # the view decorators send these to the login page

        section = section_for_url(url_name)
        if section is None:
            return None

        # Everyone may open their own profile and their own security settings,
        # even though those pages live in the Users section.
        if self._is_own_account(request, url_name, view_kwargs):
            return None

        if not can_access(user, section):
            return self._refuse(
                request, url_name,
                'You do not have access to that section.',
            )

        # Analysts may look at anything they can reach, but never change it.
        if is_read_only(user) and request.method not in SAFE_METHODS:
            return self._refuse(
                request, url_name,
                'Your role is read only, so that change was not saved.',
            )

        return None

    @staticmethod
    def _is_own_account(request, url_name, view_kwargs):
        if url_name not in {'cms_user_edit', 'cms_user_security'}:
            return False
        target = view_kwargs.get('pk') or view_kwargs.get('user_id')
        try:
            return target is not None and int(target) == request.user.pk
        except (TypeError, ValueError):
            return False

    def _refuse(self, request, url_name, note):
        wants_json = (
            url_name.endswith('_json')
            or request.headers.get('x-requested-with') == 'XMLHttpRequest'
            or 'application/json' in request.headers.get('accept', '')
        )
        if wants_json:
            return JsonResponse({'error': note}, status=403)
        messages.error(request, note)
        return redirect('cms_dashboard')
