""" book list views"""
from typing import Optional
from urllib.parse import urlencode

from django.contrib.auth.decorators import login_required
from django.core.paginator import Paginator
from django.db import IntegrityError, transaction
from django.db.models import Avg, Count, DecimalField, Q, Max
from django.db.models.functions import Coalesce
from django.http import HttpResponseBadRequest, HttpResponse, Http404
from django.shortcuts import get_object_or_404, redirect
from django.template.response import TemplateResponse
from django.urls import reverse
from django.utils.decorators import method_decorator
from django.views import View
from django.views.decorators.http import require_POST
from django.views.decorators.clickjacking import xframe_options_exempt

from bookwyrm import book_search, forms, models
from bookwyrm.activitypub import ActivitypubResponse
from bookwyrm.settings import PAGE_LENGTH
from .helpers import is_api_request
from .helpers import get_user_from_username


# pylint: disable=no-self-use
class Lists(View):
    """book list page"""

    def get(self, request):
        """display a book list"""
        # hide lists with no approved books
        lists = (
            models.List.privacy_filter(
                request.user, privacy_levels=["public", "followers"]
            )
            .annotate(item_count=Count("listitem", filter=Q(listitem__approved=True)))
            .filter(item_count__gt=0)
            .select_related("user")
            .prefetch_related("listitem_set")
            .order_by("-updated_date")
            .distinct()
        )
        paginated = Paginator(lists, 12)
        data = {
            "lists": paginated.get_page(request.GET.get("page")),
            "list_form": forms.ListForm(),
            "path": "/list",
        }
        return TemplateResponse(request, "lists/lists.html", data)

    @method_decorator(login_required, name="dispatch")
    # pylint: disable=unused-argument
    def post(self, request):
        """create a book_list"""
        form = forms.ListForm(request.POST)
        if not form.is_valid():
            return redirect("lists")
        book_list = form.save()
        # list should not have a group if it is not group curated
        if not book_list.curation == "group":
            book_list.group = None
            book_list.save(broadcast=False)

        return redirect(book_list.local_path)


@method_decorator(login_required, name="dispatch")
class SavedLists(View):
    """saved book list page"""

    def get(self, request):
        """display book lists"""
        # hide lists with no approved books
        lists = request.user.saved_lists.order_by("-updated_date")

        paginated = Paginator(lists, 12)
        data = {
            "lists": paginated.get_page(request.GET.get("page")),
            "list_form": forms.ListForm(),
            "path": "/list",
        }
        return TemplateResponse(request, "lists/lists.html", data)


@method_decorator(login_required, name="dispatch")
class UserLists(View):
    """a user's book list page"""

    def get(self, request, username):
        """display a book list"""
        user = get_user_from_username(request.user, username)
        lists = models.List.privacy_filter(request.user).filter(user=user)
        paginated = Paginator(lists, 12)

        data = {
            "user": user,
            "is_self": request.user.id == user.id,
            "lists": paginated.get_page(request.GET.get("page")),
            "list_form": forms.ListForm(),
            "path": user.local_path + "/lists",
        }
        return TemplateResponse(request, "user/lists.html", data)


class List(View):
    """book list page"""

    def get(self, request, list_id):
        """display a book list"""
        book_list = get_object_or_404(models.List, id=list_id)
        book_list.raise_visible_to_user(request.user)

        if is_api_request(request):
            return ActivitypubResponse(book_list.to_activity(**request.GET))

        query = request.GET.get("q")
        suggestions = None

        # sort_by shall be "order" unless a valid alternative is given
        sort_by = request.GET.get("sort_by", "order")
        if sort_by not in ("order", "title", "rating"):
            sort_by = "order"

        # direction shall be "ascending" unless a valid alternative is given
        direction = request.GET.get("direction", "ascending")
        if direction not in ("ascending", "descending"):
            direction = "ascending"

        directional_sort_by = {
            "order": "order",
            "title": "book__title",
            "rating": "average_rating",
        }[sort_by]
        if direction == "descending":
            directional_sort_by = "-" + directional_sort_by

        items = book_list.listitem_set.prefetch_related("user", "book", "book__authors")
        if sort_by == "rating":
            items = items.annotate(
                average_rating=Avg(
                    Coalesce("book__review__rating", 0.0),
                    output_field=DecimalField(),
                )
            )
        items = items.filter(approved=True).order_by(directional_sort_by)

        paginated = Paginator(items, PAGE_LENGTH)

        if query and request.user.is_authenticated:
            # search for books
            suggestions = book_search.search(
                query,
                filters=[~Q(parent_work__editions__in=book_list.books.all())],
            )
        elif request.user.is_authenticated:
            # just suggest whatever books are nearby
            suggestions = request.user.shelfbook_set.filter(
                ~Q(book__in=book_list.books.all())
            )
            suggestions = [s.book for s in suggestions[:5]]
            if len(suggestions) < 5:
                suggestions += [
                    s.default_edition
                    for s in models.Work.objects.filter(
                        ~Q(editions__in=book_list.books.all()),
                    ).order_by("-updated_date")
                ][: 5 - len(suggestions)]

        page = paginated.get_page(request.GET.get("page"))

        embed_key = str(book_list.embed_key.hex)
        embed_url = reverse("embed-list", args=[book_list.id, embed_key])
        embed_url = request.build_absolute_uri(embed_url)

        if request.GET:
            embed_url = f"{embed_url}?{request.GET.urlencode()}"

        data = {
            "list": book_list,
            "items": page,
            "page_range": paginated.get_elided_page_range(
                page.number, on_each_side=2, on_ends=1
            ),
            "pending_count": book_list.listitem_set.filter(approved=False).count(),
            "suggested_books": suggestions,
            "list_form": forms.ListForm(instance=book_list),
            "query": query or "",
            "sort_form": forms.SortListForm(
                {"direction": direction, "sort_by": sort_by}
            ),
            "embed_url": embed_url,
        }
        return TemplateResponse(request, "lists/list.html", data)

    @method_decorator(login_required, name="dispatch")
    def post(self, request, list_id):
        """edit a list"""
        book_list = get_object_or_404(models.List, id=list_id)
        book_list.raise_not_editable(request.user)

        form = forms.ListForm(request.POST, instance=book_list)
        if not form.is_valid():
            return redirect("list", book_list.id)
        book_list = form.save()
        if not book_list.curation == "group":
            book_list.group = None
            book_list.save(broadcast=False)

        return redirect(book_list.local_path)


class EmbedList(View):
    """embeded book list page"""

    def get(self, request, list_id, list_key):
        """display a book list"""
        book_list = get_object_or_404(models.List, id=list_id)

        embed_key = str(book_list.embed_key.hex)

        if list_key != embed_key:
            raise Http404()

        # sort_by shall be "order" unless a valid alternative is given
        sort_by = request.GET.get("sort_by", "order")
        if sort_by not in ("order", "title", "rating"):
            sort_by = "order"

        # direction shall be "ascending" unless a valid alternative is given
        direction = request.GET.get("direction", "ascending")
        if direction not in ("ascending", "descending"):
            direction = "ascending"

        directional_sort_by = {
            "order": "order",
            "title": "book__title",
            "rating": "average_rating",
        }[sort_by]
        if direction == "descending":
            directional_sort_by = "-" + directional_sort_by

        items = book_list.listitem_set.prefetch_related("user", "book", "book__authors")
        if sort_by == "rating":
            items = items.annotate(
                average_rating=Avg(
                    Coalesce("book__review__rating", 0.0),
                    output_field=DecimalField(),
                )
            )
        items = items.filter(approved=True).order_by(directional_sort_by)

        paginated = Paginator(items, PAGE_LENGTH)

        page = paginated.get_page(request.GET.get("page"))

        data = {
            "list": book_list,
            "items": page,
            "page_range": paginated.get_elided_page_range(
                page.number, on_each_side=2, on_ends=1
            ),
        }
        return TemplateResponse(request, "lists/embed-list.html", data)


class Curate(View):
    """approve or discard list suggestsions"""

    @method_decorator(login_required, name="dispatch")
    def get(self, request, list_id):
        """display a pending list"""
        book_list = get_object_or_404(models.List, id=list_id)
        book_list.raise_not_editable(request.user)

        data = {
            "list": book_list,
            "pending": book_list.listitem_set.filter(approved=False),
            "list_form": forms.ListForm(instance=book_list),
        }
        return TemplateResponse(request, "lists/curate.html", data)

    @method_decorator(login_required, name="dispatch")
    # pylint: disable=unused-argument
    def post(self, request, list_id):
        """edit a book_list"""
        book_list = get_object_or_404(models.List, id=list_id)
        book_list.raise_not_editable(request.user)

        suggestion = get_object_or_404(models.ListItem, id=request.POST.get("item"))
        approved = request.POST.get("approved") == "true"
        if approved:
            # update the book and set it to be the last in the order of approved books,
            # before any pending books
            suggestion.approved = True
            order_max = (
                book_list.listitem_set.filter(approved=True).aggregate(Max("order"))[
                    "order__max"
                ]
                or 0
            ) + 1
            suggestion.order = order_max
            increment_order_in_reverse(book_list.id, order_max)
            suggestion.save()
        else:
            deleted_order = suggestion.order
            suggestion.delete(broadcast=False)
            normalize_book_list_ordering(book_list.id, start=deleted_order)
        return redirect("list-curate", book_list.id)


@require_POST
@login_required
def save_list(request, list_id):
    """save a list"""
    book_list = get_object_or_404(models.List, id=list_id)
    request.user.saved_lists.add(book_list)
    return redirect("list", list_id)


@require_POST
@login_required
def unsave_list(request, list_id):
    """unsave a list"""
    book_list = get_object_or_404(models.List, id=list_id)
    request.user.saved_lists.remove(book_list)
    return redirect("list", list_id)


@require_POST
@login_required
def delete_list(request, list_id):
    """delete a list"""
    book_list = get_object_or_404(models.List, id=list_id)

    # only the owner or a moderator can delete a list
    book_list.raise_not_deletable(request.user)

    book_list.delete()
    return redirect("lists")


@require_POST
@login_required
def add_book(request):
    """put a book on a list"""
    book_list = get_object_or_404(models.List, id=request.POST.get("list"))
    is_group_member = False
    if book_list.curation == "group":
        is_group_member = models.GroupMember.objects.filter(
            group=book_list.group, user=request.user
        ).exists()

    book_list.raise_visible_to_user(request.user)

    book = get_object_or_404(models.Edition, id=request.POST.get("book"))
    # do you have permission to add to the list?
    try:
        if (
            request.user == book_list.user
            or is_group_member
            or book_list.curation == "open"
        ):
            # add the book at the latest order of approved books, before pending books
            order_max = (
                book_list.listitem_set.filter(approved=True).aggregate(Max("order"))[
                    "order__max"
                ]
            ) or 0
            increment_order_in_reverse(book_list.id, order_max + 1)
            models.ListItem.objects.create(
                book=book,
                book_list=book_list,
                user=request.user,
                order=order_max + 1,
            )
        elif book_list.curation == "curated":
            # make a pending entry at the end of the list
            order_max = (
                book_list.listitem_set.aggregate(Max("order"))["order__max"]
            ) or 0
            models.ListItem.objects.create(
                approved=False,
                book=book,
                book_list=book_list,
                user=request.user,
                order=order_max + 1,
            )
        else:
            # you can't add to this list, what were you THINKING
            return HttpResponseBadRequest()
    except IntegrityError:
        # if the book is already on the list, don't flip out
        pass

    path = reverse("list", args=[book_list.id])
    params = request.GET.copy()
    params["updated"] = True
    return redirect(f"{path}?{urlencode(params)}")


@require_POST
@login_required
def remove_book(request, list_id):
    """remove a book from a list"""

    book_list = get_object_or_404(models.List, id=list_id)
    item = get_object_or_404(models.ListItem, id=request.POST.get("item"))

    item.raise_not_deletable(request.user)

    with transaction.atomic():
        deleted_order = item.order
        item.delete()
        normalize_book_list_ordering(book_list.id, start=deleted_order)

    return redirect("list", list_id)


@require_POST
@login_required
def set_book_position(request, list_item_id):
    """
    Action for when the list user manually specifies a list position, takes
    special care with the unique ordering per list.
    """
    list_item = get_object_or_404(models.ListItem, id=list_item_id)
    list_item.book_list.raise_not_editable(request.user)
    try:
        int_position = int(request.POST.get("position"))
    except ValueError:
        return HttpResponseBadRequest("bad value for position. should be an integer")

    if int_position < 1:
        return HttpResponseBadRequest("position cannot be less than 1")

    book_list = list_item.book_list

    # the max position to which a book may be set is the highest order for
    # books which are approved
    order_max = book_list.listitem_set.filter(approved=True).aggregate(Max("order"))[
        "order__max"
    ]

    int_position = min(int_position, order_max)

    original_order = list_item.order
    if original_order == int_position:
        # no change
        return HttpResponse(status=204)

    with transaction.atomic():
        if original_order > int_position:
            list_item.order = -1
            list_item.save()
            increment_order_in_reverse(book_list.id, int_position, original_order)
        else:
            list_item.order = -1
            list_item.save()
            decrement_order(book_list.id, original_order, int_position)

        list_item.order = int_position
        list_item.save()

    return redirect("list", book_list.id)


@transaction.atomic
def increment_order_in_reverse(
    book_list_id: int, start: int, end: Optional[int] = None
):
    """increase the order number for every item in a list"""
    try:
        book_list = models.List.objects.get(id=book_list_id)
    except models.List.DoesNotExist:
        return
    items = book_list.listitem_set.filter(order__gte=start)
    if end is not None:
        items = items.filter(order__lt=end)
    items = items.order_by("-order")
    for item in items:
        item.order += 1
        item.save()


@transaction.atomic
def decrement_order(book_list_id, start, end):
    """decrement the order value for every item in a list"""
    try:
        book_list = models.List.objects.get(id=book_list_id)
    except models.List.DoesNotExist:
        return
    items = book_list.listitem_set.filter(order__gt=start, order__lte=end).order_by(
        "order"
    )
    for item in items:
        item.order -= 1
        item.save()


@transaction.atomic
def normalize_book_list_ordering(book_list_id, start=0, add_offset=0):
    """gives each book in a list the proper sequential order number"""
    try:
        book_list = models.List.objects.get(id=book_list_id)
    except models.List.DoesNotExist:
        return
    items = book_list.listitem_set.filter(order__gt=start).order_by("order")
    for i, item in enumerate(items, start):
        effective_order = i + add_offset
        if item.order != effective_order:
            item.order = effective_order
            item.save()


@xframe_options_exempt
def unsafe_embed_list(request, *args, **kwargs):
    """allows the EmbedList view to be loaded through unsafe iframe origins"""

    embed_list_view = EmbedList.as_view()
    return embed_list_view(request, *args, **kwargs)
