import json
import operator
import re
from collections import OrderedDict
from decimal import Decimal
from itertools import groupby
from urllib.parse import urlsplit

from django.conf import settings
from django.contrib import messages
from django.contrib.contenttypes.models import ContentType
from django.core.files import File
from django.core.serializers.json import DjangoJSONEncoder
from django.db import transaction
from django.db.models import ProtectedError
from django.forms import inlineformset_factory
from django.http import (
    Http404, HttpResponse, HttpResponseBadRequest, HttpResponseNotAllowed,
    JsonResponse,
)
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse
from django.utils import translation
from django.utils.functional import cached_property
from django.utils.timezone import now
from django.utils.translation import gettext, gettext_lazy as _
from django.views.generic import DeleteView, FormView, ListView
from django.views.generic.base import TemplateView, View
from django.views.generic.detail import SingleObjectMixin
from i18nfield.strings import LazyI18nString
from pytz import timezone

from pretix.base.channels import get_all_sales_channels
from pretix.base.email import get_available_placeholders
from pretix.base.models import (
    Event, LogEntry, Order, RequiredAction, TaxRule, Voucher,
)
from pretix.base.models.event import EventMetaValue
from pretix.base.services import tickets
from pretix.base.services.invoices import build_preview_invoice_pdf
from pretix.base.signals import register_ticket_outputs
from pretix.base.templatetags.rich_text import markdown_compile_email
from pretix.control.forms.event import (
    CancelSettingsForm, CommentForm, ConfirmTextFormset, EventDeleteForm,
    EventMetaValueForm, EventSettingsForm, EventUpdateForm,
    InvoiceSettingsForm, ItemMetaPropertyForm, MailSettingsForm,
    PaymentSettingsForm, ProviderForm, QuickSetupForm,
    QuickSetupProductFormSet, TaxRuleForm, TaxRuleLineFormSet,
    TicketSettingsForm, WidgetCodeForm,
)
from pretix.control.permissions import EventPermissionRequiredMixin
from pretix.control.views.user import RecentAuthenticationRequiredMixin
from pretix.helpers.database import rolledback_transaction
from pretix.multidomain.urlreverse import get_event_domain
from pretix.plugins.stripe.payment import StripeSettingsHolder
from pretix.presale.style import regenerate_css

from ...base.models.items import ItemMetaProperty
from ...base.settings import LazyI18nStringList
from ..logdisplay import OVERVIEW_BANLIST
from . import CreateView, PaginationMixin, UpdateView


class EventSettingsViewMixin:
    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        ctx['is_event_settings'] = True
        return ctx


class MetaDataEditorMixin:
    meta_form = EventMetaValueForm
    meta_model = EventMetaValue

    @cached_property
    def meta_forms(self):
        if hasattr(self, 'object') and self.object:
            val_instances = {
                v.property_id: v for v in self.object.meta_values.all()
            }
        else:
            val_instances = {}

        formlist = []

        for p in self.request.organizer.meta_properties.all():
            formlist.append(self._make_meta_form(p, val_instances))
        return formlist

    def _make_meta_form(self, p, val_instances):
        return self.meta_form(
            prefix='prop-{}'.format(p.pk),
            property=p,
            instance=val_instances.get(p.pk, self.meta_model(property=p, event=self.object)),
            data=(self.request.POST if self.request.method == "POST" else None)
        )

    def save_meta(self):
        for f in self.meta_forms:
            if f.cleaned_data.get('value'):
                f.save()
            elif f.instance and f.instance.pk:
                f.instance.delete()


class DecoupleMixin:

    def _save_decoupled(self, form):
        # Save fields that are currently only set via the organizer but should be decoupled
        fields = set()
        for f in self.request.POST.getlist("decouple"):
            fields |= set(f.split(","))
        for f in fields:
            if f not in form.fields:
                continue
            if f not in self.request.event.settings._cache():
                self.request.event.settings.set(f, self.request.event.settings.get(f))


class EventUpdate(DecoupleMixin, EventSettingsViewMixin, EventPermissionRequiredMixin, MetaDataEditorMixin, UpdateView):
    model = Event
    form_class = EventUpdateForm
    template_name = 'pretixcontrol/event/settings.html'
    permission = 'can_change_event_settings'

    @cached_property
    def object(self) -> Event:
        return self.request.event

    def get_object(self, queryset=None) -> Event:
        return self.object

    @cached_property
    def sform(self):
        return EventSettingsForm(
            obj=self.object,
            prefix='settings',
            data=self.request.POST if self.request.method == 'POST' else None,
            files=self.request.FILES if self.request.method == 'POST' else None,
        )

    def get_context_data(self, *args, **kwargs) -> dict:
        context = super().get_context_data(*args, **kwargs)
        context['sform'] = self.sform
        context['meta_forms'] = self.meta_forms
        context['item_meta_property_formset'] = self.item_meta_property_formset
        context['confirm_texts_formset'] = self.confirm_texts_formset
        return context

    @transaction.atomic
    def form_valid(self, form):
        self._save_decoupled(self.sform)
        self.sform.save()
        self.save_meta()
        self.save_item_meta_property_formset(self.object)
        self.save_confirm_texts_formset(self.object)
        change_css = False

        if self.sform.has_changed() or self.confirm_texts_formset.has_changed():
            data = {k: self.request.event.settings.get(k) for k in self.sform.changed_data}
            if self.confirm_texts_formset.has_changed():
                data.update(confirm_texts=self.confirm_texts_formset.cleaned_data)
            self.request.event.log_action('pretix.event.settings', user=self.request.user, data=data)
            display_properties = (
                'primary_color', 'theme_color_success', 'theme_color_danger', 'primary_font',
                'theme_color_background', 'theme_round_borders',
            )
            if any(p in self.sform.changed_data for p in display_properties):
                change_css = True
        if form.has_changed():
            self.request.event.log_action('pretix.event.changed', user=self.request.user, data={
                k: (form.cleaned_data.get(k).name
                    if isinstance(form.cleaned_data.get(k), File)
                    else form.cleaned_data.get(k))
                for k in form.changed_data
            })

        if change_css:
            regenerate_css.apply_async(args=(self.request.event.pk,))
            messages.success(self.request, _('Your changes have been saved. Please note that it can '
                                             'take a short period of time until your changes become '
                                             'active.'))
        else:
            messages.success(self.request, _('Your changes have been saved.'))
        return super().form_valid(form)

    def get_success_url(self) -> str:
        return reverse('control:event.settings', kwargs={
            'organizer': self.object.organizer.slug,
            'event': self.object.slug,
        })

    def get_form_kwargs(self):
        kwargs = super().get_form_kwargs()
        if self.request.user.has_active_staff_session(self.request.session.session_key):
            kwargs['change_slug'] = True
            kwargs['domain'] = True
        return kwargs

    def post(self, request, *args, **kwargs):
        form = self.get_form()
        if form.is_valid() and self.sform.is_valid() and all([f.is_valid() for f in self.meta_forms]) and \
                self.item_meta_property_formset.is_valid() and self.confirm_texts_formset.is_valid():
            # reset timezone
            zone = timezone(self.sform.cleaned_data['timezone'])
            event = form.instance
            event.date_from = self.reset_timezone(zone, event.date_from)
            event.date_to = self.reset_timezone(zone, event.date_to)
            event.presale_start = self.reset_timezone(zone, event.presale_start)
            event.presale_end = self.reset_timezone(zone, event.presale_end)
            return self.form_valid(form)
        else:
            messages.error(self.request, _('We could not save your changes. See below for details.'))
            return self.form_invalid(form)

    @staticmethod
    def reset_timezone(tz, dt):
        return tz.localize(dt.replace(tzinfo=None)) if dt is not None else None

    @cached_property
    def item_meta_property_formset(self):
        formsetclass = inlineformset_factory(
            Event, ItemMetaProperty,
            form=ItemMetaPropertyForm, can_order=False, can_delete=True, extra=0
        )
        return formsetclass(self.request.POST if self.request.method == "POST" else None, prefix="item-meta-property",
                            instance=self.object, queryset=self.object.item_meta_properties.all())

    def save_item_meta_property_formset(self, obj):
        for form in self.item_meta_property_formset.initial_forms:
            if form in self.item_meta_property_formset.deleted_forms:
                if not form.instance.pk:
                    continue
                form.instance.delete()
                form.instance.pk = None
            elif form.has_changed():
                form.save()

        for form in self.item_meta_property_formset.extra_forms:
            if not form.has_changed():
                continue
            if self.item_meta_property_formset._should_delete_form(form):
                continue
            form.instance.event = obj
            form.save()

    @cached_property
    def confirm_texts_formset(self):
        initial = [{"text": text, "ORDER": order} for order, text in
                   enumerate(self.object.settings.get("confirm_texts", as_type=LazyI18nStringList))]
        return ConfirmTextFormset(self.request.POST if self.request.method == "POST" else None, event=self.object,
                                  prefix="confirm-texts", initial=initial)

    def save_confirm_texts_formset(self, obj):
        obj.settings.confirm_texts = LazyI18nStringList(
            form_data['text'].data
            for form_data in sorted(self.confirm_texts_formset.cleaned_data, key=operator.itemgetter("ORDER"))
            if not form_data.get("DELETE", False)
        )


class EventPlugins(EventSettingsViewMixin, EventPermissionRequiredMixin, TemplateView, SingleObjectMixin):
    model = Event
    context_object_name = 'event'
    permission = 'can_change_event_settings'
    template_name = 'pretixcontrol/event/plugins.html'

    def get_object(self, queryset=None) -> Event:
        return self.request.event

    def get_context_data(self, *args, **kwargs) -> dict:
        from pretix.base.plugins import get_all_plugins

        context = super().get_context_data(*args, **kwargs)
        plugins = [p for p in get_all_plugins(self.object) if not p.name.startswith('.')
                   and getattr(p, 'visible', True)]
        order = [
            'FEATURE',
            'PAYMENT',
            'INTEGRATION',
            'CUSTOMIZATION',
            'FORMAT',
            'API',
        ]
        labels = {
            'FEATURE': _('Features'),
            'PAYMENT': _('Payment providers'),
            'INTEGRATION': _('Integrations'),
            'CUSTOMIZATION': _('Customizations'),
            'FORMAT': _('Output and export formats'),
            'API': _('API features'),
        }
        context['plugins'] = sorted([
            (c, labels.get(c, c), list(plist))
            for c, plist
            in groupby(
                sorted(plugins, key=lambda p: str(getattr(p, 'category', _('Other')))),
                lambda p: str(getattr(p, 'category', _('Other')))
            )
        ], key=lambda c: (order.index(c[0]), c[1]) if c[0] in order else (999, str(c[1])))
        context['plugins_active'] = self.object.get_plugins()
        return context

    def get(self, request, *args, **kwargs):
        self.object = self.get_object()
        context = self.get_context_data(object=self.object)
        return self.render_to_response(context)

    def post(self, request, *args, **kwargs):
        from pretix.base.plugins import get_all_plugins

        self.object = self.get_object()

        plugins_available = {
            p.module: p for p in get_all_plugins(self.object)
            if not p.name.startswith('.') and getattr(p, 'visible', True)
        }

        with transaction.atomic():
            allow_restricted = request.user.has_active_staff_session(request.session.session_key)

            for key, value in request.POST.items():
                if key.startswith("plugin:"):
                    module = key.split(":")[1]
                    if value == "enable" and module in plugins_available:
                        if getattr(plugins_available[module], 'restricted', False):
                            if not allow_restricted:
                                continue

                        self.request.event.log_action('pretix.event.plugins.enabled', user=self.request.user,
                                                      data={'plugin': module})
                        self.object.enable_plugin(module, allow_restricted=allow_restricted)
                    else:
                        self.request.event.log_action('pretix.event.plugins.disabled', user=self.request.user,
                                                      data={'plugin': module})
                        self.object.disable_plugin(module)
            self.object.save()
        messages.success(self.request, _('Your changes have been saved.'))
        return redirect(self.get_success_url())

    def get_success_url(self) -> str:
        return reverse('control:event.settings.plugins', kwargs={
            'organizer': self.get_object().organizer.slug,
            'event': self.get_object().slug,
        })


class PaymentProviderSettings(EventSettingsViewMixin, EventPermissionRequiredMixin, TemplateView, SingleObjectMixin):
    model = Event
    context_object_name = 'event'
    permission = 'can_change_event_settings'
    template_name = 'pretixcontrol/event/payment_provider.html'

    def get_success_url(self) -> str:
        return reverse('control:event.settings.payment', kwargs={
            'organizer': self.get_object().organizer.slug,
            'event': self.get_object().slug,
        })

    @cached_property
    def object(self):
        return self.request.event

    def get_object(self, queryset=None):
        return self.object

    @cached_property
    def provider(self):
        provider = self.request.event.get_payment_providers().get(self.kwargs['provider'])
        return provider

    @cached_property
    def form(self):
        form = ProviderForm(
            obj=self.request.event,
            settingspref=self.provider.settings.get_prefix(),
            data=(self.request.POST if self.request.method == 'POST' else None),
            files=(self.request.FILES if self.request.method == 'POST' else None),
            provider=self.provider
        )
        form.fields = OrderedDict(
            [
                ('%s%s' % (self.provider.settings.get_prefix(), k), v)
                for k, v in self.provider.settings_form_fields.items()
            ]
        )
        form.prepare_fields()
        return form

    def dispatch(self, request, *args, **kwargs):
        if not self.provider:
            messages.error(self.request, _('This payment provider does not exist or the respective plugin is '
                                           'disabled.'))
            return redirect(self.get_success_url())
        return super().dispatch(request, *args, **kwargs)

    @cached_property
    def settings_content(self):
        return self.provider.settings_content_render(self.request)

    def get_context_data(self, *args, **kwargs) -> dict:
        context = super().get_context_data(*args, **kwargs)
        context['form'] = self.form
        context['provider'] = self.provider
        context['settings_content'] = self.settings_content
        return context

    @transaction.atomic
    def post(self, request, *args, **kwargs):
        if self.form.is_valid():
            if self.form.has_changed():
                self.request.event.log_action(
                    'pretix.event.payment.provider.' + self.provider.identifier, user=self.request.user, data={
                        k: self.form.cleaned_data.get(k) for k in self.form.changed_data
                    }
                )
                self.form.save()
            messages.success(self.request, _('Your changes have been saved.'))
            return redirect(self.get_success_url())
        else:
            messages.error(self.request, _('We could not save your changes. See below for details.'))
            return self.get(request)


class EventSettingsFormView(EventPermissionRequiredMixin, DecoupleMixin, FormView):
    model = Event
    permission = 'can_change_event_settings'

    def get_context_data(self, *args, **kwargs) -> dict:
        context = super().get_context_data(*args, **kwargs)
        return context

    def get_form_kwargs(self):
        kwargs = super().get_form_kwargs()
        kwargs['obj'] = self.request.event
        return kwargs

    def form_success(self):
        pass

    @transaction.atomic
    def post(self, request, *args, **kwargs):
        form = self.get_form()
        if form.is_valid():
            form.save()
            self._save_decoupled(form)
            if form.has_changed():
                self.request.event.log_action(
                    'pretix.event.settings', user=self.request.user, data={
                        k: (form.cleaned_data.get(k).name
                            if isinstance(form.cleaned_data.get(k), File)
                            else form.cleaned_data.get(k))
                        for k in form.changed_data
                    }
                )
            self.form_success()
            messages.success(self.request, _('Your changes have been saved.'))
            return redirect(self.get_success_url())
        else:
            messages.error(self.request, _('We could not save your changes. See below for details.'))
            return self.render_to_response(self.get_context_data(form=form))


class PaymentSettings(EventSettingsViewMixin, EventSettingsFormView):
    template_name = 'pretixcontrol/event/payment.html'
    form_class = PaymentSettingsForm
    permission = 'can_change_event_settings'

    def get_success_url(self) -> str:
        return reverse('control:event.settings.payment', kwargs={
            'organizer': self.request.organizer.slug,
            'event': self.request.event.slug,
        })

    def get_context_data(self, *args, **kwargs) -> dict:
        context = super().get_context_data(*args, **kwargs)
        context['providers'] = sorted(
            [p for p in self.request.event.get_payment_providers().values()
             if not (p.is_implicit(self.request) if callable(p.is_implicit) else p.is_implicit) and
             (p.settings_form_fields or p.settings_content_render(self.request))],
            key=lambda s: s.verbose_name
        )

        sales_channels = get_all_sales_channels()
        for p in context['providers']:
            p.show_enabled = p.is_enabled
            p.sales_channels = [sales_channels[channel] for channel in p.settings.get('_restrict_to_sales_channels', as_type=list, default=['web'])]
            if p.is_meta:
                p.show_enabled = p.settings._enabled in (True, 'True')
        return context


class InvoiceSettings(EventSettingsViewMixin, EventSettingsFormView):
    model = Event
    form_class = InvoiceSettingsForm
    template_name = 'pretixcontrol/event/invoicing.html'
    permission = 'can_change_event_settings'

    def get_success_url(self) -> str:
        if 'preview' in self.request.POST:
            return reverse('control:event.settings.invoice.preview', kwargs={
                'organizer': self.request.event.organizer.slug,
                'event': self.request.event.slug
            })
        return reverse('control:event.settings.invoice', kwargs={
            'organizer': self.request.event.organizer.slug,
            'event': self.request.event.slug
        })


class CancelSettings(EventSettingsViewMixin, EventSettingsFormView):
    model = Event
    form_class = CancelSettingsForm
    template_name = 'pretixcontrol/event/cancel.html'
    permission = 'can_change_event_settings'

    def get_success_url(self) -> str:
        return reverse('control:event.settings.cancel', kwargs={
            'organizer': self.request.event.organizer.slug,
            'event': self.request.event.slug
        })

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data()
        ctx['gets_notification'] = self.request.user.notifications_send and (
            (
                self.request.user.notification_settings.filter(
                    event=self.request.event,
                    action_type='pretix.event.order.refund.requested',
                    enabled=True
                ).exists()
            ) or (
                self.request.user.notification_settings.filter(
                    event__isnull=True,
                    action_type='pretix.event.order.refund.requested',
                    enabled=True
                ).exists() and not
                self.request.user.notification_settings.filter(
                    event=self.request.event,
                    action_type='pretix.event.order.refund.requested',
                    enabled=False
                ).exists()
            )
        )
        return ctx


class InvoicePreview(EventPermissionRequiredMixin, View):
    permission = 'can_change_event_settings'

    def get(self, request, *args, **kwargs):
        fname, ftype, fcontent = build_preview_invoice_pdf(request.event)
        resp = HttpResponse(fcontent, content_type=ftype)
        resp['Content-Disposition'] = 'attachment; filename="{}"'.format(fname)
        return resp


class DangerZone(EventPermissionRequiredMixin, TemplateView):
    permission = 'can_change_event_settings'
    template_name = 'pretixcontrol/event/dangerzone.html'


class DisplaySettings(View):
    def get(self, request, *wargs, **kwargs):
        return redirect(reverse('control:event.settings', kwargs={
            'organizer': self.request.event.organizer.slug,
            'event': self.request.event.slug
        }) + '#tab-0-3-open')


class MailSettings(EventSettingsViewMixin, EventSettingsFormView):
    model = Event
    form_class = MailSettingsForm
    template_name = 'pretixcontrol/event/mail.html'
    permission = 'can_change_event_settings'

    def get_success_url(self) -> str:
        return reverse('control:event.settings.mail', kwargs={
            'organizer': self.request.event.organizer.slug,
            'event': self.request.event.slug
        })

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        ctx['renderers'] = self.request.event.get_html_mail_renderers()
        return ctx

    @transaction.atomic
    def post(self, request, *args, **kwargs):
        form = self.get_form()
        if form.is_valid():
            form.save()
            if form.has_changed():
                self.request.event.log_action(
                    'pretix.event.settings', user=self.request.user, data={
                        k: form.cleaned_data.get(k) for k in form.changed_data
                    }
                )

            if request.POST.get('test', '0').strip() == '1':
                backend = self.request.event.get_mail_backend(force_custom=True)
                try:
                    backend.test(self.request.event.settings.mail_from)
                except Exception as e:
                    messages.warning(self.request, _('An error occurred while contacting the SMTP server: %s') % str(e))
                else:
                    if form.cleaned_data.get('smtp_use_custom'):
                        messages.success(self.request, _('Your changes have been saved and the connection attempt to '
                                                         'your SMTP server was successful.'))
                    else:
                        messages.success(self.request, _('We\'ve been able to contact the SMTP server you configured. '
                                                         'Remember to check the "use custom SMTP server" checkbox, '
                                                         'otherwise your SMTP server will not be used.'))
            else:
                messages.success(self.request, _('Your changes have been saved.'))
            return redirect(self.get_success_url())
        else:
            messages.error(self.request, _('We could not save your changes. See below for details.'))
            return self.get(request)


class MailSettingsPreview(EventPermissionRequiredMixin, View):
    permission = 'can_change_event_settings'

    # return the origin text if key is missing in dict
    class SafeDict(dict):
        def __missing__(self, key):
            return '{' + key + '}'

    # create index-language mapping
    @cached_property
    def supported_locale(self):
        locales = {}
        for idx, val in enumerate(settings.LANGUAGES):
            if val[0] in self.request.event.settings.locales:
                locales[str(idx)] = val[0]
        return locales

    # get all supported placeholders with dummy values
    def placeholders(self, item):
        ctx = {}
        for p in get_available_placeholders(self.request.event, MailSettingsForm.base_context[item]).values():
            s = str(p.render_sample(self.request.event))
            if s.strip().startswith('*'):
                ctx[p.identifier] = s
            else:
                ctx[p.identifier] = '<span class="placeholder" title="{}">{}</span>'.format(
                    _('This value will be replaced based on dynamic parameters.'),
                    s
                )
        return self.SafeDict(ctx)

    def post(self, request, *args, **kwargs):
        preview_item = request.POST.get('item', '')
        if preview_item not in MailSettingsForm.base_context:
            return HttpResponseBadRequest(_('invalid item'))

        regex = r"^" + re.escape(preview_item) + r"_(?P<idx>[\d+])$"
        msgs = {}
        for k, v in request.POST.items():
            # only accept allowed fields
            matched = re.search(regex, k)
            if matched is not None:
                idx = matched.group('idx')
                if idx in self.supported_locale:
                    with translation.override(self.supported_locale[idx]):
                        msgs[self.supported_locale[idx]] = markdown_compile_email(
                            v.format_map(self.placeholders(preview_item))
                        )

        return JsonResponse({
            'item': preview_item,
            'msgs': msgs
        })


class MailSettingsRendererPreview(MailSettingsPreview):
    permission = 'can_change_event_settings'

    def post(self, request, *args, **kwargs):
        return HttpResponse(status=405)

    def get(self, request, *args, **kwargs):
        v = str(request.event.settings.mail_text_order_placed)
        v = v.format_map(self.placeholders('mail_text_order_placed'))
        renderers = request.event.get_html_mail_renderers()
        if request.GET.get('renderer') in renderers:
            with rolledback_transaction():
                order = request.event.orders.create(status=Order.STATUS_PENDING, datetime=now(),
                                                    expires=now(), code="PREVIEW", total=119)
                item = request.event.items.create(name=gettext("Sample product"), default_price=42.23,
                                                  description=gettext("Sample product description"))
                p = order.positions.create(item=item, attendee_name_parts={'_legacy': gettext("John Doe")},
                                           price=item.default_price)
                v = renderers[request.GET.get('renderer')].render(
                    v,
                    str(request.event.settings.mail_text_signature),
                    gettext('Your order: %(code)s') % {'code': order.code},
                    order,
                    position=p
                )
                r = HttpResponse(v, content_type='text/html')
                r._csp_ignore = True
                return r
        else:
            raise Http404(_('Unknown e-mail renderer.'))


class TicketSettingsPreview(EventPermissionRequiredMixin, View):
    permission = 'can_change_event_settings'

    @cached_property
    def output(self):
        responses = register_ticket_outputs.send(self.request.event)
        for receiver, response in responses:
            provider = response(self.request.event)
            if provider.identifier == self.kwargs.get('output'):
                return provider

    def get(self, request, *args, **kwargs):
        if not self.output:
            messages.error(request, _('You requested an invalid ticket output type.'))
            return redirect(self.get_error_url())

        fname, mimet, data = tickets.preview(self.request.event.pk, self.output.identifier)
        resp = HttpResponse(data, content_type=mimet)
        ftype = fname.split(".")[-1]
        resp['Content-Disposition'] = 'attachment; filename="ticket-preview.{}"'.format(ftype)
        return resp

    def get_error_url(self) -> str:
        return reverse('control:event.settings.tickets', kwargs={
            'organizer': self.request.event.organizer.slug,
            'event': self.request.event.slug
        })


class TicketSettings(EventSettingsViewMixin, EventPermissionRequiredMixin, FormView):
    model = Event
    form_class = TicketSettingsForm
    template_name = 'pretixcontrol/event/tickets.html'
    permission = 'can_change_event_settings'

    def get_context_data(self, *args, **kwargs) -> dict:
        context = super().get_context_data(*args, **kwargs)
        context['providers'] = self.provider_forms

        context['any_enabled'] = False
        responses = register_ticket_outputs.send(self.request.event)
        for receiver, response in responses:
            provider = response(self.request.event)
            if provider.is_enabled:
                context['any_enabled'] = True
                break

        return context

    def get_success_url(self) -> str:
        return reverse('control:event.settings.tickets', kwargs={
            'organizer': self.request.event.organizer.slug,
            'event': self.request.event.slug
        })

    def get_form_kwargs(self):
        kwargs = super().get_form_kwargs()
        kwargs['obj'] = self.request.event
        return kwargs

    def get_form(self, form_class=None):
        form = super().get_form(form_class)
        form.prepare_fields()
        return form

    def form_invalid(self, form):
        messages.error(self.request, _('We could not save your changes. See below for details.'))
        return super().form_invalid(form)

    @transaction.atomic
    def post(self, request, *args, **kwargs):
        success = True
        for provider in self.provider_forms:
            if provider.form.is_valid():
                provider.form.save()
                if provider.form.has_changed():
                    self.request.event.log_action(
                        'pretix.event.tickets.provider.' + provider.identifier, user=self.request.user, data={
                            k: (provider.form.cleaned_data.get(k).name
                                if isinstance(provider.form.cleaned_data.get(k), File)
                                else provider.form.cleaned_data.get(k))
                            for k in provider.form.changed_data
                        }
                    )
                    tickets.invalidate_cache.apply_async(kwargs={'event': self.request.event.pk, 'provider': provider.identifier})
            else:
                success = False
        form = self.get_form(self.get_form_class())
        if success and form.is_valid():
            form.save()
            if form.has_changed():
                self.request.event.log_action(
                    'pretix.event.tickets.settings', user=self.request.user, data={
                        k: form.cleaned_data.get(k) for k in form.changed_data
                    }
                )

            messages.success(self.request, _('Your changes have been saved.'))
            return redirect(self.get_success_url())
        else:
            return self.form_invalid(form)

    @cached_property
    def provider_forms(self) -> list:
        providers = []
        responses = register_ticket_outputs.send(self.request.event)
        for receiver, response in responses:
            provider = response(self.request.event)
            provider.form = ProviderForm(
                obj=self.request.event,
                settingspref='ticketoutput_%s_' % provider.identifier,
                data=(self.request.POST if self.request.method == 'POST' else None),
                files=(self.request.FILES if self.request.method == 'POST' else None)
            )
            provider.form.fields = OrderedDict(
                [
                    ('ticketoutput_%s_%s' % (provider.identifier, k), v)
                    for k, v in provider.settings_form_fields.items()
                ]
            )
            provider.settings_content = provider.settings_content_render(self.request)
            provider.form.prepare_fields()

            provider.evaluated_preview_allowed = True
            if not provider.preview_allowed:
                provider.evaluated_preview_allowed = False
            else:
                for k, v in provider.settings_form_fields.items():
                    if v.required and not self.request.event.settings.get('ticketoutput_%s_%s' % (provider.identifier, k)):
                        provider.evaluated_preview_allowed = False
                        break

            providers.append(provider)
        return providers


class EventPermissions(EventSettingsViewMixin, EventPermissionRequiredMixin, TemplateView):
    template_name = 'pretixcontrol/event/permissions.html'


class EventLive(EventPermissionRequiredMixin, TemplateView):
    permission = 'can_change_event_settings'
    template_name = 'pretixcontrol/event/live.html'

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        ctx['issues'] = self.request.event.live_issues
        ctx['actual_orders'] = self.request.event.orders.filter(testmode=False).exists()
        return ctx

    def post(self, request, *args, **kwargs):
        if request.POST.get("live") == "true" and not self.request.event.live_issues:
            with transaction.atomic():
                request.event.live = True
                request.event.save()
                self.request.event.log_action(
                    'pretix.event.live.activated', user=self.request.user, data={}
                )
            messages.success(self.request, _('Your shop is live now!'))
        elif request.POST.get("live") == "false":
            with transaction.atomic():
                request.event.live = False
                request.event.save()
                self.request.event.log_action(
                    'pretix.event.live.deactivated', user=self.request.user, data={}
                )
            messages.success(self.request, _('We\'ve taken your shop down. You can re-enable it whenever you want!'))
        elif request.POST.get("testmode") == "true":
            with transaction.atomic():
                request.event.testmode = True
                request.event.save()
                self.request.event.log_action(
                    'pretix.event.testmode.activated', user=self.request.user, data={}
                )
            messages.success(self.request, _('Your shop is now in test mode!'))
        elif request.POST.get("testmode") == "false":
            with transaction.atomic():
                request.event.testmode = False
                request.event.save()
                self.request.event.log_action(
                    'pretix.event.testmode.deactivated', user=self.request.user, data={
                        'delete': (request.POST.get("delete") == "yes")
                    }
                )
            request.event.cache.delete('complain_testmode_orders')
            if request.POST.get("delete") == "yes":
                try:
                    with transaction.atomic():
                        for order in request.event.orders.filter(testmode=True):
                            order.gracefully_delete(user=self.request.user)
                except ProtectedError:
                    messages.error(self.request, _('An order could not be deleted as some constraints (e.g. data '
                                                   'created by plug-ins) do not allow it.'))
                else:
                    request.event.cache.set('complain_testmode_orders', False, 30)
            request.event.cartposition_set.filter(addon_to__isnull=False).delete()
            request.event.cartposition_set.all().delete()
            messages.success(self.request, _('We\'ve disabled test mode for you. Let\'s sell some real tickets!'))
        return redirect(self.get_success_url())

    def get_success_url(self) -> str:
        return reverse('control:event.live', kwargs={
            'organizer': self.request.event.organizer.slug,
            'event': self.request.event.slug
        })


class EventDelete(RecentAuthenticationRequiredMixin, EventPermissionRequiredMixin, FormView):
    permission = 'can_change_event_settings'
    template_name = 'pretixcontrol/event/delete.html'
    form_class = EventDeleteForm

    def post(self, request, *args, **kwargs):
        if not self.request.event.allow_delete():
            messages.error(self.request, _('This event can not be deleted.'))
            return self.get(self.request, *self.args, **self.kwargs)
        return super().post(request, *args, **kwargs)

    def get_form_kwargs(self):
        kwargs = super().get_form_kwargs()
        kwargs['event'] = self.request.event
        return kwargs

    def form_valid(self, form):
        try:
            with transaction.atomic():
                self.request.organizer.log_action(
                    'pretix.event.deleted', user=self.request.user,
                    data={
                        'event_id': self.request.event.pk,
                        'name': str(self.request.event.name),
                        'slug': self.request.event.slug,
                        'logentries': list(self.request.event.logentry_set.values_list('pk', flat=True))
                    }
                )
                self.request.event.delete_sub_objects()
                self.request.event.delete()
            messages.success(self.request, _('The event has been deleted.'))
            return redirect(self.get_success_url())
        except ProtectedError:
            messages.error(self.request, _('The event could not be deleted as some constraints (e.g. data created by '
                                           'plug-ins) do not allow it.'))
            return self.get(self.request, *self.args, **self.kwargs)

    def get_success_url(self) -> str:
        return reverse('control:index')


class EventLog(EventPermissionRequiredMixin, ListView):
    template_name = 'pretixcontrol/event/logs.html'
    model = LogEntry
    context_object_name = 'logs'
    paginate_by = 20

    def get_queryset(self):
        qs = self.request.event.logentry_set.all().select_related(
            'user', 'content_type', 'api_token', 'oauth_application', 'device'
        ).order_by('-datetime')
        qs = qs.exclude(action_type__in=OVERVIEW_BANLIST)
        if not self.request.user.has_event_permission(self.request.organizer, self.request.event, 'can_view_orders',
                                                      request=self.request):
            qs = qs.exclude(content_type=ContentType.objects.get_for_model(Order))
        if not self.request.user.has_event_permission(self.request.organizer, self.request.event, 'can_view_vouchers',
                                                      request=self.request):
            qs = qs.exclude(content_type=ContentType.objects.get_for_model(Voucher))

        if self.request.GET.get('user') == 'yes':
            qs = qs.filter(user__isnull=False)
        elif self.request.GET.get('user') == 'no':
            qs = qs.filter(user__isnull=True)
        elif self.request.GET.get('user', '').startswith('d-'):
            qs = qs.filter(device_id=self.request.GET.get('user')[2:])
        elif self.request.GET.get('user'):
            qs = qs.filter(user_id=self.request.GET.get('user'))

        if self.request.GET.get('content_type'):
            qs = qs.filter(content_type=get_object_or_404(ContentType, pk=self.request.GET.get('content_type')))

            if self.request.GET.get('object'):
                qs = qs.filter(object_id=self.request.GET.get('object'))

        return qs

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data()
        ctx['userlist'] = self.request.event.logentry_set.order_by().distinct().values('user__id', 'user__email')
        ctx['devicelist'] = self.request.event.logentry_set.order_by('device__name').distinct().values('device__id', 'device__name')
        return ctx


class EventActions(EventPermissionRequiredMixin, ListView):
    template_name = 'pretixcontrol/event/actions.html'
    model = RequiredAction
    context_object_name = 'actions'
    paginate_by = 20
    permission = 'can_change_orders'

    def get_queryset(self):
        qs = self.request.event.requiredaction_set.filter(done=False)
        return qs

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data()
        for a in ctx['actions']:
            a.display = a.display(self.request)
        return ctx


class EventActionDiscard(EventPermissionRequiredMixin, View):
    permission = 'can_change_orders'

    def get(self, request, **kwargs):
        action = get_object_or_404(RequiredAction, event=request.event, pk=kwargs.get('id'))
        action.done = True
        action.user = request.user
        action.save()
        messages.success(self.request, _('The issue has been marked as resolved!'))
        return redirect(self.get_success_url())

    def get_success_url(self) -> str:
        return reverse('control:event.index', kwargs={
            'organizer': self.request.event.organizer.slug,
            'event': self.request.event.slug
        })


class EventComment(EventPermissionRequiredMixin, View):
    permission = 'can_change_event_settings'

    def post(self, *args, **kwargs):
        form = CommentForm(self.request.POST)
        if form.is_valid():
            self.request.event.comment = form.cleaned_data.get('comment')
            self.request.event.save()
            self.request.event.log_action('pretix.event.comment', user=self.request.user, data={
                'new_comment': form.cleaned_data.get('comment')
            })
            messages.success(self.request, _('The comment has been updated.'))
        else:
            messages.error(self.request, _('Could not update the comment.'))
        return redirect(self.get_success_url())

    def get(self, *args, **kwargs):
        return HttpResponseNotAllowed(['POST'])

    def get_success_url(self) -> str:
        return reverse('control:event.index', kwargs={
            'organizer': self.request.event.organizer.slug,
            'event': self.request.event.slug
        })


class TaxList(EventSettingsViewMixin, EventPermissionRequiredMixin, PaginationMixin, ListView):
    model = TaxRule
    context_object_name = 'taxrules'
    template_name = 'pretixcontrol/event/tax_index.html'
    permission = 'can_change_event_settings'

    def get_queryset(self):
        return self.request.event.tax_rules.all()


class TaxCreate(EventSettingsViewMixin, EventPermissionRequiredMixin, CreateView):
    model = TaxRule
    form_class = TaxRuleForm
    template_name = 'pretixcontrol/event/tax_edit.html'
    permission = 'can_change_event_settings'
    context_object_name = 'taxrule'

    def get_success_url(self) -> str:
        return reverse('control:event.settings.tax', kwargs={
            'organizer': self.request.event.organizer.slug,
            'event': self.request.event.slug,
        })

    def get_initial(self):
        return {
            'name': LazyI18nString.from_gettext(gettext('VAT'))
        }

    def post(self, request, *args, **kwargs):
        form = self.get_form()
        if form.is_valid() and self.formset.is_valid():
            return self.form_valid(form)
        else:
            return self.form_invalid(form)

    @cached_property
    def formset(self):
        return TaxRuleLineFormSet(
            data=self.request.POST if self.request.method == "POST" else None,
        )

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        ctx['formset'] = self.formset
        return ctx

    @transaction.atomic
    def form_valid(self, form):
        form.instance.event = self.request.event
        form.instance.custom_rules = json.dumps([
            f.cleaned_data for f in self.formset if f not in self.formset.deleted_forms
        ], cls=DjangoJSONEncoder)
        messages.success(self.request, _('The new tax rule has been created.'))
        ret = super().form_valid(form)
        form.instance.log_action('pretix.event.taxrule.added', user=self.request.user, data=dict(form.cleaned_data))
        return ret

    def form_invalid(self, form):
        messages.error(self.request, _('We could not save your changes. See below for details.'))
        return super().form_invalid(form)


class TaxUpdate(EventSettingsViewMixin, EventPermissionRequiredMixin, UpdateView):
    model = TaxRule
    form_class = TaxRuleForm
    template_name = 'pretixcontrol/event/tax_edit.html'
    permission = 'can_change_event_settings'
    context_object_name = 'rule'

    def get_object(self, queryset=None) -> TaxRule:
        try:
            return self.request.event.tax_rules.get(
                id=self.kwargs['rule']
            )
        except TaxRule.DoesNotExist:
            raise Http404(_("The requested tax rule does not exist."))

    def post(self, request, *args, **kwargs):
        self.object = self.get_object(self.get_queryset())
        form = self.get_form()
        if form.is_valid() and self.formset.is_valid():
            return self.form_valid(form)
        else:
            return self.form_invalid(form)

    @cached_property
    def formset(self):
        return TaxRuleLineFormSet(
            data=self.request.POST if self.request.method == "POST" else None,
            initial=json.loads(self.object.custom_rules) if self.object.custom_rules else []
        )

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        ctx['formset'] = self.formset
        return ctx

    @transaction.atomic
    def form_valid(self, form):
        messages.success(self.request, _('Your changes have been saved.'))
        form.instance.custom_rules = json.dumps([
            f.cleaned_data for f in self.formset if f not in self.formset.deleted_forms
        ], cls=DjangoJSONEncoder)
        if form.has_changed():
            self.object.log_action(
                'pretix.event.taxrule.changed', user=self.request.user, data={
                    k: form.cleaned_data.get(k) for k in form.changed_data
                }
            )
        return super().form_valid(form)

    def get_success_url(self) -> str:
        return reverse('control:event.settings.tax', kwargs={
            'organizer': self.request.event.organizer.slug,
            'event': self.request.event.slug,
        })

    def form_invalid(self, form):
        messages.error(self.request, _('We could not save your changes. See below for details.'))
        return super().form_invalid(form)


class TaxDelete(EventSettingsViewMixin, EventPermissionRequiredMixin, DeleteView):
    model = TaxRule
    template_name = 'pretixcontrol/event/tax_delete.html'
    permission = 'can_change_event_settings'
    context_object_name = 'taxrule'

    def get_object(self, queryset=None) -> TaxRule:
        try:
            return self.request.event.tax_rules.get(
                id=self.kwargs['rule']
            )
        except TaxRule.DoesNotExist:
            raise Http404(_("The requested tax rule does not exist."))

    @transaction.atomic
    def delete(self, request, *args, **kwargs):
        self.object = self.get_object()
        success_url = self.get_success_url()
        if self.object.allow_delete():
            self.object.log_action(action='pretix.event.taxrule.deleted', user=request.user)
            self.object.delete()
            messages.success(self.request, _('The selected tax rule has been deleted.'))
        else:
            messages.error(self.request, _('The selected tax rule can not be deleted.'))
        return redirect(success_url)

    def get_success_url(self) -> str:
        return reverse('control:event.settings.tax', kwargs={
            'organizer': self.request.event.organizer.slug,
            'event': self.request.event.slug,
        })

    def get_context_data(self, *args, **kwargs) -> dict:
        context = super().get_context_data(*args, **kwargs)
        context['possible'] = self.object.allow_delete()
        return context


class WidgetSettings(EventSettingsViewMixin, EventPermissionRequiredMixin, FormView):
    template_name = 'pretixcontrol/event/widget.html'
    permission = 'can_change_event_settings'
    form_class = WidgetCodeForm

    def get_form_kwargs(self):
        kwargs = super().get_form_kwargs()
        kwargs['event'] = self.request.event
        return kwargs

    def form_valid(self, form):
        ctx = self.get_context_data()
        ctx['form'] = form
        ctx['valid'] = True
        return self.render_to_response(ctx)

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        ctx['urlprefix'] = settings.SITE_URL
        domain = get_event_domain(self.request.event, fallback=True)
        if domain:
            siteurlsplit = urlsplit(settings.SITE_URL)
            if siteurlsplit.port and siteurlsplit.port not in (80, 443):
                domain = '%s:%d' % (domain, siteurlsplit.port)
            ctx['urlprefix'] = '%s://%s' % (siteurlsplit.scheme, domain)
        return ctx


class QuickSetupView(FormView):
    template_name = 'pretixcontrol/event/quick_setup.html'
    permission = 'can_change_event_settings'
    form_class = QuickSetupForm

    def dispatch(self, request, *args, **kwargs):
        if request.event.items.exists() or request.event.quotas.exists():
            messages.info(request, _('Your event is not empty, you need to set it up manually.'))
            return redirect(reverse('control:event.index', kwargs={
                'organizer': request.event.organizer.slug,
                'event': request.event.slug
            }))
        return super().dispatch(request, *args, **kwargs)

    def get_form_kwargs(self):
        kwargs = super().get_form_kwargs()
        kwargs['event'] = self.request.event
        return kwargs

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data()
        ctx['formset'] = self.formset
        return ctx

    def get_initial(self):
        return {
            'waiting_list_enabled': True,
            'ticket_download': True,
            'contact_mail': self.request.event.settings.contact_mail,
            'imprint_url': self.request.event.settings.imprint_url,
        }

    def post(self, request, *args, **kwargs):
        form = self.get_form()
        if form.is_valid() and self.formset.is_valid():
            return self.form_valid(form)
        else:
            messages.error(self.request, _('We could not save your changes. See below for details.'))
            return self.form_invalid(form)

    @transaction.atomic
    def form_valid(self, form):
        plugins_active = self.request.event.get_plugins()
        if form.cleaned_data['ticket_download']:
            if 'pretix.plugins.ticketoutputpdf' not in plugins_active:
                self.request.event.log_action('pretix.event.plugins.enabled', user=self.request.user,
                                              data={'plugin': 'pretix.plugins.ticketoutputpdf'})
                plugins_active.append('pretix.plugins.ticketoutputpdf')

            self.request.event.settings.ticket_download = True
            self.request.event.settings.ticketoutput_pdf__enabled = True

            try:
                import pretix_passbook  # noqa
            except ImportError:
                pass
            else:
                if 'pretix_passbook' not in plugins_active:
                    self.request.event.log_action('pretix.event.plugins.enabled', user=self.request.user,
                                                  data={'plugin': 'pretix_passbook'})
                    plugins_active.append('pretix_passbook')
                self.request.event.settings.ticketoutput_passbook__enabled = True

        if form.cleaned_data['payment_banktransfer__enabled']:
            if 'pretix.plugins.banktransfer' not in plugins_active:
                self.request.event.log_action('pretix.event.plugins.enabled', user=self.request.user,
                                              data={'plugin': 'pretix.plugins.banktransfer'})
                plugins_active.append('pretix.plugins.banktransfer')
            self.request.event.settings.payment_banktransfer__enabled = True
            for f in ('bank_details', 'bank_details_type', 'bank_details_sepa_name', 'bank_details_sepa_iban',
                      'bank_details_sepa_bic', 'bank_details_sepa_bank'):
                self.request.event.settings.set(
                    'payment_banktransfer_%s' % f,
                    form.cleaned_data['payment_banktransfer_%s' % f]
                )

        if form.cleaned_data.get('payment_stripe__enabled', None):
            if 'pretix.plugins.stripe' not in plugins_active:
                self.request.event.log_action('pretix.event.plugins.enabled', user=self.request.user,
                                              data={'plugin': 'pretix.plugins.stripe'})
                plugins_active.append('pretix.plugins.stripe')

        self.request.event.settings.show_quota_left = form.cleaned_data['show_quota_left']
        self.request.event.settings.waiting_list_enabled = form.cleaned_data['waiting_list_enabled']
        self.request.event.settings.attendee_names_required = form.cleaned_data['attendee_names_required']
        self.request.event.settings.contact_mail = form.cleaned_data['contact_mail']
        self.request.event.settings.imprint_url = form.cleaned_data['imprint_url']
        self.request.event.log_action('pretix.event.settings', user=self.request.user, data={
            k: self.request.event.settings.get(k) for k in form.changed_data
        })

        items = []
        category = None
        tax_rule = self.request.event.tax_rules.first()
        if any(f not in self.formset.deleted_forms for f in self.formset):
            category = self.request.event.categories.create(
                name=LazyI18nString.from_gettext(gettext('Tickets'))
            )
            category.log_action('pretix.event.category.added', data={'name': gettext('Tickets')},
                                user=self.request.user)

        subevent = self.request.event.subevents.first()
        for i, f in enumerate(self.formset):
            if f in self.formset.deleted_forms or not f.has_changed():
                continue

            item = self.request.event.items.create(
                name=f.cleaned_data['name'],
                category=category,
                active=True,
                default_price=f.cleaned_data['default_price'] or 0,
                tax_rule=tax_rule,
                admission=True,
                position=i,
                sales_channels=[k for k in get_all_sales_channels().keys()]
            )
            item.log_action('pretix.event.item.added', user=self.request.user, data=dict(f.cleaned_data))
            if f.cleaned_data['quota'] or not form.cleaned_data['total_quota']:
                quota = self.request.event.quotas.create(
                    name=str(f.cleaned_data['name']),
                    subevent=subevent,
                    size=f.cleaned_data['quota'],
                )
                quota.log_action('pretix.event.quota.added', user=self.request.user, data=dict(f.cleaned_data))
                quota.items.add(item)
            items.append(item)

        if form.cleaned_data['total_quota']:
            quota = self.request.event.quotas.create(
                name=gettext('Tickets'),
                size=form.cleaned_data['total_quota'],
                subevent=subevent,
            )
            quota.log_action('pretix.event.quota.added', user=self.request.user, data={
                'name': gettext('Tickets'),
                'size': quota.size
            })
            quota.items.add(*items)

        self.request.event.plugins = ",".join(plugins_active)
        self.request.event.save()
        messages.success(self.request, _('Your changes have been saved. You can now go on with looking at the details '
                                         'or take your event live to start selling!'))

        if form.cleaned_data.get('payment_stripe__enabled', False):
            self.request.session['payment_stripe_oauth_enable'] = True
            return redirect(StripeSettingsHolder(self.request.event).get_connect_url(self.request))

        return redirect(reverse('control:event.index', kwargs={
            'organizer': self.request.event.organizer.slug,
            'event': self.request.event.slug,
        }))

    @cached_property
    def formset(self):
        return QuickSetupProductFormSet(
            data=self.request.POST if self.request.method == "POST" else None,
            event=self.request.event,
            initial=[
                {
                    'name': LazyI18nString.from_gettext(gettext('Regular ticket')),
                    'default_price': Decimal('35.00'),
                    'quota': 100,
                },
                {
                    'name': LazyI18nString.from_gettext(gettext('Reduced ticket')),
                    'default_price': Decimal('29.00'),
                    'quota': 50,
                },
            ] if self.request.method != "POST" else []
        )
