"""Standard views for displaying and interacting with configuration forms."""

import logging

from django.http import (Http404, HttpResponse, HttpResponseBadRequest,
                         HttpResponseRedirect)
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_protect
from django.views.generic.base import TemplateView


logger = logging.getLogger(__name__)


class ConfigPagesView(TemplateView):
    """Base view for a set of configuration pages.

    This will render the page for managing a set of configuration sub-pages
    (:py:class:`djblets.configforms.pages.ConfigPage` subclasses).

    Subclasses are expected to provide :py:attr:`title` and
    :py:attr:`page_classes`, at a minimum.

    To dynamically compute pages, implement a :py:attr:`page_classes` method
    and decorate it with ``@property``.
    """

    #: The page title.
    title = None

    #: The title to display over the navigation sidebar.
    nav_title = None

    #: The element ID for the page's container.
    pages_id = 'config_pages'

    #: The template used to render the view.
    template_name = 'configforms/config.html'

    #: The base template that the default template should inherit from.
    base_template_name = 'base.html'

    #: The list of page subclasses to display.
    page_classes = []

    #: The list of CSS bundle names to include on the page.
    css_bundle_names = []

    #: The list of JavaScript bundle names to include on the page.
    js_bundle_names = []

    #: The optional Backbone model used for the view state.
    js_model_class = None

    #: The Backbone view used to render the main view.
    js_view_class = 'Djblets.Config.PagesView'

    #: The HTTP methods that this view will respond to.
    http_method_names = ['get', 'post']

    #: Whether or not to render the sidebar.
    render_sidebar = True

    @method_decorator(csrf_protect)
    def dispatch(self, request, *args, **kwargs):
        self.pages = [
            page for page in (
                page_cls(self, request, request.user)
                for page_cls in self.page_classes
            )
            if page.is_visible()
        ]

        self.forms = {}

        # Store a mapping of form IDs to form instances, and check for
        # duplicates.
        for page in self.pages:
            for form in page.forms:
                # This should already be handled during form registration.
                assert form.form_id not in self.forms, \
                    'Duplicate form ID %s (on page %s)' % (
                        form.form_id, page.page_id)

                self.forms[form.form_id] = form

        return super(ConfigPagesView, self).dispatch(request, *args, **kwargs)

    def post(self, request, *args, **kwargs):
        """Handle HTTP post requests to the view.

        This will instantiate the form that the POST was targeting, validate
        the form, and then trigger its save if valid, reloading the page
        with the new state.

        Args:
            request (HttpRequest):
                The HTTP request from the client.

        Returns:
            HttpResponse:
                Either a redirect (if successfully posted), or a standard
                response with the page content.
        """
        form_id = request.POST.get('form_target')

        if form_id is None:
            logger.error('Submitted form content had no form_target.',
                         extra={'request': request})
            return HttpResponseBadRequest()

        if form_id not in self.forms:
            raise Http404

        # Replace the form in the list with a new instantiation containing
        # the form data. If we fail to save, this will ensure the error is
        # shown on the page.
        old_form = self.forms[form_id]
        page = old_form.page
        form_cls = old_form.__class__
        form = form_cls(page, request, request.user, request.POST)
        self.forms[form_id] = form

        # Replace the form in the page.
        for i, page_form in enumerate(page.forms):
            if page_form.form_id == form_id:
                page.forms[i] = form
                break

        if form.is_valid():
            rsp = form.save()

            if rsp and isinstance(rsp, HttpResponse):
                return rsp

            return HttpResponseRedirect(request.path)

        # For our purposes, there's no difference between the handling of a
        # GET request and a POST request at this stage.
        return self.get(request, *args, **kwargs)

    def get_context_data(self, **kwargs):
        """Return extra context data to render in the template.

        Returns:
            dict: A dictionary of context data for the page.
        """
        return {
            'base_template_name': self.base_template_name,
            'page_title': self.title,
            'nav_title': self.nav_title or self.title,
            'pages_id': self.pages_id,
            'pages': self.pages,
            'css_bundle_names': self.css_bundle_names,
            'js_bundle_names': self.js_bundle_names,
            'js_model_class': self.js_model_class,
            'js_view_class': self.js_view_class,
            'js_model_data': self.get_js_model_data(),
            'js_view_data': self.get_js_view_data(),
            'forms': list(self.forms.values()),
            'render_sidebar': self.render_sidebar,
        }

    def get_js_view_data(self):
        """Return custom options to pass to the JavaScript view.

        By default, this will return an empty dictionary. Subclasses can
        override to provide custom data.

        Returns:
            dict:
                A dictionary of options to pass to the View instance. By
                default, this will be empty.
        """
        return {}

    def get_js_model_data(self):
        """Return custom attributes to pass to the JavaScript model.

        If :py:attr:`js_model_class` is provided, the data returned from this
        function will be provided to the model when constructued.

        By default, this will return an empty dictionary. Subclasses can
        override to provide custom data.

        Returns:
            dict:
                A dictionary of attributes to pass to the Model instance. By
                default, this will be empty.
        """
        return {}
