# SPDX-FileCopyrightText: 2017-present Tobias Kunze
# SPDX-License-Identifier: AGPL-3.0-only WITH LicenseRef-Pretalx-AGPL-3.0-Terms

import urllib
from collections import defaultdict
from contextlib import suppress
from urllib.parse import quote

from csp.decorators import csp_exempt
from django import forms
from django.conf import settings
from django.db.models import Q
from django.http import FileResponse, Http404
from django.shortcuts import get_object_or_404, redirect
from django.utils.functional import cached_property
from django.utils.module_loading import import_string
from django.utils.translation import gettext_lazy as _
from django_context_decorator import context
from formtools.wizard.forms import ManagementForm
from rules.contrib.views import PermissionRequiredMixin

from pretalx.common.forms import SearchForm
from pretalx.common.forms.mixins import PretalxI18nModelForm, ReadOnlyFlag
from pretalx.common.text.phrases import phrases
from pretalx.common.ui import Button, back_button

SessionStore = import_string(f"{settings.SESSION_ENGINE}.SessionStore")


class Filterable:
    filter_fields = []
    default_filters = []

    def get_default_filters(self):
        return self.default_filters

    def filter_queryset(self, qs):
        if self.filter_fields:
            qs = self._handle_filter(qs)
        if "q" in self.request.GET:
            query = urllib.parse.unquote(self.request.GET["q"])
            qs = self.handle_search(qs, query, self.get_default_filters())
        if (
            (filter_form := self.filter_form)
            and filter_form.is_valid()
            and hasattr(filter_form, "filter_queryset")
        ):
            qs = filter_form.filter_queryset(qs)
        return qs

    def _handle_filter(self, qs):
        for key in self.request.GET:  # Do NOT use items() to preserve multivalue fields
            # There is a special case here: we hack in OR lookups by allowing __ in values.
            lookups = defaultdict(list)
            values = self.request.GET.getlist(key)
            for value in values:
                value_parts = value.split("__", maxsplit=1)
                if len(value_parts) > 1 and value_parts[0] in self.filter_fields:
                    _key = value_parts[0]
                    _value = value_parts[1]
                else:
                    _key = key
                    _value = value_parts[0]
                if _key in self.filter_fields and _value:
                    if "__isnull" in _key:
                        # We don't append to the list here, because that's not meaningful
                        # in a boolean lookup
                        lookups[_key] = _value == "on"
                    else:
                        _key = f"{_key}__in"
                        lookups[_key].append(_value)
            _filters = Q()
            for _key, value in lookups.items():
                _filters |= Q(**{_key: value})
            qs = qs.filter(_filters)
        return qs

    @staticmethod
    def handle_search(qs, query, filters):
        _filters = [Q(**{field: query}) for field in filters]
        if len(_filters) > 1:
            _filter = _filters[0]
            for additional_filter in _filters[1:]:
                _filter = _filter | additional_filter
            qs = qs.filter(_filter)
        elif _filters:
            qs = qs.filter(_filters[0])
        return qs

    @context
    @cached_property
    def search_form(self):
        return SearchForm(self.request.GET if "q" in self.request.GET else None)

    @context
    @cached_property
    def filter_form(self):
        if hasattr(self, "filter_form_class"):
            return self.filter_form_class(self.request.GET, event=self.request.event)
        if hasattr(self, "get_filter_form"):
            return self.get_filter_form()
        if self.filter_fields:
            _form = forms.modelform_factory(self.model, fields=self.filter_fields)(
                self.request.GET
            )
            for field in _form.fields.values():
                field.required = False
                if hasattr(field, "queryset"):
                    field.queryset = field.queryset.filter(event=self.request.event)
            return _form
        return None


class PermissionRequired(PermissionRequiredMixin):
    write_permission_required = None
    create_permission_required = None
    read_only_form_class = False

    @cached_property
    def object(self):
        return self.get_object()

    @cached_property
    def permission_object(self):
        if hasattr(self, "get_permission_object"):
            return self.get_permission_object()
        return self.object

    @context
    @cached_property
    def permission_action(self):
        kwargs = getattr(self, "kwargs", None)
        if kwargs and not any(_id in self.kwargs for _id in ("pk", "code")):
            if permission := self.create_permission_required:
                # If there is a create_permission and we don't have it, raise
                if not self._check_permission(permission):
                    raise Http404()
                return "create"
            if self._check_permission(self.write_permission_required):
                # If there is no create permission, we're probably not in an object view
                return "create"
            return "view"
        if self._check_permission(self.write_permission_required):
            return "edit"
        return "view"

    def get_form_kwargs(self):
        kwargs = super().get_form_kwargs()
        cls = self.get_form_class()
        if self.read_only_form_class or issubclass(cls, ReadOnlyFlag):
            kwargs["read_only"] = self.permission_action == "view"
        event = getattr(self.request, "event", None)
        if event and issubclass(self.form_class, PretalxI18nModelForm):
            kwargs["locales"] = event.locales
        return kwargs

    def _check_permission(self, permission_name):
        return self.request.user.has_perm(permission_name, self.permission_object)

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        if not hasattr(self, "get_permission_object"):
            for key in ("permission_object", "object"):
                if getattr(self, key, None):
                    self.get_permission_object = lambda self: getattr(self, key)  # noqa

    def has_permission(self):
        result = None
        with suppress(Exception):
            result = super().has_permission()
        if not result:
            request = getattr(self, "request", None)
            if request and hasattr(request, "event"):
                key = f"pretalx_event_access_{request.event.pk}"
                if key in request.session:
                    sparent = SessionStore(request.session.get(key))
                    parentdata = []
                    with suppress(Exception):
                        parentdata = sparent.load()
                    return "event_access" in parentdata
        return result

    def get_login_url(self):
        """We do this to avoid leaking data about existing pages."""
        raise Http404()

    def handle_no_permission(self):
        request = getattr(self, "request", None)
        if (
            request
            and hasattr(request, "event")
            and request.user.is_anonymous
            and "cfp" in request.resolver_match.namespaces
        ):
            params = "&" + request.GET.urlencode() if request.GET else ""
            return redirect(
                request.event.urls.login + f"?next={quote(request.path)}" + params
            )
        raise Http404()


class EventPermissionRequired(PermissionRequired):
    def get_permission_object(self):
        return self.request.event


class SensibleBackWizardMixin:
    def post(self, *args, **kwargs):
        """Don't redirect if user presses the prev.

        step button, save data instead. The rest of this is copied from
        WizardView. We want to save data when hitting "back"!
        """
        wizard_goto_step = self.request.POST.get("wizard_goto_step")
        management_form = ManagementForm(self.request.POST, prefix=self.prefix)
        if not management_form.is_valid():
            raise forms.ValidationError(
                _("ManagementForm data is missing or has been tampered with."),
                code="missing_management_form",
            )

        form_current_step = management_form.cleaned_data["current_step"]
        if (
            form_current_step != self.steps.current
            and self.storage.current_step is not None
        ):
            # form refreshed, change current step
            self.storage.current_step = form_current_step

        # get the form for the current step
        form = self.get_form(data=self.request.POST, files=self.request.FILES)

        # and try to validate
        if form.is_valid():
            # if the form is valid, store the cleaned data and files.
            self.storage.set_step_data(self.steps.current, self.process_step(form))
            self.storage.set_step_files(
                self.steps.current, self.process_step_files(form)
            )

            # check if the current step is the last step
            if wizard_goto_step and wizard_goto_step in self.get_form_list():
                return self.render_goto_step(wizard_goto_step)
            if self.steps.current == self.steps.last:
                # no more steps, render done view
                return self.render_done(form, **kwargs)
            # proceed to the next step
            return self.render_next_step(form)
        return self.render(form)


class SocialMediaCardMixin:
    def get_image(self):
        raise NotImplementedError

    @csp_exempt()
    def get(self, request, *args, **kwargs):
        try:
            image = self.get_image()
            if image:
                return FileResponse(image)
        except Exception:
            pass
        if self.request.event.logo:
            return FileResponse(self.request.event.logo)
        if self.request.event.header_image:
            return FileResponse(self.request.event.header_image)
        raise Http404()


class PaginationMixin:

    DEFAULT_PAGINATION = 50

    def get_paginate_by(self, queryset=None):
        skey = "stored_page_size_" + self.request.resolver_match.url_name
        default = (
            self.request.session.get(skey)
            or getattr(self, "paginate_by", None)
            or self.DEFAULT_PAGINATION
        )
        if self.request.GET.get("page_size"):
            try:
                if max_page_size := getattr(
                    self, "max_page_size", settings.MAX_PAGINATION_LIMIT
                ):
                    size = min(max_page_size, int(self.request.GET.get("page_size")))
                else:
                    size = int(self.request.GET.get("page_size"))
                self.request.session[skey] = size
                return size
            except ValueError:
                return default
        return default

    def get_context_data(self, **kwargs):
        from pretalx.common.views.generic import CRUDView

        ctx = super().get_context_data(**kwargs)
        if isinstance(self, CRUDView) and self.permission_action != "list":
            return ctx
        return ctx


class ActionConfirmMixin:
    """
    Mixin providing all variables needed for the action_confirm.html template,
    which you can either include via common/includes/action_confirm.html, or
    use directly as common/action_confirm.html.

    Implement at least:
    - action_object_name

    Implement probably:
    - action_back_url

    If the view is not a delete view, also implement:
    - action_confirm_label
    - action_confirm_color
    - action_confirm_icon
    - action_title
    - action_text
    """

    template_name = "common/action_confirm.html"
    action_object_name = None  # Shown between the title and the warning text
    action_text = phrases.base.delete_warning  # Use this for context or warnings
    action_title = phrases.base.delete_confirm_heading  # Shown as heading and as title
    action_confirm_color = "danger"
    action_confirm_icon = "trash"
    action_confirm_label = phrases.base.delete_button

    @property
    def action_back_url(self):
        url_param = self.request.GET.get("next") or self.request.GET.get("back")
        if url_param:
            return urllib.parse.unquote(url_param)
        # Fallback if we don't have a next parameter: go up one level
        return self.request.path.rsplit("/", 2)[0]

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        ctx["action_text"] = self.action_text
        ctx["action_title"] = self.action_title
        ctx["action_object_name"] = self.action_object_name
        ctx["submit_buttons_extra"] = [back_button(self.action_back_url or "..")]
        ctx["submit_buttons"] = [
            Button(
                color=self.action_confirm_color,
                icon=self.action_confirm_icon,
                label=self.action_confirm_label,
            )
        ]
        return ctx


class OrderActionMixin:
    """Change an ordered model with a POST endpoint to a CRUDView list view."""

    extra_actions = {
        "list": {"post": "order_handler"},
    }

    def order_handler(self, request, *args, **kwargs):
        order = request.POST.get("order")
        if order:
            order = order.split(",")
            queryset = self.get_queryset()
            for index, pk in enumerate(order):
                obj = get_object_or_404(queryset, pk=pk)
                obj.position = index
                obj.save(update_fields=["position"])
        return self.list(request, *args, **kwargs)
