from django.core.paginator import InvalidPage, Paginator
from django.db import transaction
from django.db.models import Sum, functions
from django.http import Http404
from django.shortcuts import get_object_or_404, redirect
from django.template.response import TemplateResponse
from django.urls import reverse
from django.utils.translation import gettext as _
from django.views.decorators.vary import vary_on_headers

from wagtail.admin import messages
from wagtail.admin.auth import any_permission_required, permission_required
from wagtail.admin.forms.search import SearchForm
from wagtail.admin.modal_workflow import render_modal_workflow
from wagtail.contrib.search_promotions import forms, models
from wagtail.contrib.search_promotions.models import Query
from wagtail.log_actions import log
from wagtail.search.utils import normalise_query_string


@any_permission_required(
    "wagtailsearchpromotions.add_searchpromotion",
    "wagtailsearchpromotions.change_searchpromotion",
    "wagtailsearchpromotions.delete_searchpromotion",
)
@vary_on_headers("X-Requested-With")
def index(request):
    # Ordering
    valid_ordering = ["query_string", "-query_string", "views", "-views"]
    ordering = valid_ordering[0]

    if "ordering" in request.GET and request.GET["ordering"] in valid_ordering:
        ordering = request.GET["ordering"]

    # Query
    queries = Query.objects.filter(editors_picks__isnull=False).distinct()

    if "views" in ordering:
        queries = queries.annotate(views=functions.Coalesce(Sum("daily_hits__hits"), 0))

    queries = queries.order_by(ordering)

    # Search
    is_searching = False
    query_string = request.GET.get("q", "")

    if query_string:
        queries = queries.filter(query_string__icontains=query_string)
        is_searching = True

    # Paginate
    paginator = Paginator(queries, per_page=20)
    try:
        queries = paginator.page(request.GET.get("p", 1))
    except InvalidPage:
        raise Http404

    if request.headers.get("x-requested-with") == "XMLHttpRequest":
        return TemplateResponse(
            request,
            "wagtailsearchpromotions/results.html",
            {
                "is_searching": is_searching,
                "ordering": ordering,
                "queries": queries,
                "query_string": query_string,
            },
        )
    else:
        return TemplateResponse(
            request,
            "wagtailsearchpromotions/index.html",
            {
                "is_searching": is_searching,
                "ordering": ordering,
                "queries": queries,
                "query_string": query_string,
                "search_form": SearchForm(
                    data={"q": query_string} if query_string else None,
                    placeholder=_("Search promoted results"),
                ),
            },
        )


def save_searchpicks(query, new_query, searchpicks_formset):
    # Save
    if searchpicks_formset.is_valid():
        # Set sort_order
        for i, form in enumerate(searchpicks_formset.ordered_forms):
            form.instance.sort_order = i

            # Make sure the form is marked as changed so it gets saved with the new order
            form.has_changed = lambda: True

        # log deleted items before saving, otherwise we lose their IDs
        items_for_deletion = [
            form.instance
            for form in searchpicks_formset.deleted_forms
            if form.instance.pk
        ]
        with transaction.atomic():
            for search_pick in items_for_deletion:
                log(search_pick, "wagtail.delete")

            searchpicks_formset.save()

            for search_pick in searchpicks_formset.new_objects:
                log(search_pick, "wagtail.create")

            # If query was changed, move all search picks to the new query
            if query != new_query:
                searchpicks_formset.get_queryset().update(query=new_query)
                # log all items in the formset as having changed
                for search_pick, changed_fields in searchpicks_formset.changed_objects:
                    log(search_pick, "wagtail.edit")
            else:
                # only log objects with actual changes
                for search_pick, changed_fields in searchpicks_formset.changed_objects:
                    if changed_fields:
                        log(search_pick, "wagtail.edit")

        return True
    else:
        return False


@permission_required("wagtailsearchpromotions.add_searchpromotion")
def add(request):
    if request.method == "POST":
        # Get query
        query_form = forms.QueryForm(request.POST)
        if query_form.is_valid():
            query = Query.get(query_form["query_string"].value())

            # Save search picks
            searchpicks_formset = forms.SearchPromotionsFormSet(
                request.POST, instance=query
            )
            if save_searchpicks(query, query, searchpicks_formset):
                for search_pick in searchpicks_formset.new_objects:
                    log(search_pick, "wagtail.create")
                messages.success(
                    request,
                    _("Editor's picks for '%(query)s' created.") % {"query": query},
                    buttons=[
                        messages.button(
                            reverse("wagtailsearchpromotions:edit", args=(query.id,)),
                            _("Edit"),
                        )
                    ],
                )
                return redirect("wagtailsearchpromotions:index")
            else:
                if len(searchpicks_formset.non_form_errors()):
                    # formset level error (e.g. no forms submitted)
                    messages.error(
                        request,
                        " ".join(
                            error for error in searchpicks_formset.non_form_errors()
                        ),
                    )
                else:
                    # specific errors will be displayed within form fields
                    messages.error(
                        request,
                        _("Recommendations have not been created due to errors"),
                    )
        else:
            searchpicks_formset = forms.SearchPromotionsFormSet()
    else:
        query_form = forms.QueryForm()
        searchpicks_formset = forms.SearchPromotionsFormSet()

    return TemplateResponse(
        request,
        "wagtailsearchpromotions/add.html",
        {
            "query_form": query_form,
            "searchpicks_formset": searchpicks_formset,
            "form_media": query_form.media + searchpicks_formset.media,
        },
    )


@permission_required("wagtailsearchpromotions.change_searchpromotion")
def edit(request, query_id):
    query = get_object_or_404(Query, id=query_id)

    if request.method == "POST":
        # Get query
        query_form = forms.QueryForm(request.POST)
        # and the recommendations
        searchpicks_formset = forms.SearchPromotionsFormSet(
            request.POST, instance=query
        )

        if query_form.is_valid():
            new_query = Query.get(query_form["query_string"].value())

            # Save search picks
            if save_searchpicks(query, new_query, searchpicks_formset):
                messages.success(
                    request,
                    _("Editor's picks for '%(query)s' updated.") % {"query": new_query},
                    buttons=[
                        messages.button(
                            reverse("wagtailsearchpromotions:edit", args=(query.id,)),
                            _("Edit"),
                        )
                    ],
                )
                return redirect("wagtailsearchpromotions:index")
            else:
                if len(searchpicks_formset.non_form_errors()):
                    messages.error(
                        request,
                        " ".join(
                            error for error in searchpicks_formset.non_form_errors()
                        ),
                    )
                    # formset level error (e.g. no forms submitted)
                else:
                    messages.error(
                        request, _("Recommendations have not been saved due to errors")
                    )
                    # specific errors will be displayed within form fields

    else:
        query_form = forms.QueryForm(initial={"query_string": query.query_string})
        searchpicks_formset = forms.SearchPromotionsFormSet(instance=query)

    return TemplateResponse(
        request,
        "wagtailsearchpromotions/edit.html",
        {
            "query_form": query_form,
            "searchpicks_formset": searchpicks_formset,
            "query": query,
            "form_media": query_form.media + searchpicks_formset.media,
        },
    )


@permission_required("wagtailsearchpromotions.delete_searchpromotion")
def delete(request, query_id):
    query = get_object_or_404(Query, id=query_id)

    if request.method == "POST":
        editors_picks = query.editors_picks.all()
        with transaction.atomic():
            for search_pick in editors_picks:
                log(search_pick, "wagtail.delete")
            editors_picks.delete()
        messages.success(request, _("Editor's picks deleted."))
        return redirect("wagtailsearchpromotions:index")

    return TemplateResponse(
        request,
        "wagtailsearchpromotions/confirm_delete.html",
        {
            "query": query,
        },
    )


def chooser(request, get_results=False):
    # Get most popular queries
    queries = models.Query.get_most_popular()

    # If searching, filter results by query string
    if "q" in request.GET:
        searchform = SearchForm(request.GET)
        if searchform.is_valid():
            query_string = searchform.cleaned_data["q"]
            queries = queries.filter(
                query_string__icontains=normalise_query_string(query_string)
            )
    else:
        searchform = SearchForm()

    paginator = Paginator(queries, per_page=10)
    try:
        queries = paginator.page(request.GET.get("p", 1))
    except InvalidPage:
        raise Http404

    # Render
    if get_results:
        return TemplateResponse(
            request,
            "wagtailsearchpromotions/queries/chooser/results.html",
            {
                "queries": queries,
            },
        )
    else:
        return render_modal_workflow(
            request,
            "wagtailsearchpromotions/queries/chooser/chooser.html",
            None,
            {
                "queries": queries,
                "searchform": searchform,
            },
            json_data={"step": "chooser"},
        )


def chooserresults(request):
    return chooser(request, get_results=True)
