"""used_by mixin"""

from enum import Enum
from inspect import getmembers

from django.db.models.base import Model
from django.db.models.deletion import SET_DEFAULT, SET_NULL
from django.db.models.manager import Manager
from drf_spectacular.utils import extend_schema
from guardian.shortcuts import get_objects_for_user
from rest_framework.decorators import action
from rest_framework.fields import CharField, ChoiceField
from rest_framework.request import Request
from rest_framework.response import Response

from authentik.core.api.utils import PassiveSerializer
from authentik.rbac.filters import ObjectFilter


class DeleteAction(Enum):
    """Which action a delete will have on a used object"""

    CASCADE = "cascade"
    CASCADE_MANY = "cascade_many"
    SET_NULL = "set_null"
    SET_DEFAULT = "set_default"
    LEFT_DANGLING = "left_dangling"


class UsedBySerializer(PassiveSerializer):
    """A list of all objects referencing the queried object"""

    app = CharField()
    model_name = CharField()
    pk = CharField()
    name = CharField()
    action = ChoiceField(choices=[(x.value, x.name) for x in DeleteAction])


def get_delete_action(manager: Manager) -> str:
    """Get the delete action from the Foreign key, falls back to cascade"""
    if hasattr(manager, "field"):
        if manager.field.remote_field.on_delete.__name__ == SET_NULL.__name__:
            return DeleteAction.SET_NULL.value
        if manager.field.remote_field.on_delete.__name__ == SET_DEFAULT.__name__:
            return DeleteAction.SET_DEFAULT.value
    if hasattr(manager, "source_field"):
        return DeleteAction.CASCADE_MANY.value
    return DeleteAction.CASCADE.value


class UsedByMixin:
    """Mixin to add a used_by endpoint to return a list of all objects using this object"""

    @extend_schema(
        responses={200: UsedBySerializer(many=True)},
    )
    @action(detail=True, pagination_class=None, filter_backends=[ObjectFilter])
    def used_by(self, request: Request, *args, **kwargs) -> Response:
        """Get a list of all objects that use this object"""
        model: Model = self.get_object()
        used_by = []
        shadows = []
        for attr_name, manager in getmembers(model, lambda x: isinstance(x, Manager)):
            if attr_name == "objects":  # pragma: no cover
                continue
            manager: Manager
            if manager.model._meta.abstract:
                continue
            app = manager.model._meta.app_label
            model_name = manager.model._meta.model_name
            delete_action = get_delete_action(manager)

            # To make sure we only apply shadows when there are any objects,
            # but so we only apply them once, have a simple flag for the first object
            first_object = True

            # TODO: This will only return the used-by references that the user can see
            # Either we have to leak model information here to not make the list
            # useless if the user doesn't have all permissions, or we need to double
            # query and check if there is a difference between modes the user can see
            # and can't see and add a warning
            for obj in get_objects_for_user(
                request.user, f"{app}.view_{model_name}", manager.all()
            ).all():
                # Only merge shadows on first object
                if first_object:
                    shadows += getattr(manager.model._meta, "authentik_used_by_shadows", [])
                first_object = False
                serializer = UsedBySerializer(
                    data={
                        "app": app,
                        "model_name": model_name,
                        "pk": str(obj.pk),
                        "name": str(obj),
                        "action": delete_action,
                    }
                )
                serializer.is_valid()
                used_by.append(serializer.data)
        # Check the shadows map and remove anything that should be shadowed
        for idx, user in enumerate(used_by):
            full_model_name = f"{user['app']}.{user['model_name']}"
            if full_model_name in shadows:
                del used_by[idx]
        return Response(used_by)
