from django.apps import apps
from django.contrib import admin
from django.http import HttpResponse
from django.shortcuts import redirect
from django.urls import reverse
from django.utils.safestring import mark_safe
from django.utils.text import capfirst
from django.utils.translation import ugettext_lazy as _


class InlineActionException(Exception):
    pass


class ActionNotCallable(InlineActionException):
    def __init__(self, model_admin, action, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.model_admin = model_admin
        self.action = action


class BaseInlineActionsMixin:
    INLINE_MODEL_ADMIN = 'inline'
    MODEL_ADMIN = 'admin'

    inline_actions = []

    def get_inline_actions(self, request, obj=None):
        """
        Returns a list of all actions for this Admin.
        """
        # If self.actions is explicitly set to None that means that we don't
        # want *any* actions enabled on this page.
        if self.inline_actions is None:
            return []

        actions = []

        # Gather actions from the inline admin and all parent classes,
        # starting with self and working back up.
        for klass in self.__class__.mro()[::-1]:
            class_actions = getattr(klass, 'inline_actions', [])
            # Avoid trying to iterate over None
            if not class_actions:
                continue

            for action in class_actions:
                if action not in actions:
                    actions.append(action)

        return actions

    def get_readonly_fields(self, request, obj=None):
        fields = super().get_readonly_fields(request, obj)
        fields = list(fields)

        if 'render_inline_actions' not in fields:
            fields.append('render_inline_actions')
        return fields

    def _get_admin_type(self, model_admin=None):
        """
        Returns wether this is an InlineAdmin or not.
        """
        model_admin = model_admin or self

        if isinstance(model_admin, admin.options.InlineModelAdmin):
            return self.INLINE_MODEL_ADMIN
        return self.MODEL_ADMIN

    def render_inline_actions(self, obj=None):  # NOQA: C901
        """
        Renders all defined inline actions as html.
        """
        if not (obj and obj.pk):
            return ''

        buttons = []
        for action_name in self.get_inline_actions(self._request, obj):
            action_func = getattr(self, action_name, None)
            if not action_func:
                raise RuntimeError(
                    "Could not find action `{}`".format(action_name))

            # Add per-object label support
            action_name = action_func.__name__
            label_handler = getattr(
                self, 'get_{}_label'.format(action_name), None)
            if callable(label_handler):
                description = label_handler(obj=obj)
            else:
                try:
                    description = action_func.short_description
                except AttributeError:
                    description = capfirst(action_name.replace('_', ' '))

            # Add per-object css classes support
            css_handler = getattr(
                self, 'get_{}_css'.format(action_name), None)
            if callable(css_handler):
                css_classes = css_handler(obj=obj)
            else:
                try:
                    css_classes = action_func.css_classes
                except AttributeError:
                    css_classes = ''

            # If the form is submitted, we have no information about the
            # requested action.
            # Hence we need all data to be encoded using the action name.
            action_data = [
                # required to distinguish between multiple inlines for the same model
                self.__class__.__name__.lower(),
                self._get_admin_type(),
                action_name,
                obj._meta.app_label,
                obj._meta.model_name,
                str(obj.pk),
            ]
            buttons.append(
                '<input type="submit" name="{}" value="{}" class="{}">'.format(
                    '_action__{}'.format('__'.join(action_data)),
                    description,
                    css_classes,
                )
            )
        return mark_safe('<div class="submit_row inline_actions">{}</div>'.format(
            ''.join(buttons)
        ))
    render_inline_actions.short_description = _("Actions")
    render_inline_actions.allow_tags = True


class InlineActionsMixin(BaseInlineActionsMixin):
    def render_inline_actions(self, obj=None):
        html = super().render_inline_actions(obj=obj)
        # we have to add <p> tags as a workaround for invalid html
        return mark_safe('</p>{}<p>'.format(html))

    render_inline_actions.short_description = _("Actions")
    render_inline_actions.allow_tags = True

    def get_fields(self, request, obj=None):
        # store `request` for `get_inline_actions`
        self._request = request

        fields = super().get_fields(request, obj)
        if self.inline_actions is not None:  # is it explicitly disabled?
            fields = list(fields)
            if 'render_inline_actions' not in fields:
                fields.append('render_inline_actions')
        return fields


class InlineActionsModelAdminMixin(BaseInlineActionsMixin):
    class Media:
        css = {
            "all": (
                "inline_actions/css/inline_actions.css",
            )
        }

    def get_list_display(self, request):
        # store `request` for `get_inline_actions`
        self._request = request

        fields = super().get_list_display(request)
        if self.inline_actions is not None:  # is it explicitly disabled?
            fields = list(fields)
            if 'render_inline_actions' not in fields:
                fields.append('render_inline_actions')
        return fields

    def get_fields(self, request, obj=None):
        # store `request` for `get_inline_actions`
        self._request = request

        fields = super().get_fields(request, obj=obj)
        if not self.fields:
            # django adds all readonly fields by default
            # if `self.fields` is not defined we don't want to include
            # `render_inline_actions
            fields.remove('render_inline_actions')
        return fields

    def _execute_action(self, request, model_admin, action, obj, parent_obj=None):
        """
        Tries to execute the requested action and returns a `HttpResponse`.

        raises
            ActionNotCallable - When action is not a function
        """
        # execute action
        func = getattr(model_admin, action, None)
        try:
            response = func(request, obj, parent_obj=parent_obj)
        except TypeError as e:
            raise ActionNotCallable(model_admin, action) from e

        # we should receive an HttpResponse
        if isinstance(response, HttpResponse):
            return response

        # otherwise redirect back
        if parent_obj is None:  # InlineActionsMixin.MODEL_ADMIN:
            # redirect to `changelist`
            url = reverse(
                'admin:{}_{}_changelist'.format(
                    obj._meta.app_label,
                    obj._meta.model_name,
                ),
            )
        else:
            # redirect to `changeform`
            url = reverse(
                'admin:{}_{}_change'.format(
                    parent_obj._meta.app_label,
                    parent_obj._meta.model_name,
                ),
                args=(parent_obj.pk,)
            )

        # readd query string
        query = request.META['QUERY_STRING'] or request.GET.urlencode()
        if query:
            url = '{}?{}'.format(url, query)

        return redirect(url)

    def _handle_action(self, request, object_id=None):
        """
        Resolve and executes the action issued by the current request.
        If no action was triggered, it does nothing.

        Returns `HttpResponse` or `None`
        """
        all_actions = [key for key in list(request.POST.keys())
                       if key.startswith('_action__')]

        if request.method == 'POST' and all_actions:
            assert len(all_actions) == 1
            raw_action_name = all_actions[0].replace('_action__', '', 1)

            # resolve action and target models
            raw_action_parts = raw_action_name.split('__')
            admin_class_name, admin_type = raw_action_parts[:2]
            action, app_label, model_name, object_pk = raw_action_parts[2:]

            model = apps.get_model(app_label=app_label,
                                   model_name=model_name)
            parent_obj = self.get_object(request, object_id)

            # find action and execute
            if admin_type == self.MODEL_ADMIN:
                model_admin = self
                obj = model_admin.get_queryset(request).get(pk=object_pk)
                # parent_obj is None because `object_id` is None

            else:
                for inline in self.get_inline_instances(request):
                    inline_class_name = inline.__class__.__name__.lower()
                    matches_inline_class = inline_class_name == admin_class_name
                    matches_model = inline.model == model

                    if not matches_model or not matches_inline_class:
                        continue
                    model_admin = inline
                obj = model_admin.get_queryset(request).get(pk=object_pk)

            if model_admin:
                return self._execute_action(
                    request, model_admin, action, obj, parent_obj)
        return None

    def changeform_view(self, request, object_id=None, form_url='', extra_context=None):
        # handle requested action if required
        response = self._handle_action(request, object_id=object_id)
        if response:
            return response

        # continue normally
        return super().changeform_view(
            request, object_id, form_url, extra_context)

    def changelist_view(self, request, extra_context=None):
        # handle requested action if required
        response = self._handle_action(request)
        if response:
            return response

        # continue normally
        return super().changelist_view(
            request, extra_context)