from django.db.models import Model
from rest_framework import exceptions
from rest_framework.request import Request
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from console_base.api import LCViewSet

    PermissionRequiredBase = LCViewSet
else:
    PermissionRequiredBase = object


class PermissionRequiredMixin(PermissionRequiredBase):
    """
    Loosely based on restframework.permission.DjangoObjectPermissions class
    and modified for django-rules (https://github.com/dfunckt/django-rules/).
    """

    perms_map: dict[str, list[str]] = {
        'GET': [],
        'OPTIONS': [],
        'HEAD': [],
        # VIEW method is assigned when actual request method
        # is GET and a database record object was found
        'LIST': ['%(app_label)s.list_%(model_name)s'],
        'VIEW': ['%(app_label)s.view_%(model_name)s'],
        'POST': ['%(app_label)s.add_%(model_name)s'],
        'PUT': ['%(app_label)s.change_%(model_name)s'],
        'PATCH': ['%(app_label)s.change_%(model_name)s'],
        'DELETE': ['%(app_label)s.delete_%(model_name)s'],
    }

    permission_required: list[str] | None = None

    def confirm_authentication(self, request: Request) -> None:
        """
        Check if user is authenticated before doing other permission checks.
        Can be overridden on subclasses to refine logic for auth requirements.
        """
        if not request.user.is_authenticated:
            self.permission_denied(request)

    def get_required_permissions(self, method: str, model_cls: Model) -> list[str]:
        """
        Given a model and an HTTP method, return the list of permission
        codes that the user is required to have.
        """
        kwargs = {
            'app_label': model_cls._meta.app_label,
            'model_name': model_cls._meta.model_name,
        }

        if method not in self.perms_map:
            raise exceptions.MethodNotAllowed(method)

        if per_action_perms := self.permissions_from_action_handler():
            return per_action_perms

        return [perm % kwargs for perm in self.perms_map[method]]

    def permissions_from_action_handler(self) -> list[str]:
        """
        If this is an @action handler, check that specific action
        for any specific permissions.
        """
        if not (extra_actions := self.get_extra_actions()):
            return []

        handler = getattr(self, str(self.request.method).lower(), '')
        if isinstance(handler, str) or not handler:
            return []
        if handler.__func__ not in extra_actions:
            return []

        try:
            per_action_perms = self.permission_required or []
            if isinstance(per_action_perms, str):
                return [per_action_perms]
            return per_action_perms

        except AttributeError:
            return []

    def check_object_permissions(self, request: Request, obj: Model | None) -> None:
        """
        In custom ViewSets or custom Actions on ModelViewSets, call
        this method directly after getting the object via get_object.
        """
        if not obj:
            raise exceptions.NotFound()

        self.confirm_authentication(request)

        # Set perms for Detail pages, but not list pages, where no object is present
        # won't always be able to refine query to prevent lookups
        method = 'VIEW' if request.method == 'GET' else str(request.method)
        required_perms = self.get_required_permissions(method, obj)

        user = request.user
        missing_permissions = [perm for perm in required_perms if not user.has_perm(perm, obj)]
        if any(missing_permissions):
            # Return 404 so the user can't determine if the object even exists
            if method == 'VIEW' or method == 'HEAD':
                raise exceptions.NotFound()

            self.permission_denied(
                request, message=f'Missing Perms: {", ".join(missing_permissions)}'
            )

    def check_permissions(self, request: Request) -> None:
        self.confirm_authentication(request)
        url_name = request.resolver_match and request.resolver_match.url_name or ''
        request_method = request.method or ''

        try:
            is_list_view = url_name.endswith(('-list', '_list'))
        except AttributeError:
            # url_name may not be set in some test-framework situations
            is_list_view = False

        method = 'LIST' if is_list_view and request_method in ('GET', 'HEAD') else request_method
        required_perms = self.get_required_permissions(method, self.queryset.model)

        user = request.user
        missing_permissions = [perm for perm in required_perms if not user.has_perm(perm)]
        if any(missing_permissions):
            self.permission_denied(
                request, message=f'Missing Perms: {", ".join(missing_permissions)}'
            )


__all__ = [
    'PermissionRequiredMixin',
]
