from django.apps import apps
from django.conf import settings
from django.contrib.admin.utils import quote
from django.core import checks
from django.core.exceptions import ImproperlyConfigured, PermissionDenied
from django.http import Http404
from django.urls import path, re_path, reverse, reverse_lazy
from django.utils.functional import cached_property
from django.utils.text import capfirst
from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy

from wagtail import hooks
from wagtail.admin.checks import check_panels_in_model
from wagtail.admin.panels import ObjectList, extract_panel_definitions_from_model_class
from wagtail.admin.ui.components import MediaContainer
from wagtail.admin.ui.menus import MenuItem
from wagtail.admin.ui.side_panels import ChecksSidePanel, PreviewSidePanel
from wagtail.admin.ui.tables import (
    BulkActionsCheckboxColumn,
    NumberColumn,
    TitleColumn,
)
from wagtail.admin.views import generic
from wagtail.admin.views.generic import history, lock, workflow
from wagtail.admin.views.generic.permissions import PermissionCheckedMixin
from wagtail.admin.views.generic.preview import (
    PreviewOnCreate,
    PreviewOnEdit,
    PreviewRevision,
)
from wagtail.admin.viewsets import viewsets
from wagtail.admin.viewsets.model import ModelViewSet, ModelViewSetGroup
from wagtail.admin.widgets.button import (
    BaseButton,
    Button,
    ButtonWithDropdown,
)
from wagtail.models import (
    DraftStateMixin,
    LockableMixin,
    PreviewableMixin,
    RevisionMixin,
    WorkflowMixin,
)
from wagtail.permissions import ModelPermissionPolicy
from wagtail.snippets.action_menu import SnippetActionMenu
from wagtail.snippets.models import SnippetAdminURLFinder, get_snippet_models
from wagtail.snippets.side_panels import SnippetStatusSidePanel
from wagtail.snippets.views.chooser import SnippetChooserViewSet


# == Helper functions ==
def get_snippet_model_from_url_params(app_name, model_name):
    """
    Retrieve a model from an app_label / model_name combo.
    Raise Http404 if the model is not a valid snippet type.
    """
    try:
        model = apps.get_model(app_name, model_name)
    except LookupError as e:
        raise Http404 from e
    if model not in get_snippet_models():
        # don't allow people to hack the URL to edit content types that aren't registered as snippets
        raise Http404

    return model


# == Views ==


def get_snippet_models_for_index_view():
    models = get_snippet_models()

    if getattr(settings, "WAGTAILSNIPPETS_MENU_SHOW_ALL", False):
        return models

    return [
        model
        for model in models
        if not model.snippet_viewset.get_menu_item_is_registered()
    ]


class ModelIndexView(generic.BaseListingView):
    page_title = gettext_lazy("Snippets")
    header_icon = "snippet"
    index_url_name = "wagtailsnippets:index"
    default_ordering = "name"

    def setup(self, request, *args, **kwargs):
        super().setup(request, *args, **kwargs)
        self.snippet_types = self._get_snippet_types()

    def _get_snippet_types(self):
        return [
            {
                "name": capfirst(model._meta.verbose_name_plural),
                "count": model._default_manager.all().count(),
                "model": model,
                "url": url,
            }
            for model in get_snippet_models_for_index_view()
            if (url := self.get_list_url(model))
        ]

    def dispatch(self, request, *args, **kwargs):
        if not self.snippet_types:
            raise PermissionDenied
        return super().dispatch(request, *args, **kwargs)

    def get_list_url(self, model):
        if model.snippet_viewset.permission_policy.user_has_any_permission(
            self.request.user,
            {"add", "change", "delete", "view"},
        ):
            return reverse(model.snippet_viewset.get_url_name("list"))

    def get_queryset(self):
        return None

    @cached_property
    def columns(self):
        return [
            TitleColumn(
                "name",
                label=_("Name"),
                get_url=lambda type: type["url"],
                sort_key="name",
            ),
            NumberColumn(
                "count",
                label=_("Instances"),
                sort_key="count",
            ),
        ]

    def get_context_data(self, **kwargs):
        reverse = self.ordering[0] == "-"

        if self.ordering in ["count", "-count"]:
            snippet_types = sorted(
                self.snippet_types,
                key=lambda type: type["count"],
                reverse=reverse,
            )
        else:
            snippet_types = sorted(
                self.snippet_types,
                key=lambda type: type["name"].lower(),
                reverse=reverse,
            )

        return super().get_context_data(object_list=snippet_types)

    def get_template_names(self):
        # We use the generic index template instead of model_index.html,
        # but we look for it anyway so users can customise this view's template
        # without having to override the entire view or the generic template.
        return [
            "wagtailsnippets/snippets/model_index.html",
            self.template_name,
        ]


class IndexView(generic.IndexViewOptionalFeaturesMixin, generic.IndexView):
    view_name = "list"

    def get_base_queryset(self):
        # Allow the queryset to be a callable that takes a request
        # so that it can be evaluated in the context of the request
        if callable(self.queryset):
            self.queryset = self.queryset(self.request)
        return super().get_base_queryset()

    @cached_property
    def columns(self):
        return [
            BulkActionsCheckboxColumn("bulk_actions", obj_type="snippet"),
            *super().columns,
        ]

    def _get_title_column(self, *args, **kwargs):
        return super()._get_title_column(
            *args,
            **kwargs,
            get_title_id=lambda instance: f"snippet_{quote(instance.pk)}_title",
        )

    def get_list_buttons(self, instance):
        next_url = self.request.path
        list_buttons = []
        more_buttons = []

        buttons = self.get_list_more_buttons(instance)
        for hook in hooks.get_hooks("register_snippet_listing_buttons"):
            buttons.extend(hook(instance, self.request.user, next_url))

        for button in buttons:
            if isinstance(button, BaseButton) and not button.allow_in_dropdown:
                # If the button is not allowed in a dropdown menu, add it to
                # the top-level list of buttons
                list_buttons.append(button)
            elif isinstance(button, MenuItem):
                # Allow simple MenuItem instances to be passed in directly
                if button.is_shown(self.request.user):
                    more_buttons.append(Button.from_menu_item(button))
            elif button.show:
                # Otherwise, add it to the default "More" dropdown
                more_buttons.append(button)

        # Pass the more_buttons to the construct hooks, as that's what contains
        # the default buttons and most buttons added via register_snippet_listing_buttons
        for hook in hooks.get_hooks("construct_snippet_listing_buttons"):
            hook(more_buttons, instance, self.request.user)

        if more_buttons:
            list_buttons.append(
                ButtonWithDropdown(
                    buttons=more_buttons,
                    icon_name="dots-horizontal",
                    attrs={
                        "aria-label": _("More options for '%(title)s'")
                        % {"title": str(instance)},
                    },
                )
            )

        return list_buttons


class CreateView(generic.CreateEditViewOptionalFeaturesMixin, generic.CreateView):
    view_name = "create"
    template_name = "wagtailsnippets/snippets/create.html"

    def run_before_hook(self):
        response = self.run_hook("before_create_snippet", self.request, self.model)
        if response:
            if self.expects_json_response and not self.response_is_json(response):
                # Hook response is not suitable for a JSON response, so construct our own error response
                return self.json_error_response(
                    "blocked_by_hook",
                    _("Request to create %(model_name)s was blocked by hook.")
                    % {"model_name": self.model._meta.verbose_name},
                )
            else:
                return response

    def run_after_hook(self):
        response = self.run_hook("after_create_snippet", self.request, self.object)
        if response:
            if self.expects_json_response and not self.response_is_json(response):
                # Hook response is not suitable for a JSON response, so ignore it and just use
                # the standard one
                return None
            else:
                return response

    def _get_action_menu(self):
        return SnippetActionMenu(self.request, view=self.view_name, model=self.model)

    def get_side_panels(self):
        side_panels = [
            SnippetStatusSidePanel(
                self.form.instance,
                self.request,
                show_schedule_publishing_toggle=getattr(
                    self.form, "show_schedule_publishing_toggle", False
                ),
                locale=self.locale,
                translations=self.translations,
            )
        ]
        if self.preview_enabled and self.form.instance.is_previewable():
            side_panels.append(
                PreviewSidePanel(
                    self.form.instance, self.request, preview_url=self.get_preview_url()
                )
            )
            side_panels.append(ChecksSidePanel(self.form.instance, self.request))
        return MediaContainer(side_panels)

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        action_menu = self._get_action_menu()
        context["media"] += action_menu.media
        context["action_menu"] = action_menu
        return context


class CopyView(generic.CopyViewMixin, CreateView):
    pass


class EditView(
    generic.CreateEditViewOptionalFeaturesMixin,
    generic.EditView,
):
    view_name = "edit"
    template_name = "wagtailsnippets/snippets/edit.html"

    def run_before_hook(self):
        response = self.run_hook("before_edit_snippet", self.request, self.object)
        if response:
            if self.expects_json_response and not self.response_is_json(response):
                # Hook response is not suitable for a JSON response, so construct our own error response
                return self.json_error_response(
                    "blocked_by_hook",
                    _("Request to edit %(model_name)s was blocked by hook.")
                    % {"model_name": self.model._meta.verbose_name},
                )
            else:
                return response

    def run_after_hook(self):
        response = self.run_hook("after_edit_snippet", self.request, self.object)
        if response:
            if self.expects_json_response and not self.response_is_json(response):
                # Hook response is not suitable for a JSON response, so ignore it and just use
                # the standard one
                return None
            else:
                return response

    def _get_action_menu(self):
        return SnippetActionMenu(
            self.request,
            view=self.view_name,
            instance=self.object,
            locked_for_user=self.locked_for_user,
        )

    def get_side_panels(self):
        side_panels = [
            SnippetStatusSidePanel(
                self.object,
                self.request,
                show_schedule_publishing_toggle=getattr(
                    self.form, "show_schedule_publishing_toggle", False
                ),
                live_object=self.live_object,
                scheduled_object=self.live_object.get_scheduled_revision_as_object()
                if self.draftstate_enabled
                else None,
                locale=self.locale,
                translations=self.translations,
                usage_url=self.get_usage_url(),
                history_url=self.get_history_url(),
                last_updated_info=self.get_last_updated_info(),
            )
        ]
        if self.preview_enabled and self.object.is_previewable():
            side_panels.append(
                PreviewSidePanel(
                    self.object, self.request, preview_url=self.get_preview_url()
                )
            )
            side_panels.append(ChecksSidePanel(self.object, self.request))
        return MediaContainer(side_panels)

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        action_menu = self._get_action_menu()
        context["media"] += action_menu.media
        context["action_menu"] = action_menu
        return context


class DeleteView(generic.DeleteView):
    view_name = "delete"

    def run_before_hook(self):
        return self.run_hook("before_delete_snippet", self.request, [self.object])

    def run_after_hook(self):
        return self.run_hook("after_delete_snippet", self.request, [self.object])


class UsageView(generic.UsageView):
    view_name = "usage"


class HistoryView(history.HistoryView):
    view_name = "history"


class InspectView(generic.InspectView):
    view_name = "inspect"


class PreviewOnCreateView(PreviewOnCreate):
    pass


class PreviewOnEditView(PreviewOnEdit):
    pass


class PreviewRevisionView(PermissionCheckedMixin, PreviewRevision):
    permission_required = "change"


class RevisionsCompareView(PermissionCheckedMixin, generic.RevisionsCompareView):
    permission_required = "change"


class UnpublishView(PermissionCheckedMixin, generic.UnpublishView):
    permission_required = "publish"


class RevisionsUnscheduleView(PermissionCheckedMixin, generic.RevisionsUnscheduleView):
    permission_required = "publish"


class LockView(PermissionCheckedMixin, lock.LockView):
    permission_required = "lock"

    def user_has_permission(self, permission):
        if self.request.user.is_superuser:
            return True

        if permission == self.permission_required and isinstance(
            self.object, WorkflowMixin
        ):
            current_workflow_task = self.object.current_workflow_task
            if current_workflow_task:
                return current_workflow_task.user_can_lock(
                    self.object, self.request.user
                )

        return super().user_has_permission(permission)


class UnlockView(PermissionCheckedMixin, lock.UnlockView):
    permission_required = "unlock"

    def user_has_permission(self, permission):
        if self.request.user.is_superuser:
            return True

        if permission == self.permission_required:
            # Allow unlocking even if the user does not have the 'unlock' permission
            # if they are the user who locked the object
            if self.object.locked_by_id == self.request.user.pk:
                return True

            if isinstance(self.object, WorkflowMixin):
                current_workflow_task = self.object.current_workflow_task
                if current_workflow_task:
                    return current_workflow_task.user_can_unlock(
                        self.object, self.request.user
                    )

        return super().user_has_permission(permission)


class WorkflowActionView(workflow.WorkflowAction):
    pass


class CollectWorkflowActionDataView(workflow.CollectWorkflowActionData):
    pass


class ConfirmWorkflowCancellationView(workflow.ConfirmWorkflowCancellation):
    pass


class WorkflowPreviewView(workflow.PreviewRevisionForTask):
    pass


class WorkflowHistoryView(PermissionCheckedMixin, history.WorkflowHistoryView):
    permission_required = "change"


class WorkflowHistoryDetailView(
    PermissionCheckedMixin, history.WorkflowHistoryDetailView
):
    permission_required = "change"


class ReorderView(generic.ReorderView):
    @cached_property
    def permission_required(self):
        if issubclass(self.model, DraftStateMixin):
            return "publish"
        return super().permission_required


class SnippetViewSet(ModelViewSet):
    """
    A viewset that instantiates the admin views for snippets.

    All attributes and methods from
    :class:`~wagtail.admin.viewsets.model.ModelViewSet` are available.

    For more information on how to use this class,
    see :ref:`wagtailsnippets_custom_admin_views`.
    """

    #: The model class to be registered as a snippet with this viewset.
    model = None

    #: The number of items to display in the chooser view. Defaults to 10.
    chooser_per_page = 10

    #: The URL namespace to use for the admin views.
    #: If left unset, ``wagtailsnippets_{app_label}_{model_name}`` is used instead.
    #:
    #: **Deprecated** - the preferred attribute to customise is :attr:`~.ViewSet.url_namespace`.
    admin_url_namespace = None

    #: The base URL path to use for the admin views.
    #: If left unset, ``snippets/{app_label}/{model_name}`` is used instead.
    #:
    #: **Deprecated** - the preferred attribute to customise is :attr:`~.ViewSet.url_prefix`.
    base_url_path = None

    #: The URL namespace to use for the chooser admin views.
    #: If left unset, ``wagtailsnippetchoosers_{app_label}_{model_name}`` is used instead.
    chooser_admin_url_namespace = None

    #: The base URL path to use for the chooser admin views.
    #: If left unset, ``snippets/choose/{app_label}/{model_name}`` is used instead.
    chooser_base_url_path = None

    #: The view class to use for the index view; must be a subclass of ``wagtail.snippets.views.snippets.IndexView``.
    index_view_class = IndexView

    #: The view class to use for the create view; must be a subclass of ``wagtail.snippets.views.snippets.CreateView``.
    add_view_class = CreateView

    #: The view class to use for the copy view; must be a subclass of ``wagtail.snippet.views.snippets.CopyView``.
    copy_view_class = CopyView

    #: The view class to use for the edit view; must be a subclass of ``wagtail.snippets.views.snippets.EditView``.
    edit_view_class = EditView

    #: The view class to use for the delete view; must be a subclass of ``wagtail.snippets.views.snippets.DeleteView``.
    delete_view_class = DeleteView

    #: The view class to use for the usage view; must be a subclass of ``wagtail.snippets.views.snippets.UsageView``.
    usage_view_class = UsageView

    #: The view class to use for the history view; must be a subclass of ``wagtail.snippets.views.snippets.HistoryView``.
    history_view_class = HistoryView

    #: The view class to use for the inspect view; must be a subclass of ``wagtail.snippets.views.snippets.InspectView``.
    inspect_view_class = InspectView

    #: The view class to use for the reorder view; must be a subclass of ``wagtail.snippets.views.snippets.ReorderView``.
    reorder_view_class = ReorderView

    #: The view class to use for previewing revisions; must be a subclass of ``wagtail.snippets.views.snippets.PreviewRevisionView``.
    revisions_view_class = PreviewRevisionView

    #: The view class to use for comparing revisions; must be a subclass of ``wagtail.snippets.views.snippets.RevisionsCompareView``.
    revisions_compare_view_class = RevisionsCompareView

    #: The view class to use for unscheduling revisions; must be a subclass of ``wagtail.snippets.views.snippets.RevisionsUnscheduleView``.
    revisions_unschedule_view_class = RevisionsUnscheduleView

    #: The view class to use for unpublishing a snippet; must be a subclass of ``wagtail.snippets.views.snippets.UnpublishView``.
    unpublish_view_class = UnpublishView

    #: The view class to use for previewing on the create view; must be a subclass of ``wagtail.snippets.views.snippets.PreviewOnCreateView``.
    preview_on_add_view_class = PreviewOnCreateView

    #: The view class to use for previewing on the edit view; must be a subclass of ``wagtail.snippets.views.snippets.PreviewOnEditView``.
    preview_on_edit_view_class = PreviewOnEditView

    #: The view class to use for locking a snippet; must be a subclass of ``wagtail.snippets.views.snippets.LockView``.
    lock_view_class = LockView

    #: The view class to use for unlocking a snippet; must be a subclass of ``wagtail.snippets.views.snippets.UnlockView``.
    unlock_view_class = UnlockView

    #: The view class to use for performing a workflow action; must be a subclass of ``wagtail.snippets.views.snippets.WorkflowActionView``.
    workflow_action_view_class = WorkflowActionView

    #: The view class to use for performing a workflow action that returns the validated data in the response; must be a subclass of ``wagtail.snippets.views.snippets.CollectWorkflowActionDataView``.
    collect_workflow_action_data_view_class = CollectWorkflowActionDataView

    #: The view class to use for confirming the cancellation of a workflow; must be a subclass of ``wagtail.snippets.views.snippets.ConfirmWorkflowCancellationView``.
    confirm_workflow_cancellation_view_class = ConfirmWorkflowCancellationView

    #: The view class to use for previewing a revision for a specific task; must be a subclass of ``wagtail.snippets.views.snippets.WorkflowPreviewView``.
    workflow_preview_view_class = WorkflowPreviewView

    #: The view class to use for the workflow history view; must be a subclass of ``wagtail.snippets.views.snippets.WorkflowHistoryView``.
    workflow_history_view_class = WorkflowHistoryView

    #: The view class to use for the workflow history detail view; must be a subclass of ``wagtail.snippets.views.snippets.WorkflowHistoryDetailView``.
    workflow_history_detail_view_class = WorkflowHistoryDetailView

    #: The ViewSet class to use for the chooser views; must be a subclass of ``wagtail.snippets.views.chooser.SnippetChooserViewSet``.
    chooser_viewset_class = SnippetChooserViewSet

    #: The prefix of template names to look for when rendering the admin views.
    template_prefix = "wagtailsnippets/snippets/"

    def __init__(self, **kwargs):
        super().__init__(**kwargs)

        if self.model is None:
            raise ImproperlyConfigured(
                "SnippetViewSet must be passed a model or define a model attribute."
            )

        self.model_opts = self.model._meta
        self.app_label = self.model_opts.app_label
        self.model_name = self.model_opts.model_name

        self.preview_enabled = issubclass(self.model, PreviewableMixin)
        self.revision_enabled = issubclass(self.model, RevisionMixin)
        self.draftstate_enabled = issubclass(self.model, DraftStateMixin)
        self.workflow_enabled = issubclass(self.model, WorkflowMixin)
        self.locking_enabled = issubclass(self.model, LockableMixin)

        self.menu_item_is_registered = getattr(
            self, "menu_item_is_registered", bool(self.menu_hook)
        )

    @cached_property
    def url_prefix(self):
        # SnippetViewSet historically allows overriding the URL prefix via the
        # get_admin_base_path method or the admin_base_path attribute, so preserve that here
        return self.get_admin_base_path()

    @cached_property
    def url_namespace(self):
        # SnippetViewSet historically allows overriding the URL namespace via the
        # get_admin_url_namespace method or the admin_url_namespace attribute,
        # so preserve that here
        return self.get_admin_url_namespace()

    @property
    def revisions_revert_view_class(self):
        """
        The view class to use for reverting to a previous revision.

        By default, this class is generated by combining the edit view with
        ``wagtail.admin.views.generic.mixins.RevisionsRevertMixin``. As a result,
        this class must be a subclass of ``wagtail.snippets.views.snippets.EditView``
        and must handle the reversion correctly.
        """
        revisions_revert_view_class = type(
            "_RevisionsRevertView",
            (generic.RevisionsRevertMixin, self.edit_view_class),
            {"view_name": "revisions_revert"},
        )
        return revisions_revert_view_class

    @property
    def permission_policy(self):
        return ModelPermissionPolicy(self.model)

    def get_common_view_kwargs(self, **kwargs):
        return super().get_common_view_kwargs(
            **{
                "index_url_name": self.get_url_name("list"),
                "index_results_url_name": self.get_url_name("list_results"),
                "lock_url_name": self.get_url_name("lock"),
                "unlock_url_name": self.get_url_name("unlock"),
                "revisions_view_url_name": self.get_url_name("revisions_view"),
                "revisions_revert_url_name": self.get_url_name("revisions_revert"),
                "revisions_compare_url_name": self.get_url_name("revisions_compare"),
                "revisions_unschedule_url_name": self.get_url_name(
                    "revisions_unschedule"
                ),
                "unpublish_url_name": self.get_url_name("unpublish"),
                "breadcrumbs_items": self.breadcrumbs_items,
                **kwargs,
            }
        )

    def get_index_view_kwargs(self, **kwargs):
        return super().get_index_view_kwargs(
            queryset=self.get_queryset,
            **kwargs,
        )

    def get_add_view_kwargs(self, **kwargs):
        return super().get_add_view_kwargs(
            preview_url_name=self.get_url_name("preview_on_add"),
            **kwargs,
        )

    def get_copy_view_kwargs(self, **kwargs):
        return self.get_add_view_kwargs(**kwargs)

    def get_edit_view_kwargs(self, **kwargs):
        return super().get_edit_view_kwargs(
            preview_url_name=self.get_url_name("preview_on_edit"),
            workflow_history_url_name=self.get_url_name("workflow_history"),
            confirm_workflow_cancellation_url_name=self.get_url_name(
                "confirm_workflow_cancellation"
            ),
            **kwargs,
        )

    @property
    def revisions_view(self):
        return self.construct_view(self.revisions_view_class)

    @property
    def revisions_revert_view(self):
        return self.construct_view(
            self.revisions_revert_view_class,
            **self.get_edit_view_kwargs(),
        )

    @property
    def revisions_compare_view(self):
        return self.construct_view(
            self.revisions_compare_view_class,
            edit_handler=self._edit_handler,
            template_name=self.get_templates(
                "revisions_compare",
                fallback=self.revisions_compare_view_class.template_name,
            ),
        )

    @property
    def revisions_unschedule_view(self):
        return self.construct_view(
            self.revisions_unschedule_view_class,
            template_name=self.get_templates(
                "revisions_unschedule",
                fallback=self.revisions_unschedule_view_class.template_name,
            ),
        )

    @property
    def unpublish_view(self):
        return self.construct_view(
            self.unpublish_view_class,
            template_name=self.get_templates(
                "unpublish", fallback=self.unpublish_view_class.template_name
            ),
        )

    @property
    def preview_on_add_view(self):
        return self.construct_view(
            self.preview_on_add_view_class,
            form_class=self.get_form_class(),
        )

    @property
    def preview_on_edit_view(self):
        return self.construct_view(
            self.preview_on_edit_view_class,
            form_class=self.get_form_class(for_update=True),
        )

    @property
    def lock_view(self):
        return self.construct_view(
            self.lock_view_class,
            success_url_name=self.get_url_name("edit"),
        )

    @property
    def copy_view(self):
        return self.construct_view(self.copy_view_class, **self.get_copy_view_kwargs())

    @property
    def unlock_view(self):
        return self.construct_view(
            self.unlock_view_class,
            success_url_name=self.get_url_name("edit"),
        )

    @property
    def workflow_action_view(self):
        return self.construct_view(
            self.workflow_action_view_class,
            redirect_url_name=self.get_url_name("edit"),
            submit_url_name=self.get_url_name("workflow_action"),
        )

    @property
    def collect_workflow_action_data_view(self):
        return self.construct_view(
            self.collect_workflow_action_data_view_class,
            redirect_url_name=self.get_url_name("edit"),
            submit_url_name=self.get_url_name("collect_workflow_action_data"),
        )

    @property
    def confirm_workflow_cancellation_view(self):
        return self.construct_view(self.confirm_workflow_cancellation_view_class)

    @property
    def workflow_preview_view(self):
        return self.construct_view(self.workflow_preview_view_class)

    @property
    def workflow_history_view(self):
        return self.construct_view(
            self.workflow_history_view_class,
            template_name=self.get_templates(
                "workflow_history/index",
                fallback=self.workflow_history_view_class.template_name,
            ),
            workflow_history_detail_url_name=self.get_url_name(
                "workflow_history_detail"
            ),
        )

    @property
    def workflow_history_detail_view(self):
        return self.construct_view(
            self.workflow_history_detail_view_class,
            template_name=self.get_templates(
                "workflow_history/detail",
                fallback=self.workflow_history_detail_view_class.template_name,
            ),
            workflow_history_url_name=self.get_url_name("workflow_history"),
        )

    @property
    def chooser_viewset(self):
        return self.chooser_viewset_class(
            self.get_chooser_admin_url_namespace(),
            model=self.model,
            url_prefix=self.get_chooser_admin_base_path(),
            icon=self.icon,
            per_page=self.chooser_per_page,
        )

    @cached_property
    def icon(self):
        return self.get_icon()

    def get_icon(self):
        """
        Returns the icon to be used for the admin views.

        **Deprecated** - the preferred way to customise this is to define an ``icon`` property.
        """
        return "snippet"

    @cached_property
    def menu_label(self):
        return self.get_menu_label()

    def get_menu_label(self):
        """
        Returns the label text to be used for the menu item.

        **Deprecated** - the preferred way to customise this is to define a ``menu_label`` property.
        """
        return capfirst(self.model_opts.verbose_name_plural)

    @cached_property
    def menu_name(self):
        return self.get_menu_name()

    def get_menu_name(self):
        """
        Returns the name to be used for the menu item.

        **Deprecated** - the preferred way to customise this is to define a ``menu_name`` property.
        """
        return ""

    @cached_property
    def menu_icon(self):
        return self.get_menu_icon()

    def get_menu_icon(self):
        """
        Returns the icon to be used for the menu item.

        **Deprecated** - the preferred way to customise this is to define a ``menu_icon`` property.
        """
        return self.icon

    @cached_property
    def menu_order(self):
        return self.get_menu_order()

    def get_menu_order(self):
        """
        Returns the ordering number to be applied to the menu item.

        **Deprecated** - the preferred way to customise this is to define a ``menu_order`` property.
        """
        # By default, put it at the last item before Reports, whose order is 9000.
        return 8999

    def get_menu_item_is_registered(self):
        return self.menu_item_is_registered

    @property
    def breadcrumbs_items(self):
        # Use reverse_lazy instead of reverse
        # because this will be passed to the view classes at startup
        breadcrumbs = [
            {"url": reverse_lazy("wagtailadmin_home"), "label": _("Home")},
        ]

        if (
            getattr(settings, "WAGTAILSNIPPETS_MENU_SHOW_ALL", False)
            or not self.get_menu_item_is_registered()
        ):
            breadcrumbs.append(
                {"url": reverse_lazy("wagtailsnippets:index"), "label": _("Snippets")},
            )

        return breadcrumbs

    def get_queryset(self, request):
        """
        Returns a QuerySet of all model instances to be shown on the index view.
        If ``None`` is returned (the default), the logic in
        ``index_view.get_base_queryset()`` will be used instead.
        """
        return None

    @cached_property
    def index_template_name(self):
        return self.get_index_template()

    def get_index_template(self):
        """
        Returns a template to be used when rendering ``index_view``. If a
        template is specified by the ``index_template_name`` attribute, that will
        be used. Otherwise, a list of preferred template names are returned.

        **Deprecated** - the preferred way to customise this is to define an ``index_template_name`` property.
        """
        return self.get_templates("index")

    @cached_property
    def index_results_template_name(self):
        return self.get_index_results_template()

    def get_index_results_template(self):
        """
        Returns a template to be used when rendering ``index_results_view``. If a
        template is specified by the ``index_results_template_name`` attribute, that will
        be used. Otherwise, a list of preferred template names are returned.

        **Deprecated** - the preferred way to customise this is to define an ``index_results_template_name`` property.
        """
        return self.get_templates("index_results")

    @cached_property
    def create_template_name(self):
        return self.get_create_template()

    def get_create_template(self):
        """
        Returns a template to be used when rendering ``add_view``. If a
        template is specified by the ``create_template_name`` attribute, that will
        be used. Otherwise, a list of preferred template names are returned.

        **Deprecated** - the preferred way to customise this is to define a ``create_template_name`` property.
        """
        return self.get_templates("create")

    @cached_property
    def edit_template_name(self):
        return self.get_edit_template()

    def get_edit_template(self):
        """
        Returns a template to be used when rendering ``edit_view``. If a
        template is specified by the ``edit_template_name`` attribute, that will
        be used. Otherwise, a list of preferred template names are returned.

        **Deprecated** - the preferred way to customise this is to define an ``edit_template_name`` property.
        """
        return self.get_templates("edit")

    @cached_property
    def delete_template_name(self):
        return self.get_delete_template()

    def get_delete_template(self):
        """
        Returns a template to be used when rendering ``delete_view``. If a
        template is specified by the ``delete_template_name`` attribute, that will
        be used. Otherwise, a list of preferred template names are returned.

        **Deprecated** - the preferred way to customise this is to define a ``delete_template_name`` property.
        """
        return self.get_templates("delete")

    @cached_property
    def history_template_name(self):
        """
        A template to be used when rendering ``history_view``.

        Default: if :attr:`template_prefix` is specified, a ``history.html``
        template in the prefix directory and its ``{app_label}/{model_name}/``
        or ``{app_label}/`` subdirectories will be used. Otherwise, the
        ``history_view_class.template_name`` will be used.
        """
        return self.get_history_template()

    def get_history_template(self):
        """
        Returns a template to be used when rendering ``history_view``. If a
        template is specified by the ``history_template_name`` attribute, that will
        be used. Otherwise, a list of preferred template names are returned.

        **Deprecated** - the preferred way to customise this is to define a ``history_template_name`` property.
        """
        return self.get_templates("history")

    @cached_property
    def inspect_template_name(self):
        """
        A template to be used when rendering ``inspect_view``.

        Default: if :attr:`template_prefix` is specified, an ``inspect.html``
        template in the prefix directory and its ``{app_label}/{model_name}/``
        or ``{app_label}/`` subdirectories will be used. Otherwise, the
        ``inspect_view_class.template_name`` will be used.
        """
        return self.get_inspect_template()

    def get_inspect_template(self):
        """
        Returns a template to be used when rendering ``inspect_view``. If a
        template is specified by the ``inspect_template_name`` attribute, that will
        be used. Otherwise, a list of preferred template names are returned.

        **Deprecated** - the preferred way to customise this is to define an ``inspect_template_name`` property.
        """
        return self.get_templates(
            "inspect", fallback=self.inspect_view_class.template_name
        )

    def get_admin_url_namespace(self):
        """
        Returns the URL namespace for the admin URLs for this model.

        **Deprecated** - the preferred way to customise this is to define a ``url_namespace`` property.
        """
        if self.admin_url_namespace:
            return self.admin_url_namespace
        return f"wagtailsnippets_{self.app_label}_{self.model_name}"

    def get_admin_base_path(self):
        """
        Returns the base path for the admin URLs for this model.
        The returned string must not begin or end with a slash.

        **Deprecated** - the preferred way to customise this is to define a ``url_prefix`` property.
        """
        if self.base_url_path:
            return self.base_url_path.strip().strip("/")
        return f"snippets/{self.app_label}/{self.model_name}"

    def get_chooser_admin_url_namespace(self):
        """Returns the URL namespace for the chooser admin URLs for this model."""
        if self.chooser_admin_url_namespace:
            return self.chooser_admin_url_namespace
        return f"wagtailsnippetchoosers_{self.app_label}_{self.model_name}"

    def get_chooser_admin_base_path(self):
        """
        Returns the base path for the chooser admin URLs for this model.
        The returned string must not begin or end with a slash.
        """
        if self.chooser_base_url_path:
            return self.chooser_base_url_path.strip().strip("/")
        return f"snippets/choose/{self.app_label}/{self.model_name}"

    @property
    def url_finder_class(self):
        return type(
            "_SnippetAdminURLFinder", (SnippetAdminURLFinder,), {"model": self.model}
        )

    def get_urlpatterns(self):
        conv = self.pk_path_converter
        urlpatterns = [
            path("", self.index_view, name="list"),
            path("results/", self.index_results_view, name="list_results"),
            path("add/", self.add_view, name="add"),
            path(f"edit/<{conv}:pk>/", self.edit_view, name="edit"),
            path(f"delete/<{conv}:pk>/", self.delete_view, name="delete"),
            path(f"usage/<{conv}:pk>/", self.usage_view, name="usage"),
            path(f"history/<{conv}:pk>/", self.history_view, name="history"),
            path(
                f"history-results/<{conv}:pk>/",
                self.history_results_view,
                name="history_results",
            ),
        ]

        if self.reorder_view_enabled:
            urlpatterns += [
                path(f"reorder/<{conv}:pk>/", self.reorder_view, name="reorder")
            ]

        if self.copy_view_enabled:
            urlpatterns += [path(f"copy/<{conv}:pk>/", self.copy_view, name="copy")]

        if self.inspect_view_enabled:
            urlpatterns += [
                path(f"inspect/<{conv}:pk>/", self.inspect_view, name="inspect")
            ]

        if self.preview_enabled:
            urlpatterns += [
                path("preview/", self.preview_on_add_view, name="preview_on_add"),
                path(
                    f"preview/<{conv}:pk>/",
                    self.preview_on_edit_view,
                    name="preview_on_edit",
                ),
            ]

        if self.revision_enabled:
            if self.preview_enabled:
                urlpatterns += [
                    path(
                        f"history/<{conv}:pk>/revisions/<int:revision_id>/view/",
                        self.revisions_view,
                        name="revisions_view",
                    )
                ]

            urlpatterns += [
                path(
                    f"history/<{conv}:pk>/revisions/<int:revision_id>/revert/",
                    self.revisions_revert_view,
                    name="revisions_revert",
                ),
                re_path(
                    r"history/(?P<pk>.+)/revisions/compare/(?P<revision_id_a>live|earliest|\d+)\.\.\.(?P<revision_id_b>live|latest|\d+)/$",
                    self.revisions_compare_view,
                    name="revisions_compare",
                ),
            ]

        if self.draftstate_enabled:
            urlpatterns += [
                path(
                    f"history/<{conv}:pk>/revisions/<int:revision_id>/unschedule/",
                    self.revisions_unschedule_view,
                    name="revisions_unschedule",
                ),
                path(f"unpublish/<{conv}:pk>/", self.unpublish_view, name="unpublish"),
            ]

        if self.locking_enabled:
            urlpatterns += [
                path(f"lock/<{conv}:pk>/", self.lock_view, name="lock"),
                path(f"unlock/<{conv}:pk>/", self.unlock_view, name="unlock"),
            ]

        if self.workflow_enabled:
            urlpatterns += [
                path(
                    f"workflow/action/<{conv}:pk>/<slug:action_name>/<int:task_state_id>/",
                    self.workflow_action_view,
                    name="workflow_action",
                ),
                path(
                    f"workflow/collect_action_data/<{conv}:pk>/<slug:action_name>/<int:task_state_id>/",
                    self.collect_workflow_action_data_view,
                    name="collect_workflow_action_data",
                ),
                path(
                    f"workflow/confirm_cancellation/<{conv}:pk>/",
                    self.confirm_workflow_cancellation_view,
                    name="confirm_workflow_cancellation",
                ),
                path(
                    f"workflow_history/<{conv}:pk>/",
                    self.workflow_history_view,
                    name="workflow_history",
                ),
                path(
                    f"workflow_history/<{conv}:pk>/detail/<int:workflow_state_id>/",
                    self.workflow_history_detail_view,
                    name="workflow_history_detail",
                ),
            ]

            if self.preview_enabled:
                urlpatterns += [
                    path(
                        f"workflow/preview/<{conv}:pk>/<int:task_id>/",
                        self.workflow_preview_view,
                        name="workflow_preview",
                    ),
                ]

        return urlpatterns

    def get_edit_handler(self):
        """
        Like :meth:`ModelViewSet.get_edit_handler()
        <wagtail.admin.viewsets.model.ModelViewSet.get_edit_handler>`,
        but falls back to extracting panel definitions from the model class
        if no edit handler is defined.
        """
        edit_handler = super().get_edit_handler()
        if edit_handler:
            return edit_handler

        exclude = self.get_exclude_form_fields()
        panels = extract_panel_definitions_from_model_class(self.model, exclude=exclude)
        edit_handler = ObjectList(panels)
        return edit_handler.bind_to_model(self.model)

    def register_chooser_viewset(self):
        viewsets.register(self.chooser_viewset)

    def register_model_check(self):
        def snippets_model_check(app_configs, **kwargs):
            return check_panels_in_model(self.model, "snippets")

        checks.register(snippets_model_check, "panels")

    def register_snippet_model(self):
        snippet_models = get_snippet_models()
        if self.model in snippet_models:
            raise ImproperlyConfigured(
                f"The {self.model.__name__} model is already registered as a snippet"
            )
        snippet_models.append(self.model)
        snippet_models.sort(key=lambda x: x._meta.verbose_name)

    def on_register(self):
        super().on_register()
        # For convenience, attach viewset to the model class to allow accessing
        # the configuration of a given model.
        self.model.snippet_viewset = self
        self.register_chooser_viewset()
        self.register_model_check()
        self.register_snippet_model()


class SnippetViewSetGroup(ModelViewSetGroup):
    """
    A container for grouping together multiple
    :class:`~wagtail.snippets.views.snippets.SnippetViewSet` instances.

    All attributes and methods from
    :class:`~wagtail.admin.viewsets.model.ModelViewSetGroup` are available.
    """

    def __init__(self):
        menu_item_is_registered = getattr(
            self, "menu_item_is_registered", bool(self.menu_hook)
        )
        # If the menu item is registered, mark all viewsets as such so that we can
        # hide the "Snippets" menu item if all snippets have their own menu items.
        for item in self.items:
            item.menu_item_is_registered = menu_item_is_registered

        # Call super() after setting menu_item_is_registered so that nested groups
        # can inherit the value.
        super().__init__()
