import datetime
import json
import logging

from django.contrib import messages
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
from django.db import IntegrityError, models
from django.http import Http404
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse_lazy
from django.utils.datastructures import MultiValueDictKeyError
from django.utils.translation import gettext_lazy as _
from django.views import View
from django.views.generic import CreateView, DeleteView, DetailView, UpdateView
from django.views.generic.edit import DeletionMixin
from django.views.generic.list import ListView

from ..base import (  # get_registered_form_handler_plugins
    fire_form_callbacks,
    form_element_plugin_registry,
    form_handler_plugin_registry,
    form_wizard_handler_plugin_registry,
    get_theme,
    run_form_handlers,
    run_form_wizard_handlers,
    submit_plugin_form_data,
)
from ..constants import (
    CALLBACK_BEFORE_FORM_VALIDATION,
    CALLBACK_FORM_INVALID,
    CALLBACK_FORM_VALID,
    CALLBACK_FORM_VALID_AFTER_FORM_HANDLERS,
    CALLBACK_FORM_VALID_BEFORE_SUBMIT_PLUGIN_FORM_DATA,
)
from ..dynamic import assemble_form_class
from ..form_importers import (
    ensure_autodiscover as ensure_importers_autodiscover,
    form_importer_plugin_registry,
    get_form_importer_plugin_urls,
)
from ..forms import (
    FormElementEntryFormSet,
    FormEntryForm,
    FormWizardEntryForm,
    FormWizardFormEntryFormSet,
    ImportFormEntryForm,
    ImportFormWizardEntryForm,
)
from ..models import (
    FormElementEntry,
    FormEntry,
    FormHandlerEntry,
    FormWizardEntry,
    FormWizardFormEntry,
    FormWizardHandlerEntry,
)
from ..permissions.default import (
    AddFormElementEntryPermission,
    AddFormHandlerEntryPermission,
    CreateFormEntryPermission,
    DeleteFormElementEntryPermission,
    DeleteFormEntryPermission,
    DeleteFormHandlerEntryPermission,
    EditFormElementEntryPermission,
    EditFormEntryPermission,
    EditFormHandlerEntryPermission,
    ViewDashboardPermission,
    ViewFormEntryPermission,
)
from ..settings import DEBUG, GET_PARAM_INITIAL_DATA, SORT_PLUGINS_BY_VALUE
from ..utils import (
    append_edit_and_delete_links_to_field,
    get_user_form_element_plugins_grouped,
    get_user_form_field_plugin_uids,
    get_user_form_handler_plugin_uids,
    get_user_form_handler_plugins,
    get_user_form_wizard_handler_plugin_uids,
    get_user_form_wizard_handler_plugins,
    get_wizard_files_upload_dir,
    perform_form_entry_import,
    prepare_form_entry_export_data,
)

__all__ = (
    "PermissionMixin",
    "AbstractDeletePluginEntryView",
    "CreateFormEntryView",
    "EditFormEntryView",
    "DeleteFormEntryView",
    "AddFormElementEntryView",
    "EditFormElementEntryView",
    "DeleteFormElementEntryView",
    "AddFormHandlerEntryView",
    "EditFormHandlerEntryView",
    "DeleteFormHandlerEntryView",
    "ViewFormEntryView",
    "ViewFormEntrySubmittedView",
    "DashboardView",
)

logger = logging.getLogger(__name__)


# *****************************************************************************
# *****************************************************************************
# *********************************** Generic *********************************
# *****************************************************************************
# *****************************************************************************


class PermissionMixin(View):
    """Mixin for permission-based views."""

    permission_classes: tuple = ()

    def permission_denied(self, request, message=None, code=None):
        """If request is not permitted, raise."""
        raise PermissionDenied()

    def get_permissions(self):
        """Return initialized list of permissions required by this view."""
        return [permission() for permission in self.permission_classes]

    def dispatch(self, request, *args, **kwargs):
        """Dispatch the request."""
        self.check_permissions(request)
        return super(PermissionMixin, self).dispatch(request, *args, **kwargs)

    def check_permissions(self, request):
        """Check if the request should be permitted.

        Raises an appropriate exception if the request is not permitted.
        """
        for permission in self.get_permissions():
            if not permission.has_permission(request, self):
                self.permission_denied(
                    request,
                    message=getattr(permission, "message", None),
                    code=getattr(permission, "code", None),
                )

    def check_object_permissions(self, request, obj):
        """Check if the request should be permitted for a given object.

        Raises an appropriate exception if the request is not permitted.
        """
        for permission in self.get_permissions():
            if not permission.has_object_permission(request, self, obj):
                self.permission_denied(
                    request,
                    message=getattr(permission, "message", None),
                    code=getattr(permission, "code", None),
                )


class AbstractDeletePluginEntryView(PermissionMixin, DeleteView):
    """Abstract delete view for plugin entries."""

    pk_url_kwarg: str
    get_user_plugin_uids_func: callable
    message: str
    html_anchor: str

    def _get_queryset(self, request):
        """Get queryset."""
        return self.model._default_manager.select_related("form_entry").filter(
            form_entry__user__pk=request.user.pk
        )

    def get_object(self, queryset=None):
        """Get object."""
        # TODO: There's a tiny deviation from `_delete_plugin_entry`
        #  implementation. The message in the latter is fully custom, while
        #  in this case we're stuck to Django's own implementation.
        #  Comment added on 2022-07-10.
        obj = get_object_or_404(
            self._get_queryset(self.request),
            pk=self.kwargs.get(self.pk_url_kwarg),
        )
        self.check_object_permissions(self.request, obj)
        return obj

    # Add support for browsers which only accept GET and POST for now.
    def get(self, request, *args, **kwargs):
        return self.delete(request, *args, **kwargs)

    def delete(self, request, *args, **kwargs):
        """Delete."""
        self.object = self.get_object()
        self._run_before_plugin_entry_delete(request, self.object)
        form_entry = self.object.form_entry
        plugin = self.object.get_plugin(request=request)
        plugin.request = request

        plugin._delete_plugin_data()

        self.object.delete()

        self._run_after_plugin_entry_delete(
            request, self.kwargs.get("form_entry_id")
        )
        messages.info(request, self.message.format(plugin.name))
        redirect_url = reverse_lazy(
            "fobi.edit_form_entry", kwargs={"form_entry_id": form_entry.pk}
        )
        return redirect("{0}{1}".format(redirect_url, self.html_anchor))

    def _run_before_plugin_entry_delete(self, request, form_entry):
        """Run just before plugin entry has been deleted."""
        try:
            self.run_before_plugin_entry_delete(request, form_entry)
            return True
        except:
            return False

    def run_before_plugin_entry_delete(self, request, form_entry):
        """Run just before plugin entry has been deleted."""

    def _run_after_plugin_entry_delete(self, request, form_entry_id):
        """Run after plugin entry has been deleted."""
        try:
            self.run_after_plugin_entry_delete(request, form_entry_id)
            return True
        except:
            return False

    def run_after_plugin_entry_delete(self, request, form_entry_id):
        """Run after plugin entry has been deleted."""


# *****************************************************************************
# *****************************************************************************
# ******************************** Dashboards *********************************
# *****************************************************************************
# *****************************************************************************

# *****************************************************************************
# ********************************** Forms ************************************
# *****************************************************************************


class DashboardView(ListView):
    """Dashboard view."""

    template_name = None
    theme = None
    permission_classes = (ViewDashboardPermission,)

    def get_queryset(self):
        """
        Return the list of items for this view.

        The return value must be an iterable and may be an instance of
        `QuerySet` in which case `QuerySet` specific behavior will be enabled.
        """
        queryset = FormEntry._default_manager.filter(
            user__pk=self.request.user.pk
        )
        ordering = self.get_ordering()
        if ordering:
            if isinstance(ordering, str):
                ordering = (ordering,)
            queryset = queryset.order_by(*ordering)

        return queryset

    def get_context_data(self, **kwargs):
        """Get context data."""
        context = super(DashboardView, self).get_context_data(**kwargs)
        context.update(
            {
                "form_entries": self.get_queryset(),
                "form_importers": get_form_importer_plugin_urls(),
            }
        )
        if not self.theme:
            theme = get_theme(request=self.request, as_instance=True)
        else:
            theme = self.theme

        if theme:
            context.update({"fobi_theme": theme})
        return context

    def get_template_names(self):
        """Get template names."""
        template_name = self.template_name
        if not template_name:
            if not self.theme:
                theme = get_theme(request=self.request, as_instance=True)
            else:
                theme = self.theme
            template_name = theme.dashboard_template
        return [template_name]


# *****************************************************************************
# *****************************************************************************
# ********************************** Builder **********************************
# *****************************************************************************
# *****************************************************************************

# *****************************************************************************
# **************************** Create form entry ******************************
# *****************************************************************************


class CreateFormEntryView(PermissionMixin, CreateView):
    """Create form entry."""

    template_name = None
    form_class = FormEntryForm
    theme = None
    permission_classes = (CreateFormEntryPermission,)

    def get_context_data(self, **kwargs):
        """Get context data."""
        context = super(CreateFormEntryView, self).get_context_data(**kwargs)
        context["form"] = self.get_form()
        if not self.theme:
            theme = get_theme(request=self.request, as_instance=True)
        else:
            theme = self.theme

        if theme:
            context.update({"fobi_theme": theme})
        return context

    def get_template_names(self):
        """Get template names."""
        template_name = self.template_name
        if not template_name:
            if not self.theme:
                theme = get_theme(request=self.request, as_instance=True)
            else:
                theme = self.theme
            template_name = theme.create_form_entry_template
        return [template_name]

    def get_form_kwargs(self):
        kwargs = super(CreateFormEntryView, self).get_form_kwargs()
        kwargs["request"] = self.request
        return kwargs

    def get(self, request, *args, **kwargs):
        """Handle GET requests: instantiate a blank version of the form."""
        self.object = None
        return self.render_to_response(self.get_context_data())

    def post(self, request, *args, **kwargs):
        """
        Handle POST requests: instantiate a form instance with the passed
        POST variables and then check if it's valid.
        """
        self.object = None
        form = self.get_form()
        if form.is_valid():
            form_entry = form.save(commit=False)
            form_entry.user = request.user
            self._run_before_form_create(request, form_entry)
            try:
                form_entry.save()
                self._run_after_form_create(request, form_entry)
                messages.info(
                    request,
                    _("Form {0} was created successfully.").format(
                        form_entry.name
                    ),
                )
                return redirect(
                    "fobi.edit_form_entry", form_entry_id=form_entry.pk
                )
            except IntegrityError as err:
                messages.info(
                    request,
                    _("Errors occurred while saving the form: {0}.").format(
                        str(err)
                    ),
                )

        return self.render_to_response(self.get_context_data())

    def _run_before_form_create(self, request, form_entry):
        """Run just before form_entry has been created/saved."""
        try:
            self.run_before_form_create(request, form_entry)
            return True
        except:
            return False

    def run_before_form_create(self, request, form_entry):
        """Run just before form_entry has been created/saved."""

    def _run_after_form_create(self, request, form_entry):
        """Run after form_entry has been created/saved."""
        try:
            self.run_after_form_create(request, form_entry)
            return True
        except:
            return False

    def run_after_form_create(self, request, form_entry):
        """Run after the form_entry has been created/saved."""


# **************************************************************************
# ******************************* Edit form entry **************************
# **************************************************************************


class EditFormEntryView(PermissionMixin, UpdateView):
    """Edit form entry."""

    template_name = None
    form_class = FormEntryForm
    theme = None
    pk_url_kwarg = "form_entry_id"
    permission_classes = (EditFormEntryPermission,)

    def get_context_data(self, **kwargs):
        """Get context data."""
        context = super(EditFormEntryView, self).get_context_data(**kwargs)

        # In case of success, we don't need this (since redirect would happen).
        # Thus, fetch only if needed.
        form_elements = self.object.formelemententry_set.all()
        form_handlers = self.object.formhandlerentry_set.all()[:]
        used_form_handler_uids = [
            form_handler.plugin_uid for form_handler in form_handlers
        ]

        # The code below (two lines below) is not really used at the moment,
        # thus - comment out, but do not remove, as we might need it later on.
        # all_form_entries = FormEntry._default_manager \
        #                            .only('id', 'name', 'slug') \
        #                            .filter(user__pk=request.user.pk)

        # List of form element plugins allowed to user
        user_form_element_plugins = get_user_form_element_plugins_grouped(
            self.request.user, sort_by_value=SORT_PLUGINS_BY_VALUE
        )
        # List of form handler plugins allowed to user
        user_form_handler_plugins = get_user_form_handler_plugins(
            self.request.user,
            exclude_used_singles=True,
            used_form_handler_plugin_uids=used_form_handler_uids,
        )

        # Assembling the form for preview
        form_cls = assemble_form_class(
            self.object,
            origin="edit_form_entry",
            origin_kwargs_update_func=append_edit_and_delete_links_to_field,
            request=self.request,
        )

        assembled_form = form_cls()

        # In debug mode, try to identify possible problems.
        if DEBUG:
            assembled_form.as_p()
        else:
            try:
                assembled_form.as_p()
            except Exception as err:
                logger.error(err)

        # If no theme provided, pick a default one.
        if not self.theme:
            theme = get_theme(request=self.request, as_instance=True)
        else:
            theme = self.theme

        if theme:
            context.update({"fobi_theme": theme})

        theme.collect_plugin_media(form_elements)

        context.update(
            {
                "form": self.get_form(),
                "form_entry": self.object,
                "form_elements": form_elements,
                "form_handlers": form_handlers,
                "user_form_element_plugins": user_form_element_plugins,
                "user_form_handler_plugins": user_form_handler_plugins,
                "assembled_form": assembled_form,
                "fobi_theme": theme,
            }
        )

        return context

    def get_template_names(self):
        """Get template names."""
        template_name = self.template_name
        if not template_name:
            if not self.theme:
                theme = get_theme(request=self.request, as_instance=True)
            else:
                theme = self.theme
            template_name = theme.edit_form_entry_template
        return [template_name]

    def get_form_kwargs(self):
        kwargs = super(EditFormEntryView, self).get_form_kwargs()
        kwargs["request"] = self.request
        return kwargs

    def _get_queryset(self, request):
        """Get queryset."""
        return (
            FormEntry._default_manager.select_related("user")
            .prefetch_related("formelemententry_set")
            .filter(user__pk=request.user.pk)
        )

    def get_object(self, queryset=None):
        """Get object."""
        obj = super(EditFormEntryView, self).get_object(queryset)
        self.check_object_permissions(self.request, obj)
        return obj

    def get(self, request, *args, **kwargs):
        self.object = self.get_object(queryset=self._get_queryset(request))
        """Handle GET requests: instantiate a blank version of the form."""
        form_element_entry_formset = FormElementEntryFormSet(
            queryset=self.object.formelemententry_set.all(),
            # prefix='form_element'
        )
        return self.render_to_response(
            self.get_context_data(
                form_element_entry_formset=form_element_entry_formset,
            )
        )

    def post(self, request, *args, **kwargs):
        """
        Handle POST requests: instantiate a form instance with the passed
        POST variables and then check if it's valid.
        """
        self.object = self.get_object(queryset=self._get_queryset(request))
        form = self.get_form()

        # This is where we save ordering if it has been changed.
        # The `FormElementEntryFormSet` contain ids and positions only.
        if "ordering" in request.POST:
            form_element_entry_formset = FormElementEntryFormSet(
                request.POST,
                request.FILES,
                queryset=self.object.formelemententry_set.all(),
                # prefix = 'form_element'
            )
            # If form elements aren't properly made (developers's fault)
            # there might be problems with saving the ordering - likely
            # in case of hidden elements only. Thus, we want to avoid
            # errors here.
            try:
                if form_element_entry_formset.is_valid():
                    form_element_entry_formset.save()
                    messages.info(
                        request, _("Elements ordering edited successfully.")
                    )
                    return redirect(
                        reverse_lazy(
                            "fobi.edit_form_entry",
                            kwargs={"form_entry_id": self.object.pk},
                        )
                    )
            except MultiValueDictKeyError as err:
                messages.error(
                    request,
                    _(
                        "Errors occurred while trying to change the "
                        "elements ordering!"
                    ),
                )
                return redirect(
                    reverse_lazy(
                        "fobi.edit_form_entry",
                        kwargs={"form_entry_id": self.object.pk},
                    )
                )
        else:
            form_element_entry_formset = FormElementEntryFormSet(
                queryset=self.object.formelemententry_set.all(),
                # prefix='form_element'
            )

        if form.is_valid():
            obj = form.save(commit=False)
            obj.user = request.user
            try:
                obj.save()
                messages.info(
                    request,
                    _("Form {0} was edited successfully.").format(obj.name),
                )
                return redirect(
                    reverse_lazy(
                        "fobi.edit_form_entry", kwargs={"form_entry_id": obj.pk}
                    )
                )
            except IntegrityError as err:
                messages.info(
                    request,
                    _("Errors occurred while saving the form: {0}.").format(
                        str(err)
                    ),
                )

        return self.render_to_response(
            self.get_context_data(
                form_element_entry_formset=form_element_entry_formset,
            )
        )


# *****************************************************************************
# ********************************* Delete form entry *************************
# *****************************************************************************


class DeleteFormEntryView(PermissionMixin, DeletionMixin):
    """Delete form entry."""

    model = FormEntry
    success_url = reverse_lazy("fobi.dashboard")
    permission_classes = (DeleteFormEntryPermission,)

    def get_object(self, queryset=None):
        """Get object."""
        obj = get_object_or_404(
            FormEntry._default_manager.all(),
            pk=self.kwargs.get("form_entry_id"),
            user__pk=self.request.user.pk,
        )
        self.check_object_permissions(self.request, obj)
        return obj

    # Add support for browsers which only accept GET and POST for now.
    def get(self, request, *args, **kwargs):
        return self.delete(request, *args, **kwargs)

    def delete(self, request, *args, **kwargs):
        """Delete."""
        self.object = self.get_object()
        success_url = self.get_success_url()
        self._run_before_form_delete(request, self.object)
        self.object.delete()
        self._run_after_form_delete(request, self.kwargs.get("form_entry_id"))
        messages.info(
            request,
            _("Form {0} was deleted successfully.").format(self.object.name),
        )
        return redirect(success_url)

    def _run_before_form_delete(self, request, form_entry):
        """Run just before form_entry has been deleted."""
        try:
            self.run_before_form_delete(request, form_entry)
            return True
        except:
            return False

    def run_before_form_delete(self, request, form_entry):
        """Run just before form_entry has been deleted."""

    def _run_after_form_delete(self, request, form_entry_id):
        """Run after form_entry has been deleted."""
        try:
            self.run_after_form_delete(request, form_entry_id)
            return True
        except:
            return False

    def run_after_form_delete(self, request, form_entry_id):
        """Run after form_entry has been deleted."""


# *****************************************************************************
# **************************** Add form element entry *************************
# *****************************************************************************


class AddFormElementEntryView(PermissionMixin, CreateView):
    """Add form element entry."""

    template_name = None
    form_class = None
    theme = None
    permission_classes = (AddFormElementEntryPermission,)

    def get_essential_objects(
        self,
        form_entry_id,
        form_element_plugin_uid,
        request,
    ):
        """Get essential objects."""
        try:
            form_entry = FormEntry._default_manager.prefetch_related(
                "formelemententry_set"
            ).get(pk=form_entry_id)
        except ObjectDoesNotExist as err:
            raise Http404(_("Form entry not found."))

        form_elements = form_entry.formelemententry_set.all()

        user_form_element_plugin_uids = get_user_form_field_plugin_uids(
            request.user
        )

        if form_element_plugin_uid not in user_form_element_plugin_uids:
            raise Http404(
                _(
                    "Plugin does not exist or you are not allowed "
                    "to use this plugin!"
                )
            )

        form_element_plugin_cls = form_element_plugin_registry.get(
            form_element_plugin_uid
        )
        form_element_plugin = form_element_plugin_cls(user=request.user)
        form_element_plugin.request = request

        form_element_plugin_form_cls = form_element_plugin.get_form()
        # form = None

        obj = FormElementEntry()
        obj.form_entry = form_entry
        obj.plugin_uid = form_element_plugin_uid
        obj.user = request.user

        return (
            form_entry,
            form_elements,
            form_element_plugin_cls,
            form_element_plugin,
            form_element_plugin_form_cls,
            user_form_element_plugin_uids,
            obj,
        )

    def do_save_object(
        self, form_entry_id, form_entry, obj, form_element_plugin, request
    ):
        """Do save object."""
        # Handling the position
        position = 1
        records = FormElementEntry.objects.filter(
            form_entry=form_entry
        ).aggregate(models.Max("position"))
        if records:
            try:
                position = records["{0}__max".format("position")] + 1

            except TypeError as err:
                pass

        obj.position = position

        # Save the object.
        obj.save()

        messages.info(
            request,
            _(
                'The form element plugin "{0}" was added ' "successfully."
            ).format(form_element_plugin.name),
        )
        return redirect(
            "{0}?active_tab=tab-form-elements".format(
                reverse_lazy(
                    "fobi.edit_form_entry",
                    kwargs={"form_entry_id": form_entry_id},
                )
            )
        )

    def get_context_data(self, **kwargs):
        """Get context data."""
        context = super(AddFormElementEntryView, self).get_context_data(
            **kwargs
        )

        if not self.theme:
            theme = get_theme(request=self.request, as_instance=True)
        else:
            theme = self.theme

        if theme:
            context.update({"fobi_theme": theme})
        return context

    def get_template_names(self):
        """Get template names."""
        template_name = self.template_name
        if not template_name:
            if not self.theme:
                theme = get_theme(request=self.request, as_instance=True)
            else:
                theme = self.theme
            template_name = theme.add_form_element_entry_template
        return [template_name]

    def get(self, request, *args, **kwargs):
        """Handle GET requests: instantiate a blank version of the form."""
        self.object = None
        (
            form_entry,
            form_elements,
            form_element_plugin_cls,
            form_element_plugin,
            form_element_plugin_form_cls,
            user_form_element_plugin_uids,
            obj,
        ) = self.get_essential_objects(
            self.kwargs.get("form_entry_id"),
            self.kwargs.get("form_element_plugin_uid"),
            self.request,
        )

        save_object = False
        if not form_element_plugin_form_cls:
            save_object = True

        if not save_object:
            form = form_element_plugin.get_initialised_create_form_or_404()

        if save_object:
            return self.do_save_object(
                self.kwargs.get("form_entry_id"),
                form_entry,
                obj,
                form_element_plugin,
                request,
            )

        return self.render_to_response(
            self.get_context_data(
                form=form,
                form_entry=form_entry,
                form_element_plugin=form_element_plugin,
            )
        )

    def post(self, request, *args, **kwargs):
        """
        Handle POST requests: instantiate a form instance with the passed
        POST variables and then check if it's valid.
        """
        self.object = None
        (
            form_entry,
            form_elements,
            form_element_plugin_cls,
            form_element_plugin,
            form_element_plugin_form_cls,
            user_form_element_plugin_uids,
            obj,
        ) = self.get_essential_objects(
            self.kwargs.get("form_entry_id"),
            self.kwargs.get("form_element_plugin_uid"),
            self.request,
        )

        save_object = False
        if not form_element_plugin_form_cls:
            save_object = True

        if not save_object:
            form = form_element_plugin.get_initialised_create_form_or_404(
                data=request.POST, files=request.FILES
            )
            form.validate_plugin_data(form_elements, request=request)
            if form.is_valid():
                # Saving the plugin form data.
                form.save_plugin_data(request=request)

                # Getting the plugin data.
                obj.plugin_data = form.get_plugin_data(request=request)
                save_object = True

        if save_object:
            return self.do_save_object(
                self.kwargs.get("form_entry_id"),
                form_entry,
                obj,
                form_element_plugin,
                request,
            )

        return self.render_to_response(
            self.get_context_data(
                form=form,
                form_entry=form_entry,
                form_element_plugin=form_element_plugin,
            )
        )


# *****************************************************************************
# **************************** Edit form element entry ************************
# *****************************************************************************


class EditFormElementEntryView(PermissionMixin, UpdateView):
    """Edit form element entry view."""

    template_name = None
    form_class = None
    theme = None
    pk_url_kwarg = "form_element_entry_id"
    permission_classes = (EditFormElementEntryPermission,)

    def _get_queryset(self, request):
        """Get queryset."""
        return FormElementEntry._default_manager.select_related(
            "form_entry", "form_entry__user"
        ).filter(form_entry__user__pk=request.user.pk)

    def get_object(self, queryset=None):
        """Get object."""
        obj = super(EditFormElementEntryView, self).get_object(queryset)
        self.check_object_permissions(self.request, obj)
        return obj

    def get_essential_objects(
        self,
        form_element_entry_id,
        request,
    ):
        """Get essential objects."""
        try:
            obj = self.get_object(queryset=self._get_queryset(request))
        except ObjectDoesNotExist as err:
            raise Http404(_("Form element entry not found."))

        form_entry = obj.form_entry
        form_element_plugin = obj.get_plugin(request=request)
        form_element_plugin.request = request

        form_element_plugin_form_cls = form_element_plugin.get_form()

        return (
            form_entry,
            form_element_entry_id,
            form_element_plugin,
            form_element_plugin_form_cls,
            obj,
        )

    def get_context_data(self, **kwargs):
        """Get context data."""
        context = super(EditFormElementEntryView, self).get_context_data(
            **kwargs
        )

        if not self.theme:
            theme = get_theme(request=self.request, as_instance=True)
        else:
            theme = self.theme

        if theme:
            context.update({"fobi_theme": theme})
        return context

    def get_template_names(self):
        """Get template names."""
        template_name = self.template_name
        if not template_name:
            if not self.theme:
                theme = get_theme(request=self.request, as_instance=True)
            else:
                theme = self.theme
            template_name = theme.edit_form_element_entry_template
        return [template_name]

    def get(self, request, *args, **kwargs):
        """Handle GET requests: instantiate a blank version of the form."""
        (
            form_entry,
            form_element_entry_id,
            form_element_plugin,
            form_element_plugin_form_cls,
            obj,
        ) = self.get_essential_objects(
            self.kwargs.get("form_element_entry_id"),
            self.request,
        )
        self.object = obj
        form = None

        if not form_element_plugin_form_cls:
            messages.info(
                request,
                _(
                    'The form element plugin "{0}" ' "is not configurable!"
                ).format(form_element_plugin.name),
            )
            return redirect("fobi.edit_form_entry", form_entry_id=form_entry.pk)

        else:
            form = form_element_plugin.get_initialised_edit_form_or_404()

        return self.render_to_response(
            self.get_context_data(
                form=form,
                form_entry=form_entry,
                form_element_plugin=form_element_plugin,
            )
        )

    def post(self, request, *args, **kwargs):
        """
        Handle POST requests: instantiate a form instance with the passed
        POST variables and then check if it's valid.
        """
        self.object = None
        (
            form_entry,
            form_element_entry_id,
            form_element_plugin,
            form_element_plugin_form_cls,
            obj,
        ) = self.get_essential_objects(
            self.kwargs.get("form_element_entry_id"),
            self.request,
        )
        self.object = obj

        if not form_element_plugin_form_cls:
            messages.info(
                request,
                _(
                    'The form element plugin "{0}" ' "is not configurable!"
                ).format(form_element_plugin.name),
            )
            return redirect("fobi.edit_form_entry", form_entry_id=form_entry.pk)

        form = form_element_plugin.get_initialised_edit_form_or_404(
            data=request.POST, files=request.FILES
        )

        form_elements = (
            FormElementEntry._default_manager.select_related(
                "form_entry", "form_entry__user"
            )
            .exclude(pk=form_element_entry_id)
            .filter(form_entry=form_entry)
        )

        form.validate_plugin_data(form_elements, request=request)

        if form.is_valid():
            # Saving the plugin form data.
            form.save_plugin_data(request=request)

            # Getting the plugin data.
            obj.plugin_data = form.get_plugin_data(request=request)

            # Save the object.
            obj.save()

            messages.info(
                request,
                _(
                    'The form element plugin "{0}" was edited ' "successfully."
                ).format(form_element_plugin.name),
            )

            return redirect("fobi.edit_form_entry", form_entry_id=form_entry.pk)

        return self.render_to_response(
            self.get_context_data(
                form=form,
                form_entry=form_entry,
                form_element_plugin=form_element_plugin,
            )
        )


# *****************************************************************************
# **************************** Delete form element entry **********************
# *****************************************************************************


class DeleteFormElementEntryView(AbstractDeletePluginEntryView):
    """Delete form element entry."""

    model = FormElementEntry
    permission_classes = (DeleteFormElementEntryPermission,)
    pk_url_kwarg = "form_element_entry_id"
    get_user_plugin_uids_func = get_user_form_field_plugin_uids
    message = _('The form element plugin "{0}" was deleted successfully.')
    html_anchor = "?active_tab=tab-form-elements"


# *****************************************************************************
# **************************** Add form handler entry *************************
# *****************************************************************************


class AddFormHandlerEntryView(PermissionMixin, CreateView):
    """Add form handler entry."""

    template_name = None
    form_class = None
    theme = None
    permission_classes = (AddFormHandlerEntryPermission,)

    def get_essential_objects(
        self,
        form_entry_id,
        form_handler_plugin_uid,
        request,
    ):
        """Get essential objects."""
        try:
            form_entry = FormEntry._default_manager.get(pk=form_entry_id)
        except ObjectDoesNotExist as err:
            raise Http404(_("Form entry not found."))

        # TODO: Form handlers don't have this, while form elements do.
        #  Find out whether including this improves performance.
        #  Comment added on 2022-07-10.
        # form_elements = form_entry.formelemententry_set.all()

        user_form_handler_plugin_uids = get_user_form_handler_plugin_uids(
            request.user
        )

        if form_handler_plugin_uid not in user_form_handler_plugin_uids:
            raise Http404(
                _(
                    "Plugin does not exist or you are not allowed "
                    "to use this plugin!"
                )
            )

        form_handler_plugin_cls = form_handler_plugin_registry.get(
            form_handler_plugin_uid
        )

        # Check if we deal with form handler plugin that is only allowed to be
        # used once. In that case, check if it has been used already in the
        # current form entry.
        if not form_handler_plugin_cls.allow_multiple:
            times_used = FormHandlerEntry._default_manager.filter(
                form_entry__id=form_entry_id,
                plugin_uid=form_handler_plugin_cls.uid,
            ).count()
            if times_used > 0:
                raise Http404(
                    _(
                        "The {0} plugin can be used only once in a " "form."
                    ).format(form_handler_plugin_cls.name)
                )

        form_handler_plugin = form_handler_plugin_cls(user=request.user)
        form_handler_plugin.request = request

        form_handler_plugin_form_cls = form_handler_plugin.get_form()

        obj = FormHandlerEntry()
        obj.form_entry = form_entry
        obj.plugin_uid = form_handler_plugin_uid
        obj.user = request.user

        return (
            form_entry,
            # form_elements,
            form_handler_plugin_cls,
            form_handler_plugin,
            form_handler_plugin_form_cls,
            user_form_handler_plugin_uids,
            obj,
        )

    def do_save_object(
        self, form_entry_id, form_entry, obj, form_handler_plugin, request
    ):
        """Do save object."""
        # Save the object.
        obj.save()

        messages.info(
            request,
            _(
                'The form handler plugin "{0}" was added ' "successfully."
            ).format(form_handler_plugin.name),
        )
        return redirect(
            "{0}?active_tab=tab-form-handlers".format(
                reverse_lazy(
                    "fobi.edit_form_entry",
                    kwargs={"form_entry_id": form_entry_id},
                )
            )
        )

    def get_context_data(self, **kwargs):
        """Get context data."""
        context = super(AddFormHandlerEntryView, self).get_context_data(
            **kwargs
        )

        if not self.theme:
            theme = get_theme(request=self.request, as_instance=True)
        else:
            theme = self.theme

        if theme:
            context.update({"fobi_theme": theme})
        return context

    def get_template_names(self):
        """Get template names."""
        template_name = self.template_name
        if not template_name:
            if not self.theme:
                theme = get_theme(request=self.request, as_instance=True)
            else:
                theme = self.theme
            template_name = theme.add_form_handler_entry_template
        return [template_name]

    def get(self, request, *args, **kwargs):
        """Handle GET requests: instantiate a blank version of the form."""
        self.object = None
        (
            form_entry,
            # form_elements,
            form_handler_plugin_cls,
            form_handler_plugin,
            form_handler_plugin_form_cls,
            user_form_handler_plugin_uids,
            obj,
        ) = self.get_essential_objects(
            self.kwargs.get("form_entry_id"),
            self.kwargs.get("form_handler_plugin_uid"),
            self.request,
        )

        save_object = False
        if not form_handler_plugin_form_cls:
            save_object = True

        if not save_object:
            form = form_handler_plugin.get_initialised_create_form_or_404()

        if save_object:
            return self.do_save_object(
                self.kwargs.get("form_entry_id"),
                form_entry,
                obj,
                form_handler_plugin,
                request,
            )

        return self.render_to_response(
            self.get_context_data(
                form=form,
                form_entry=form_entry,
                form_handler_plugin=form_handler_plugin,
            )
        )

    def post(self, request, *args, **kwargs):
        """
        Handle POST requests: instantiate a form instance with the passed
        POST variables and then check if it's valid.
        """
        self.object = None
        (
            form_entry,
            # form_elements,
            form_handler_plugin_cls,
            form_handler_plugin,
            form_handler_plugin_form_cls,
            user_form_handler_plugin_uids,
            obj,
        ) = self.get_essential_objects(
            self.kwargs.get("form_entry_id"),
            self.kwargs.get("form_handler_plugin_uid"),
            self.request,
        )

        save_object = False
        if not form_handler_plugin_form_cls:
            save_object = True

        if not save_object:
            form = form_handler_plugin.get_initialised_create_form_or_404(
                data=request.POST, files=request.FILES
            )
            # TODO: Form handlers don't have this, while form elements do.
            #  Find out whether this is something that could be correct
            #  for form handlers.
            # form.validate_plugin_data(form_elements, request=request)
            if form.is_valid():
                # Saving the plugin form data.
                form.save_plugin_data(request=request)

                # Getting the plugin data.
                obj.plugin_data = form.get_plugin_data(request=request)
                save_object = True

        if save_object:
            return self.do_save_object(
                self.kwargs.get("form_entry_id"),
                form_entry,
                obj,
                form_handler_plugin,
                request,
            )

        return self.render_to_response(
            self.get_context_data(
                form=form,
                form_entry=form_entry,
                form_handler_plugin=form_handler_plugin,
            )
        )


# *****************************************************************************
# **************************** Edit form handler entry ************************
# *****************************************************************************


class EditFormHandlerEntryView(PermissionMixin, UpdateView):
    """Edit form handler entry view."""

    template_name = None
    form_class = None
    theme = None
    pk_url_kwarg = "form_handler_entry_id"
    permission_classes = (EditFormHandlerEntryPermission,)

    def _get_queryset(self, request):
        """Get queryset."""
        # TODO: The form element entry has also `form_entry__user` in
        #  `seleect_related`. Find out if that something that could
        #  be also be applied here to improve the performance.
        return FormHandlerEntry._default_manager.select_related(
            "form_entry"
        ).filter(form_entry__user__pk=request.user.pk)

    def get_object(self, queryset=None):
        """Get object."""
        obj = super(EditFormHandlerEntryView, self).get_object(queryset)
        self.check_object_permissions(self.request, obj)
        return obj

    def get_essential_objects(
        self,
        form_handler_entry_id,
        request,
    ):
        """Get essential objects."""
        try:
            obj = self.get_object(queryset=self._get_queryset(request))
        except ObjectDoesNotExist as err:
            raise Http404(_("Form element entry not found."))

        form_entry = obj.form_entry
        form_handler_plugin = obj.get_plugin(request=request)
        form_handler_plugin.request = request

        form_handler_plugin_form_cls = form_handler_plugin.get_form()

        return (
            form_entry,
            form_handler_entry_id,
            form_handler_plugin,
            form_handler_plugin_form_cls,
            obj,
        )

    def get_context_data(self, **kwargs):
        """Get context data."""
        context = super(EditFormHandlerEntryView, self).get_context_data(
            **kwargs
        )

        if not self.theme:
            theme = get_theme(request=self.request, as_instance=True)
        else:
            theme = self.theme

        if theme:
            context.update({"fobi_theme": theme})
        return context

    def get_template_names(self):
        """Get template names."""
        template_name = self.template_name
        if not template_name:
            if not self.theme:
                theme = get_theme(request=self.request, as_instance=True)
            else:
                theme = self.theme
            template_name = theme.edit_form_handler_entry_template
        return [template_name]

    def get(self, request, *args, **kwargs):
        """Handle GET requests: instantiate a blank version of the form."""
        (
            form_entry,
            form_handler_entry_id,
            form_handler_plugin,
            form_handler_plugin_form_cls,
            obj,
        ) = self.get_essential_objects(
            self.kwargs.get("form_handler_entry_id"),
            self.request,
        )
        self.object = obj
        form = None

        if not form_handler_plugin_form_cls:
            messages.info(
                request,
                _(
                    'The form handler plugin "{0}" is not ' "configurable!"
                ).format(form_handler_plugin.name),
            )
            return redirect("fobi.edit_form_entry", form_entry_id=form_entry.pk)

        else:
            form = form_handler_plugin.get_initialised_edit_form_or_404()

        return self.render_to_response(
            self.get_context_data(
                form=form,
                form_entry=form_entry,
                form_handler_plugin=form_handler_plugin,
            )
        )

    def post(self, request, *args, **kwargs):
        """
        Handle POST requests: instantiate a form instance with the passed
        POST variables and then check if it's valid.
        """
        self.object = None
        (
            form_entry,
            form_handler_entry_id,
            form_handler_plugin,
            form_handler_plugin_form_cls,
            obj,
        ) = self.get_essential_objects(
            self.kwargs.get("form_handler_entry_id"),
            self.request,
        )
        self.object = obj

        if not form_handler_plugin_form_cls:
            messages.info(
                request,
                _(
                    'The form handler plugin "{0}" is not ' "configurable!"
                ).format(form_handler_plugin.name),
            )
            return redirect("fobi.edit_form_entry", form_entry_id=form_entry.pk)

        form = form_handler_plugin.get_initialised_edit_form_or_404(
            data=request.POST, files=request.FILES
        )

        if form.is_valid():
            # Saving the plugin form data.
            form.save_plugin_data(request=request)

            # Getting the plugin data.
            obj.plugin_data = form.get_plugin_data(request=request)

            # Save the object.
            obj.save()

            messages.info(
                request,
                _(
                    'The form handler plugin "{0}" was edited ' "successfully."
                ).format(form_handler_plugin.name),
            )

            return redirect(
                "{0}?active_tab=tab-form-handlers".format(
                    reverse_lazy(
                        "fobi.edit_form_entry",
                        kwargs={"form_entry_id": form_entry.pk},
                    )
                )
            )

        return self.render_to_response(
            self.get_context_data(
                form=form,
                form_entry=form_entry,
                form_handler_plugin=form_handler_plugin,
            )
        )


# *****************************************************************************
# **************************** Delete form handler entry **********************
# *****************************************************************************


class DeleteFormHandlerEntryView(AbstractDeletePluginEntryView):
    """Delete form handler entry."""

    model = FormHandlerEntry
    permission_classes = (DeleteFormHandlerEntryPermission,)
    pk_url_kwarg = "form_handler_entry_id"
    get_user_plugin_uids_func = get_user_form_handler_plugin_uids
    message = _('The form handler plugin "{0}" was deleted successfully.')
    html_anchor = "?active_tab=tab-form-handlers"


# *****************************************************************************
# *****************************************************************************
# ******************************** View form entry ****************************
# *****************************************************************************
# *****************************************************************************

# *****************************************************************************
# ******************************** View form entry ****************************
# *****************************************************************************


class AbstractViewFormEntryView(PermissionMixin, DetailView):
    """Abstract view form entry."""

    model = FormEntry
    slug_url_kwarg = "form_entry_slug"
    template_name = None
    theme = None
    permission_classes = (ViewFormEntryPermission,)

    def get_object(self, queryset=None):
        """Get object."""
        if queryset is None:
            queryset = self._get_queryset(request=self.request)
        obj = super(AbstractViewFormEntryView, self).get_object(
            queryset=queryset,
        )
        self.check_object_permissions(self.request, obj)
        return obj

    def _get_queryset(self, request):
        """Get queryset."""
        queryset = FormEntry._default_manager.all().select_related("user")
        if not request.user.is_authenticated:
            queryset = queryset.filter(is_public=True)
        return queryset


class ViewFormEntryView(AbstractViewFormEntryView):
    """View created form."""

    def get_context_data(self, **kwargs):
        """Get context data."""
        context = super(ViewFormEntryView, self).get_context_data(**kwargs)

        if not self.theme:
            theme = get_theme(request=self.request, as_instance=True)
        else:
            theme = self.theme

        if theme:
            context.update({"fobi_theme": theme})
        return context

    def get_template_names(self):
        """Get template names."""
        template_name = self.template_name
        if not template_name:
            if not self.theme:
                theme = get_theme(request=self.request, as_instance=True)
            else:
                theme = self.theme
            template_name = theme.view_form_entry_template
        return [template_name]

    def get_essential_objects(
        self,
        form_entry,
        request,
    ):
        """Get essential objects."""
        form_element_entries = form_entry.formelemententry_set.all()[:]

        # This is where the most of the magic happens. Our form is being built
        # dynamically.
        form_cls = assemble_form_class(
            form_entry,
            form_element_entries=form_element_entries,
            request=request,
        )

        return (
            form_element_entries,
            form_cls,
        )

    def get_initial_data(self, request, form_entry):
        """Get initial data.

        :param request: HTTP request.
        :param form_entry: Form entry object.
        :return: Dictionary with initial form data.
        """
        # Providing initial form data by feeding entire GET dictionary
        # to the form, if ``GET_PARAM_INITIAL_DATA`` is present in the
        # GET.
        if GET_PARAM_INITIAL_DATA in request.GET:
            return {"initial": request.GET}
        return {}

    def inactive_form_response(self, request, form_entry):
        context = {
            "form_entry": form_entry,
            "page_header": (
                form_entry.inactive_page_title
                or form_entry.title
                or form_entry.name
            ),
        }

        if not self.template_name:
            theme = get_theme(request=request, as_instance=True)
            template_name = theme.form_entry_inactive_template
        else:
            template_name = self.template_name

        return render(request, template_name, context)

    def get_object(self, queryset=None):
        """Get object."""
        obj = super(ViewFormEntryView, self).get_object(queryset=queryset)
        self.check_object_permissions(self.request, obj)
        return obj

    def get(self, request, *args, **kwargs):
        """Handle GET requests: instantiate a blank version of the form."""
        try:
            form_entry = self.get_object()
        except ObjectDoesNotExist as err:
            raise Http404(_("Form entry not found."))

        self.object = form_entry

        if not form_entry.is_active:
            return self.inactive_form_response(request, form_entry)

        form_element_entries, form_cls = self.get_essential_objects(
            form_entry,
            request,
        )

        # Get initial data.
        kwargs = self.get_initial_data(request, form_entry)
        form = form_cls(**kwargs)

        # In debug mode, try to identify possible problems.
        if DEBUG:
            form.as_p()
        else:
            try:
                form.as_p()
            except Exception as err:
                logger.error(err)

        theme = get_theme(request=request, as_instance=True)
        theme.collect_plugin_media(form_element_entries)

        return self.render_to_response(
            self.get_context_data(
                form=form,
                form_entry=form_entry,
                fobi_form_title=form_entry.title,
            )
        )

    def post(self, request, *args, **kwargs):
        """
        Handle POST requests: instantiate a form instance with the passed
        POST variables and then check if it's valid.
        """
        try:
            form_entry = self.get_object()
        except ObjectDoesNotExist as err:
            raise Http404(_("Form entry not found."))

        self.object = form_entry

        if not form_entry.is_active:
            return self.inactive_form_response(request, form_entry)

        form_element_entries, form_cls = self.get_essential_objects(
            form_entry,
            request,
        )

        form = form_cls(request.POST, request.FILES)

        # Fire pre form validation callbacks
        fire_form_callbacks(
            form_entry=form_entry,
            request=request,
            form=form,
            stage=CALLBACK_BEFORE_FORM_VALIDATION,
        )

        if form.is_valid():
            # Fire form valid callbacks, before handling submitted plugin
            # form data.
            form = fire_form_callbacks(
                form_entry=form_entry,
                request=request,
                form=form,
                stage=CALLBACK_FORM_VALID_BEFORE_SUBMIT_PLUGIN_FORM_DATA,
            )

            # Fire plugin processors
            form = submit_plugin_form_data(
                form_entry=form_entry, request=request, form=form
            )

            # Fire form valid callbacks
            form = fire_form_callbacks(
                form_entry=form_entry,
                request=request,
                form=form,
                stage=CALLBACK_FORM_VALID,
            )

            # Run all handlers
            handler_responses, handler_errors = run_form_handlers(
                form_entry=form_entry,
                request=request,
                form=form,
                form_element_entries=form_element_entries,
            )

            # Warning that not everything went ok.
            if handler_errors:
                for handler_error in handler_errors:
                    messages.warning(
                        request, _("Error occurred: {0}.").format(handler_error)
                    )

            # Fire post handler callbacks
            fire_form_callbacks(
                form_entry=form_entry,
                request=request,
                form=form,
                stage=CALLBACK_FORM_VALID_AFTER_FORM_HANDLERS,
            )

            messages.info(
                request,
                _("Form {0} was submitted successfully.").format(
                    form_entry.name
                ),
            )
            return redirect(
                reverse_lazy(
                    "fobi.form_entry_submitted", args=[form_entry.slug]
                )
            )
        else:
            # Fire post form validation callbacks
            fire_form_callbacks(
                form_entry=form_entry,
                request=request,
                form=form,
                stage=CALLBACK_FORM_INVALID,
            )

        # In debug mode, try to identify possible problems.
        if DEBUG:
            form.as_p()
        else:
            try:
                form.as_p()
            except Exception as err:
                logger.error(err)

        theme = get_theme(request=request, as_instance=True)
        theme.collect_plugin_media(form_element_entries)

        return self.render_to_response(
            self.get_context_data(
                form=form,
                form_entry=form_entry,
                fobi_form_title=form_entry.title,
            )
        )


# *****************************************************************************
# **************************** View form entry success ************************
# *****************************************************************************


class ViewFormEntrySubmittedView(AbstractViewFormEntryView):
    """View form entry submitted."""

    def get(self, request, *args, **kwargs):
        """Handle GET requests: instantiate a blank version of the form."""
        try:
            form_entry = self.get_object()
        except ObjectDoesNotExist as err:
            raise Http404(_("Form entry not found."))

        self.object = form_entry

        return self.render_to_response(
            self.get_context_data(
                form_entry_slug=self.kwargs.get(self.slug_url_kwarg),
                form_entry=form_entry,
            )
        )

    def get_context_data(self, **kwargs):
        """Get context data."""
        context = super(ViewFormEntrySubmittedView, self).get_context_data(
            **kwargs
        )

        if not self.theme:
            theme = get_theme(request=self.request, as_instance=True)
        else:
            theme = self.theme

        if theme:
            context.update({"fobi_theme": theme})
        return context

    def get_template_names(self):
        """Get template names."""
        template_name = self.template_name
        if not template_name:
            if not self.theme:
                theme = get_theme(request=self.request, as_instance=True)
            else:
                theme = self.theme
            template_name = theme.form_entry_submitted_template
        return [template_name]
