#

from typing import Union

from django.conf import settings
from django.contrib import messages
from django.contrib.auth.models import AnonymousUser, User
from django.contrib.auth.views import redirect_to_login as dj_redirect_to_login
from django.core.exceptions import ImproperlyConfigured, PermissionDenied
from django.db import models
from django.db.models import QuerySet
from django.http import Http404, HttpResponseRedirect
from django.utils.translation import gettext
from rolepermissions.utils import user_is_authenticated

from customrolepermissions.permissions import P, PermissionGroup, has_permission_exactly


class RequiredPermissionMixinMetaClass(type):  # currently not used because of metaclass conflict with FilterView
    def __new__(mcs, name: str, bases: tuple, attrs: dict):
        errors = []
        required_permission__ = attrs.get('required_permission__', '')
        if not isinstance(required_permission__, str):
            type_name = type(required_permission__).__name__
            errors.append(f'{name}.required_permission__ must be str only. given `{type_name}`')
        for sec in ['all', 'rel', 'own', 'ext']:
            required_permission__sec = attrs.get(f'required_permission__{sec}', '')
            if not isinstance(required_permission__sec, (str, P, PermissionGroup, property)):
                type_name = type(required_permission__sec).__name__
                errors.append(
                    f'{name}.required_permission__{sec}'
                    f' must be (str, P, PermissionGroup) only. given `{type_name}`'
                )
        if errors:
            raise ImproperlyConfigured(errors)
        # else:
        #   ensure that at class creation the following statements are true
        #     required_permission__ is instance of str
        #     required_permission__* are instance of (str, P, PermissionGroup, property)
        new_cls = super().__new__(mcs, name, bases, attrs)
        return new_cls


class RequiredPermissionMixin(object):  # (metaclass=RequiredPermissionMixinMetaClass):
    # required permissions to access {own|all|rel} restricted object.
    # if you want to define other than the default once you could set them to full permission name
    # or `False` to prevent access it.
    #
    # NOTE: `required_permission__` is not used directly.
    required_permission__ = ''
    required_permission__ext = ''
    model = None
    auto_related_to = True

    def _required_permission__all(self, *, required_permission__: str = '') -> str:
        required_permission__ = self.required_permission__ if required_permission__ == '' else required_permission__
        return '{}__all'.format(required_permission__) if isinstance(required_permission__, str) else ''

    def _required_permission__rel(self, *, required_permission__: str = '') -> str:
        required_permission__ = self.required_permission__ if required_permission__ == '' else required_permission__
        return '{}__rel'.format(required_permission__) if isinstance(required_permission__, str) else ''

    def _required_permission__own(self, *, required_permission__: str = '') -> str:
        required_permission__ = self.required_permission__ if required_permission__ == '' else required_permission__
        return '{}__own'.format(required_permission__) if isinstance(required_permission__, str) else ''

    required_permission__all = property(_required_permission__all)
    required_permission__rel = property(_required_permission__rel)
    required_permission__own = property(_required_permission__own)

    #
    # Now alter the queryset of main model to only return related queryset based on user permissions
    #
    def super__get_queryset(self) -> QuerySet:
        try:
            queryset = super().get_queryset
        except AttributeError:
            if self.model:
                queryset = self.model._meta.default_manager.all()
            else:
                raise
        else:
            queryset = queryset()

        # if self.auto_related_to and hasattr(queryset, 'related_to'):
        #     queryset = queryset.related_to(self.get_request_user())

        return queryset

    def get_queryset(self) -> QuerySet:
        queryset = self.super__get_queryset()
        request_user = self.get_request_user()

        if has_permission_exactly(request_user, self.required_permission__all):
            return self.get_queryset__all(queryset)
        elif has_permission_exactly(request_user, self.required_permission__rel):
            return self.get_queryset__rel(queryset)
        elif has_permission_exactly(request_user, self.required_permission__own):
            return self.get_queryset__own(queryset)
        else:
            return self.get_queryset__none(queryset)

    def get_queryset__all(self, queryset):
        return queryset

    def get_queryset__rel(self, queryset):
        if not hasattr(queryset, 'related_to'):
            queryset = queryset.none()
        return queryset.related_to(self.get_request_user())

    def get_queryset__own(self, queryset):
        return queryset.none()

    def get_queryset__none(self, queryset):
        return queryset.none()

    def is_permitted(
        self,
        *,
        permission__: str = '',
        permission__all: Union[str, P, PermissionGroup] = '',
        permission__rel: Union[str, P, PermissionGroup] = '',
        permission__own: Union[str, P, PermissionGroup] = '',
        is_rel_func=None,
        is_own_func=None,
    ):
        assert not (  # nosec
            permission__ and any((permission__all, permission__rel, permission__own))  #
        ), 'dont use permission__ with permission__{all,rel,own}'

        request_user = self.get_request_user()  # type: User

        if permission__all == '':
            if permission__ == '':
                permission__all = self.required_permission__all
            else:
                permission__all = self._required_permission__all(required_permission__=permission__)

        permitted__all = (
            permission__all != ''  #
            and has_permission_exactly(request_user, permission__all)
        )

        if permission__rel == '':
            if permission__ == '':
                permission__rel = self.required_permission__rel
            else:
                permission__rel = self._required_permission__rel(required_permission__=permission__)

        permitted__rel = (
            permission__rel != ''  #
            and has_permission_exactly(request_user, permission__rel)
            and (is_rel_func() if is_rel_func else self.is_rel())
        )

        if permission__own == '':
            if permission__ == '':
                permission__own = self.required_permission__own
            else:
                permission__own = self._required_permission__own(required_permission__=permission__)

        permitted__own = (
            permission__own != ''  #
            and has_permission_exactly(request_user, permission__own)
            and (is_own_func() if is_own_func else self.is_own())
        )

        permitted = permitted__all or permitted__rel or permitted__own

        # checking againest PermissionGroup or intersection with ALL_PERMISSIONS was good at first,
        # but it have inconsistent behaviour with get_queryset and lead to complex and implicit flow.
        #
        # if isinstance(self.required_permission__, PermissionGroup):
        #     permission_group = self.required_permission__
        #     permitted = permission_group(request_user)
        #
        # elif not any(ALL_PERMISSIONS.intersection((permission__all, permission__rel, permission__own))):
        #     # maybe it is a simple permission and not permission__
        #     permission = self.required_permission__ if permission__ == '' else permission__
        #     permitted = (
        #         permission
        #         and has_permission_exactly(request_user, permission)
        #     )

        if self.required_permission__ext != '':
            permitted = permitted and has_permission_exactly(request_user, self.required_permission__ext)

        # for debug
        # print(
        #     f'### {request_user} \n'
        #     f'### {permission__all} {permission__rel} {permission__own} \n'
        #     f'### {permitted__all} {permitted__rel} {permitted__own} \n'
        # )

        return permitted

    def get_request_user(self):
        try:
            return self.request.user
        except AttributeError:
            return AnonymousUser()

    def is_rel(self):
        """ check if current queryset or object is related to request_user or not """
        return False

    def is_own(self):
        """ check if current queryset or object is owned by request_user or not """
        return False


class RequiredPermissionViewMixin(RequiredPermissionMixin):
    kwargs = None
    request = None

    redirect_to_login = None

    def dispatch(self, request, *args, **kwargs):
        request_user = self.get_request_user()
        if user_is_authenticated(request_user):
            if self.is_permitted():
                return super().dispatch(request, *args, **kwargs)
            else:
                raise PermissionDenied

        redirect = self.redirect_to_login
        if redirect is None:
            redirect = getattr(settings, 'ROLEPERMISSIONS_REDIRECT_TO_LOGIN', False)
        if redirect:
            return dj_redirect_to_login(request.get_full_path())
        raise PermissionDenied

    def get_request_user(self):
        return self.request.user


class RedirectRequiredPermissionViewMixin(RequiredPermissionMixin):
    kwargs = None
    request = None

    redirect_to_login = None
    redirect_to_url = None
    redirect_to_url_message = None

    def dispatch(self, request, *args, **kwargs):
        request_user = self.get_request_user()
        if user_is_authenticated(request_user):
            if self.is_permitted():
                return super().dispatch(request, *args, **kwargs)
            elif self.redirect_to_url:
                if self.redirect_to_url_message:
                    messages.add_message(self.request, messages.INFO, self.redirect_to_url_message)
                return HttpResponseRedirect(self.redirect_to_url)
            else:
                raise PermissionDenied

        redirect = self.redirect_to_login
        if redirect is None:
            redirect = getattr(settings, 'ROLEPERMISSIONS_REDIRECT_TO_LOGIN', False)
        if redirect:
            return dj_redirect_to_login(request.get_full_path())
        raise PermissionDenied

    def get_request_user(self):
        return self.request.user


class RestrictedSingleObjectMixin(RequiredPermissionViewMixin):
    restricted_object = None
    restricted_model = None
    restricted_queryset = None
    restricted_slug_field = 'slug'
    restricted_context_object_name = None
    restricted_slug_url_kwarg = 'slug'
    restricted_pk_url_kwarg = 'pk'
    restricted_query_pk_and_slug = False
    _is_own = False

    def is_permitted(self):
        self.restricted_object = self.get_restricted_object()  # shortcut
        return super().is_permitted()

    def get_restricted_object(self, restricted_queryset=None):
        """
        Return the object the view is displaying.

        Require `self.restricted_queryset` and a `pk` or `slug` argument in the URLconf.
        Subclasses can override this to return any object.
        """
        self._is_own = False

        # Use a custom restricted_queryset if provided; this is required for subclasses
        # like DateDetailView
        if restricted_queryset is None:
            restricted_queryset = self.get_restricted_queryset()

        # Next, try looking up by primary key.
        pk = self.kwargs.get(self.restricted_pk_url_kwarg)
        slug = self.kwargs.get(self.restricted_slug_url_kwarg)
        if pk is not None:
            restricted_queryset = restricted_queryset.filter(pk=pk)

        # Next, try looking up by slug.
        if slug is not None and (pk is None or self.restricted_query_pk_and_slug):
            slug_field = self.get_restricted_slug_field()
            restricted_queryset = restricted_queryset.filter(**{slug_field: slug})

        # If none of those are defined, it's an error.
        if pk is None and slug is None:
            if self.required_permission__own != '':
                obj = self.get_own_restricted_object()
                self._is_own = True
                return obj
            else:
                raise AttributeError(
                    'Generic detail view %s must be called with either an object '
                    'pk or a slug in the URLconf.' % self.__class__.__name__
                )

        try:
            # Get the single item from the filtered restricted_queryset
            obj = restricted_queryset.get()
        except restricted_queryset.model.DoesNotExist:
            raise Http404(
                gettext('No {verbose_name} found matching the query').format(
                    verbose_name=restricted_queryset.model._meta.verbose_name
                )
            )
        return obj

    def get_restricted_queryset(self):
        """
        Return the `QuerySet` that will be used to look up the object.

        This method is called by the default implementation of get_object() and
        may not be called if get_object() is overridden.
        """
        if self.restricted_queryset is None:
            if self.restricted_model:
                queryset = self.restricted_model._default_manager.all()
            else:
                raise ImproperlyConfigured(
                    '{cls} is missing a QuerySet. Define '
                    '{cls}.restricted_model, {cls}.queryset, or override '
                    '{cls}.get_restricted_queryset().'.format(cls=self.__class__.__name__)
                )
        else:
            queryset = self.restricted_queryset.all()

        if hasattr(queryset, 'related_to'):
            queryset = queryset.related_to(self.get_request_user())

        return queryset

    def get_restricted_slug_field(self):
        """Get the name of a slug field to be used to look up by slug."""
        return self.restricted_slug_field

    def get_context_restricted_object_name(self, obj):
        """Get the name to use for the object."""
        if self.restricted_context_object_name:
            return self.restricted_context_object_name
        elif isinstance(obj, models.Model):
            return obj._meta.model_name
        else:
            return None

    def get_context_data(self, **kwargs):
        """Insert the single object into the context dict."""
        context = {}
        if self.restricted_object:
            context['restricted_object'] = self.restricted_object
            context['restricted_is_own'] = self._is_own
            context_restricted_object_name = self.get_context_restricted_object_name(self.restricted_object)
            if context_restricted_object_name:
                context[context_restricted_object_name] = self.restricted_object
        context.update(kwargs)
        return super().get_context_data(**context)

    def get_own_restricted_object(self):
        """ return object when no pk or slug defined """
        # dont forget to raise PermissionDenied if dont find own_restricted_object
        raise NotImplementedError

    def is_own(self):
        return (
                self._is_own

                # sometimes we forget to raise PermissionDenied in .get_own_restricted_object() when dont found anything
                # to prevent permitting None as restricted_object, we need to add this check
                and self.restricted_object is not None
        )
