from typing import Any, Optional, Sequence, TYPE_CHECKING

from django.conf import settings
from django.core.exceptions import (
    ImproperlyConfigured,
    PermissionDenied,
)
from django.contrib.auth.decorators import user_passes_test
from django.contrib.auth.mixins import (
    AccessMixin,
    LoginRequiredMixin,
    UserPassesTestMixin,
)
from django.contrib import messages
from django.http import HttpResponse
from django.template.response import TemplateResponse
from console_base.http import FastJsonResponse
from console_base.theme import Icons  # type: ignore[attr-defined]
from console_base.typehints import LCHttpRequest
from vanilla import UpdateView

from rules.contrib.views import PermissionRequiredMixin

if TYPE_CHECKING:
    from .models import BaseIntegerPKModel

    class LoginBase(AccessMixin, UpdateView):
        pass

    class PermissionBaseMixin(PermissionRequiredMixin, UpdateView):
        def dispatch(self, request: LCHttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:  # type: ignore[override]
            ...

else:
    LoginBase = AccessMixin
    PermissionBaseMixin = PermissionRequiredMixin


# ----------------------------------------------------------------------
class LCLoginOrTokenRequiredMixin(AccessMixin):
    """
    Authenticate via logged-in user or Token, passed via header or GET param
    Used mainly for health check scripts.
    """

    login_url = settings.LOGIN_URL
    token_name = 'Bearer'
    valid_tokens: Sequence[str] = ()

    def get_token(self, request: LCHttpRequest) -> str:
        """
        Try to get the passed token, starting with the header and fall back to `GET` param
        """
        try:
            auth_header = request.META['HTTP_AUTHORIZATION']
            name, token = auth_header.strip().split()
            if name != self.token_name:
                token = ''
        except KeyError:
            token = request.GET.get(self.token_name)
        return token

    def dispatch(self, request: LCHttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
        if self.get_token(request) not in self.valid_tokens and not request.user.is_authenticated:
            return self.handle_no_permission()
        return super().dispatch(request, *args, **kwargs)  # type: ignore[misc]


# ----------------------------------------------------------------------
class LCLoginRequiredMixin(LoginRequiredMixin):
    login_url = settings.LOGIN_URL


# ----------------------------------------------------------------------
class LCPermissionRequired(PermissionBaseMixin):
    """
    Includes all functionality of django.contrib.auth.mixins.AccessMixin
    """

    def get_permission_object(self) -> Optional['BaseIntegerPKModel']:
        """
        Override this method to provide the object to check for permission
        against. By default, uses ``self.get_object()`` as provided by
        ``SingleObjectMixin``. Returns None if there's no ``get_object``
        method, or is not properly configured.
        """
        try:
            # Requires SingleObjectMixin or equivalent ``get_object`` method
            return self.get_object()
        except (AttributeError, ValueError, ImproperlyConfigured):  # pragma: no cover
            return None

    def get_view_action(self) -> str:
        try:
            return 'toggle' if self.url_name.endswith('_toggle') else self.action  # noqa
        except AttributeError:
            return 'view'

    def superuser_denied_perms(self) -> bool | None:
        """
        short-circuit perms check here to deny superusers
        the ability to change builtin/un-removable records
        """
        if not self.request.user.is_superuser:
            return None

        if self.is_human_staff_superuser():
            return False

        obj = self.get_permission_object()
        action = self.get_view_action()
        if obj and action == 'delete':
            try:
                return obj.unremovable()
            except AttributeError:
                return False

        if getattr(obj, 'is_builtin', False) and not getattr(obj, 'is_editable', False):
            return action in ('update', 'edit', 'toggle')

        return False

    def is_human_staff_superuser(self) -> bool:
        """
        Qualified Humans need to be able to perform any operation.
        Removing "unremovable" records is the most sensitive, so
        differentiate between API users and human users.
        """
        user = self.request.user
        return user.is_superuser and user.is_staff and not user.is_api  # noqa

    def has_permission(self) -> bool:
        perms = self.get_permission_required()
        user = self.request.user

        # Login not required. Should be used only very rarely
        if perms and settings.NO_LOGIN_REQUIRED in perms:
            return True

        if not user.is_authenticated:
            return False

        if perms and settings.NO_PERM_CHECK in perms:
            return True

        if self.superuser_denied_perms():
            return False

        obj = self.get_permission_object()

        return self.request.user.has_perms(perms, obj)

    def handle_no_permission(self) -> HttpResponse:  # type: ignore[override]
        request = self.request
        if not request.user.is_authenticated:
            return super().handle_no_permission()

        msg = self.get_permission_denied_message()

        if request.accepts('application/json') and not request.accepts('text/html'):
            return FastJsonResponse({'msg': msg}, status=403)

        messages.error(request, msg, fail_silently=True)

        if self.up.is_unpoly():  # noqa
            ctx = {'view_css_icon': Icons.HAND_WARNING, 'title': 'Insufficient Permissions'}
            return TemplateResponse(request, settings.MESSAGES_TEMPLATE, context=ctx, status=403)

        return super().handle_no_permission()

    def get_permission_denied_message(self) -> str:
        # self.denied_message should be set in the test_func method
        try:
            return self.denied_message  # noqa
        except AttributeError:
            pass

        try:
            obj = str(self.get_object())
        except (AttributeError, ImproperlyConfigured):
            obj = 'records'

        try:
            action = 'toggle' if self.url_name.endswith('_toggle') else self.action  # noqa
        except AttributeError:
            action = 'view'

        return f'{self.request.user} has no permission to {action} {obj}'


# ----------------------------------------------------------------------
class LCUserPassesTestMixin(UserPassesTestMixin):
    redirect_unauthenticated_users = settings.LOGIN_URL
    action = 'create'


################################################################################################
# Decorators
################################################################################################


# ---------------------------------------------------------------------------------------
def group_required(
    group_names: str | Sequence[str],
    login_url: str | None = None,
    raise_exception: bool = False,
) -> Any:
    """Requires user membership in at least one of
    the groups passed in.
    """

    groups = (group_names,) if not isinstance(group_names, (list, tuple)) else group_names

    def in_groups(u) -> bool:  # type: ignore[no-untyped-def]
        if u.is_authenticated:
            if u.is_superuser | bool(u.groups.filter(name__in=groups)):
                return True
        if raise_exception:
            raise PermissionDenied
        return False

    return user_passes_test(in_groups, login_url=login_url)


__all__ = (
    'LCLoginRequiredMixin',
    'LCLoginOrTokenRequiredMixin',
    'LCPermissionRequired',
    'LCUserPassesTestMixin',
    'group_required',
)
