import copy
from datetime import datetime, timedelta

from dateutil.rrule import DAILY, MONTHLY, WEEKLY, YEARLY, rrule, rruleset
from django.contrib import messages
from django.core.files import File
from django.db import connections, transaction
from django.db.models import F, IntegerField, OuterRef, Prefetch, Subquery, Sum
from django.db.models.functions import Coalesce
from django.forms import inlineformset_factory
from django.http import Http404, HttpResponseRedirect
from django.shortcuts import redirect, render
from django.urls import reverse
from django.utils.functional import cached_property
from django.utils.timezone import make_aware
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django.views import View
from django.views.generic import CreateView, DeleteView, ListView, UpdateView

from pretix.base.models import CartPosition, LogEntry
from pretix.base.models.checkin import CheckinList
from pretix.base.models.event import SubEvent, SubEventMetaValue
from pretix.base.models.items import (
    ItemVariation, Quota, SubEventItem, SubEventItemVariation,
)
from pretix.base.reldate import RelativeDate, RelativeDateWrapper
from pretix.base.services.quotas import QuotaAvailability
from pretix.control.forms.checkin import SimpleCheckinListForm
from pretix.control.forms.filter import SubEventFilterForm
from pretix.control.forms.item import QuotaForm
from pretix.control.forms.subevents import (
    CheckinListFormSet, QuotaFormSet, RRuleFormSet, SubEventBulkForm,
    SubEventForm, SubEventItemForm, SubEventItemVariationForm,
    SubEventMetaValueForm, TimeFormSet,
)
from pretix.control.permissions import EventPermissionRequiredMixin
from pretix.control.signals import subevent_forms
from pretix.control.views import PaginationMixin
from pretix.control.views.event import MetaDataEditorMixin
from pretix.helpers.models import modelcopy


class SubEventList(EventPermissionRequiredMixin, PaginationMixin, ListView):
    model = SubEvent
    context_object_name = 'subevents'
    template_name = 'pretixcontrol/subevents/index.html'
    permission = 'can_change_settings'

    def get_queryset(self):
        sum_tickets_paid = Quota.objects.filter(
            subevent=OuterRef('pk')
        ).order_by().values('subevent').annotate(
            s=Sum('cached_availability_paid_orders')
        ).values(
            's'
        )

        qs = self.request.event.subevents.annotate(
            sum_tickets_paid=Subquery(sum_tickets_paid, output_field=IntegerField())
        ).prefetch_related(
            Prefetch('quotas',
                     queryset=Quota.objects.annotate(s=Coalesce(F('size'), 0)).order_by('-s'),
                     to_attr='first_quotas')
        )
        if self.filter_form.is_valid():
            qs = self.filter_form.filter_qs(qs)
        return qs

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

        quotas = []
        for s in ctx['subevents']:
            s.first_quotas = s.first_quotas[:4]
            quotas += list(s.first_quotas)

        qa = QuotaAvailability(early_out=False)
        for q in quotas:
            if q.cached_availability_time is None or q.cached_availability_paid_orders is None:
                qa.queue(q)
        qa.compute()

        for q in quotas:
            q.cached_avail = (
                qa.results[q] if q in qa.results
                else (q.cached_availability_state, q.cached_availability_number)
            )
            if q.size is not None:
                q.percent_paid = min(
                    100,
                    round(q.cached_availability_paid_orders / q.size * 100) if q.size > 0 else 100
                )
        return ctx

    @cached_property
    def filter_form(self):
        return SubEventFilterForm(data=self.request.GET)


class SubEventDelete(EventPermissionRequiredMixin, DeleteView):
    model = SubEvent
    template_name = 'pretixcontrol/subevents/delete.html'
    permission = 'can_change_settings'
    context_object_name = 'subevents'

    def get_object(self, queryset=None) -> SubEvent:
        try:
            return self.request.event.subevents.get(
                id=self.kwargs['subevent']
            )
        except SubEvent.DoesNotExist:
            raise Http404(pgettext_lazy("subevent", "The requested date does not exist."))

    def get(self, request, *args, **kwargs):
        if not self.get_object().allow_delete():
            messages.error(request, pgettext_lazy('subevent', 'A date can not be deleted if orders already have been '
                                                              'placed.'))
            return HttpResponseRedirect(self.get_success_url())
        return super().get(request, *args, **kwargs)

    @transaction.atomic
    def delete(self, request, *args, **kwargs):
        self.object = self.get_object()
        success_url = self.get_success_url()

        if not self.object.allow_delete():
            messages.error(request, pgettext_lazy('subevent', 'A date can not be deleted if orders already have been '
                                                              'placed.'))
            return HttpResponseRedirect(self.get_success_url())
        else:
            self.object.log_action('pretix.subevent.deleted', user=self.request.user)
            CartPosition.objects.filter(addon_to__subevent=self.object).delete()
            self.object.cartposition_set.all().delete()
            self.object.delete()
            messages.success(request, pgettext_lazy('subevent', 'The selected date has been deleted.'))
        return HttpResponseRedirect(success_url)

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


class SubEventEditorMixin(MetaDataEditorMixin):
    meta_form = SubEventMetaValueForm
    meta_model = SubEventMetaValue

    @cached_property
    def plugin_forms(self):
        forms = []
        for rec, resp in subevent_forms.send(sender=self.request.event, subevent=self.object, request=self.request):
            if isinstance(resp, (list, tuple)):
                forms.extend(resp)
            else:
                forms.append(resp)
        return forms

    def _make_meta_form(self, p, val_instances):
        if not hasattr(self, '_default_meta'):
            self._default_meta = self.request.event.meta_data

        return self.meta_form(
            prefix='prop-{}'.format(p.pk),
            property=p,
            default=self._default_meta.get(p.name, ''),
            instance=val_instances.get(p.pk, self.meta_model(property=p, subevent=self.object)),
            data=(self.request.POST if self.request.method == "POST" else None)
        )

    @cached_property
    def cl_formset(self):
        extra = 0
        kwargs = {}

        if self.copy_from and self.request.method != "POST":
            kwargs['initial'] = [
                {
                    'name': cl.name,
                    'all_products': cl.all_products,
                    'limit_products': cl.limit_products.all(),
                    'include_pending': cl.include_pending,
                } for cl in self.copy_from.checkinlist_set.prefetch_related('limit_products')
            ]
            extra = len(kwargs['initial'])
        elif not self.object and self.request.method != "POST":
            kwargs['initial'] = [
                {
                    'name': '',
                    'all_products': True,
                    'include_pending': False,
                }
            ]
            extra = 0

        formsetclass = inlineformset_factory(
            SubEvent, CheckinList,
            form=SimpleCheckinListForm, formset=CheckinListFormSet,
            can_order=False, can_delete=True, extra=extra,
        )
        if self.object:
            kwargs['queryset'] = self.object.checkinlist_set.prefetch_related('limit_products')

        return formsetclass(self.request.POST if self.request.method == "POST" else None,
                            instance=self.object,
                            event=self.request.event, **kwargs)

    @cached_property
    def formset(self):
        extra = 0
        kwargs = {}

        if self.copy_from and self.request.method != "POST":
            kwargs['initial'] = [
                {
                    'size': q.size,
                    'name': q.name,
                    'release_after_exit': q.release_after_exit,
                    'itemvars': [str(i.pk) for i in q.items.all()] + [
                        '{}-{}'.format(v.item_id, v.pk) for v in q.variations.all()
                    ]
                } for q in self.copy_from.quotas.prefetch_related('items', 'variations')
            ]
            extra = len(kwargs['initial'])

        formsetclass = inlineformset_factory(
            SubEvent, Quota,
            form=QuotaForm, formset=QuotaFormSet, min_num=1, validate_min=True,
            can_order=False, can_delete=True, extra=extra,
        )
        if self.object:
            kwargs['queryset'] = self.object.quotas.prefetch_related('items', 'variations')

        return formsetclass(
            self.request.POST if self.request.method == "POST" else None,
            instance=self.object,
            event=self.request.event, **kwargs
        )

    def save_cl_formset(self, obj):
        for form in self.cl_formset.initial_forms:
            if form in self.cl_formset.deleted_forms:
                if not form.instance.pk:
                    continue
                form.instance.checkins.all().delete()
                form.instance.log_action(action='pretix.event.checkinlist.deleted', user=self.request.user)
                form.instance.delete()
                form.instance.pk = None
            elif form.has_changed():
                form.instance.subevent = obj
                form.instance.event = obj.event
                form.save()
                change_data = {k: form.cleaned_data.get(k) for k in form.changed_data}
                change_data['id'] = form.instance.pk
                form.instance.log_action(
                    'pretix.event.checkinlist.changed', user=self.request.user, data={
                        k: form.cleaned_data.get(k) for k in form.changed_data
                    }
                )

        for form in self.cl_formset.extra_forms:
            if not form.has_changed():
                continue
            if self.formset._should_delete_form(form):
                continue
            form.instance.subevent = obj
            form.instance.event = obj.event
            form.save()
            change_data = {k: form.cleaned_data.get(k) for k in form.changed_data}
            change_data['id'] = form.instance.pk
            form.instance.log_action(action='pretix.event.checkinlist.added', user=self.request.user, data=change_data)

    def save_formset(self, obj):
        for form in self.formset.initial_forms:
            if form in self.formset.deleted_forms:
                if not form.instance.pk:
                    continue
                form.instance.log_action(action='pretix.event.quota.deleted', user=self.request.user)
                obj.log_action('pretix.subevent.quota.deleted', user=self.request.user, data={
                    'id': form.instance.pk
                })
                form.instance.delete()
                form.instance.pk = None
            elif form.has_changed():
                form.instance.question = obj
                form.save()
                change_data = {k: form.cleaned_data.get(k) for k in form.changed_data}
                change_data['id'] = form.instance.pk
                obj.log_action(
                    'pretix.subevent.quota.changed', user=self.request.user, data={
                        k: form.cleaned_data.get(k) for k in form.changed_data
                    }
                )
                form.instance.log_action(
                    'pretix.event.quota.changed', user=self.request.user, data={
                        k: form.cleaned_data.get(k) for k in form.changed_data
                    }
                )

        for form in self.formset.extra_forms:
            if not form.has_changed():
                continue
            if self.formset._should_delete_form(form):
                continue
            form.instance.subevent = obj
            form.instance.event = obj.event
            form.save()
            change_data = {k: form.cleaned_data.get(k) for k in form.changed_data}
            change_data['id'] = form.instance.pk
            form.instance.log_action(action='pretix.event.quota.added', user=self.request.user, data=change_data)
            obj.log_action('pretix.subevent.quota.added', user=self.request.user, data=change_data)

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

    @cached_property
    def copy_from(self):
        if self.request.GET.get("copy_from") and not getattr(self, 'object', None):
            try:
                return self.request.event.subevents.get(pk=self.request.GET.get("copy_from"))
            except SubEvent.DoesNotExist:
                pass

    @cached_property
    def itemvar_forms(self):
        se_item_instances = {
            sei.item_id: sei for sei in SubEventItem.objects.filter(subevent=self.object)
        }
        se_var_instances = {
            sei.variation_id: sei for sei in SubEventItemVariation.objects.filter(subevent=self.object)
        }

        if self.copy_from:
            se_item_instances = {
                sei.item_id: SubEventItem(item=sei.item, price=sei.price, disabled=sei.disabled)
                for sei in SubEventItem.objects.filter(subevent=self.copy_from).select_related('item')
            }
            se_var_instances = {
                sei.variation_id: SubEventItemVariation(variation=sei.variation, price=sei.price, disabled=sei.disabled)
                for sei in SubEventItemVariation.objects.filter(subevent=self.copy_from).select_related('variation')
            }

        formlist = []
        for i in self.request.event.items.filter(active=True).prefetch_related('variations'):
            if i.has_variations:
                for v in i.variations.all():
                    inst = se_var_instances.get(v.pk) or SubEventItemVariation(subevent=self.object, variation=v)
                    formlist.append(SubEventItemVariationForm(
                        prefix='itemvar-{}'.format(v.pk),
                        item=i, variation=v,
                        instance=inst,
                        data=(self.request.POST if self.request.method == "POST" else None)
                    ))
            else:
                inst = se_item_instances.get(i.pk) or SubEventItem(subevent=self.object, item=i)
                formlist.append(SubEventItemForm(
                    prefix='item-{}'.format(i.pk),
                    item=i,
                    instance=inst,
                    data=(self.request.POST if self.request.method == "POST" else None)
                ))
        return formlist

    def is_valid(self, form):
        return form.is_valid() and all([f.is_valid() for f in self.itemvar_forms]) and self.formset.is_valid() and (
            all([f.is_valid() for f in self.meta_forms])
        ) and self.cl_formset.is_valid() and all(f.is_valid() for f in self.plugin_forms)


class SubEventUpdate(EventPermissionRequiredMixin, SubEventEditorMixin, UpdateView):
    model = SubEvent
    template_name = 'pretixcontrol/subevents/detail.html'
    permission = 'can_change_settings'
    context_object_name = 'subevent'
    form_class = SubEventForm

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

    def get_object(self, queryset=None) -> SubEvent:
        try:
            return self.request.event.subevents.get(
                id=self.kwargs['subevent']
            )
        except SubEvent.DoesNotExist:
            raise Http404(pgettext_lazy("subevent", "The requested date does not exist."))

    @transaction.atomic
    def form_valid(self, form):
        self.save_formset(self.object)
        self.save_cl_formset(self.object)
        self.save_meta()

        for f in self.itemvar_forms:
            f.save()
            # TODO: LogEntry?

        messages.success(self.request, _('Your changes have been saved.'))
        if form.has_changed() or any(f.has_changed() for f in self.plugin_forms):
            data = {
                k: form.cleaned_data.get(k) for k in form.changed_data
            }
            for f in self.plugin_forms:
                data.update({
                    k: (f.cleaned_data.get(k).name
                        if isinstance(f.cleaned_data.get(k), File)
                        else f.cleaned_data.get(k))
                    for k in f.changed_data
                })
            self.object.log_action(
                'pretix.subevent.changed', user=self.request.user, data=data
            )
        for f in self.plugin_forms:
            f.subevent = self.object
            f.save()
        return super().form_valid(form)

    def get_success_url(self) -> str:
        return reverse('control:event.subevents', kwargs={
            'organizer': self.request.event.organizer.slug,
            'event': self.request.event.slug,
        }) + ('?' + self.request.GET.get('returnto') if 'returnto' in self.request.GET else '')

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


class SubEventCreate(SubEventEditorMixin, EventPermissionRequiredMixin, CreateView):
    model = SubEvent
    template_name = 'pretixcontrol/subevents/detail.html'
    permission = 'can_change_settings'
    context_object_name = 'subevent'
    form_class = SubEventForm

    def post(self, request, *args, **kwargs):
        self.object = SubEvent(event=self.request.event)
        form = self.get_form()
        if self.is_valid(form):
            return self.form_valid(form)
        return self.form_invalid(form)

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

    def get_form_kwargs(self):
        kwargs = super().get_form_kwargs()
        kwargs['event'] = self.request.event
        initial = kwargs.get('initial', {})
        if self.copy_from:
            i = modelcopy(self.copy_from)
            i.pk = None
            kwargs['instance'] = i
        else:
            kwargs['instance'] = SubEvent(event=self.request.event)
            initial['location'] = self.request.event.location
            initial['geo_lat'] = self.request.event.geo_lat
            initial['geo_lon'] = self.request.event.geo_lon
        kwargs['initial'] = initial
        return kwargs

    @transaction.atomic
    def form_valid(self, form):
        form.instance.event = self.request.event
        messages.success(self.request, pgettext_lazy('subevent', 'The new date has been created.'))
        ret = super().form_valid(form)
        self.object = form.instance

        data = dict(form.cleaned_data)
        for f in self.plugin_forms:
            data.update({
                k: (f.cleaned_data.get(k).name
                    if isinstance(f.cleaned_data.get(k), File)
                    else f.cleaned_data.get(k))
                for k in f.cleaned_data
            })
        form.instance.log_action('pretix.subevent.added', data=dict(data), user=self.request.user)

        self.save_formset(form.instance)
        self.save_cl_formset(form.instance)
        for f in self.itemvar_forms:
            f.instance.subevent = form.instance
            f.save()
        for f in self.meta_forms:
            f.instance.subevent = form.instance
        self.save_meta()
        for f in self.plugin_forms:
            f.subevent = form.instance
            f.save()
        return ret

    @cached_property
    def meta_forms(self):
        def clone(o):
            o = copy.copy(o)
            o.pk = None
            return o

        if self.copy_from:
            val_instances = {
                v.property_id: clone(v) for v in self.copy_from.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


class SubEventBulkAction(EventPermissionRequiredMixin, View):
    permission = 'can_change_settings'

    @cached_property
    def objects(self):
        return self.request.event.subevents.filter(
            id__in=self.request.POST.getlist('subevent')
        )

    @transaction.atomic
    def post(self, request, *args, **kwargs):
        if request.POST.get('action') == 'disable':
            for obj in self.objects:
                obj.log_action(
                    'pretix.subevent.changed', user=self.request.user, data={
                        'active': False
                    }
                )
                obj.active = False
                obj.save(update_fields=['active'])
            messages.success(request, pgettext_lazy('subevent', 'The selected dates have been disabled.'))
        elif request.POST.get('action') == 'enable':
            for obj in self.objects:
                obj.log_action(
                    'pretix.subevent.changed', user=self.request.user, data={
                        'active': True
                    }
                )
                obj.active = True
                obj.save(update_fields=['active'])
            messages.success(request, pgettext_lazy('subevent', 'The selected dates have been enabled.'))
        elif request.POST.get('action') == 'delete':
            return render(request, 'pretixcontrol/subevents/delete_bulk.html', {
                'allowed': self.objects.filter(orderposition__isnull=True),
                'forbidden': self.objects.filter(orderposition__isnull=False),
            })
        elif request.POST.get('action') == 'delete_confirm':
            for obj in self.objects:
                if obj.allow_delete():
                    CartPosition.objects.filter(addon_to__subevent=obj).delete()
                    obj.cartposition_set.all().delete()
                    obj.log_action('pretix.subevent.deleted', user=self.request.user)
                    obj.delete()
                else:
                    obj.log_action(
                        'pretix.subevent.changed', user=self.request.user, data={
                            'active': False
                        }
                    )
                    obj.active = False
                    obj.save(update_fields=['active'])
            messages.success(request, pgettext_lazy('subevent', 'The selected dates have been deleted or disabled.'))
        return redirect(self.get_success_url())

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


class SubEventBulkCreate(SubEventEditorMixin, EventPermissionRequiredMixin, CreateView):
    model = SubEvent
    template_name = 'pretixcontrol/subevents/bulk.html'
    permission = 'can_change_settings'
    context_object_name = 'subevent'
    form_class = SubEventBulkForm

    def is_valid(self, form):
        return self.rrule_formset.is_valid() and self.time_formset.is_valid() and super().is_valid(form)

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

    @cached_property
    def rrule_formset(self):
        return RRuleFormSet(
            data=self.request.POST if self.request.method == "POST" else None,
            prefix='rruleformset'
        )

    @cached_property
    def time_formset(self):
        return TimeFormSet(
            data=self.request.POST if self.request.method == "POST" else None,
            prefix='timeformset'
        )

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

    @cached_property
    def meta_forms(self):
        def clone(o):
            o = copy.copy(o)
            o.pk = None
            return o

        if self.copy_from:
            val_instances = {
                v.property_id: clone(v) for v in self.copy_from.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 get_form_kwargs(self):
        kwargs = super().get_form_kwargs()
        initial = {
            'active': True,
        }
        kwargs['event'] = self.request.event
        tz = self.request.event.timezone
        if self.copy_from:
            i = copy.copy(self.copy_from)
            i.pk = None
            kwargs['instance'] = i
            initial['time_from'] = i.date_from.astimezone(tz).time()
            initial['time_to'] = i.date_to.astimezone(tz).time() if i.date_to else None
            initial['time_admission'] = i.date_admission.astimezone(tz).time() if i.date_admission else None
            initial['rel_presale_start'] = RelativeDateWrapper(RelativeDate(
                days_before=(i.date_from.astimezone(tz).date() - i.presale_start.astimezone(tz).date()).days,
                base_date_name='date_from',
                time=i.presale_start.astimezone(tz).time(),
                minutes_before=None
            )) if i.presale_start else None
            initial['rel_presale_end'] = RelativeDateWrapper(RelativeDate(
                days_before=(i.date_from.astimezone(tz).date() - i.presale_end.astimezone(tz).date()).days,
                base_date_name='date_from',
                time=i.presale_end.astimezone(tz).time(),
                minutes_before=None
            )) if i.presale_end else None
        else:
            kwargs['instance'] = SubEvent(event=self.request.event)
            initial['location'] = self.request.event.location
            initial['geo_lat'] = self.request.event.geo_lat
            initial['geo_lon'] = self.request.event.geo_lon
        kwargs['initial'] = initial
        return kwargs

    def get_times(self):
        times = []
        for f in self.time_formset:
            if f in self.time_formset.deleted_forms or not f.cleaned_data.get('time_from'):
                continue
            times.append(f.cleaned_data)
        return times

    def get_rrule_set(self):
        s = rruleset()
        for f in self.rrule_formset:
            if f in self.rrule_formset.deleted_forms:
                continue

            rule_kwargs = {}
            rule_kwargs['dtstart'] = f.cleaned_data['dtstart']
            rule_kwargs['interval'] = f.cleaned_data['interval']

            if f.cleaned_data['freq'] == 'yearly':
                freq = YEARLY
                if f.cleaned_data['yearly_same'] == "off":
                    rule_kwargs['bysetpos'] = int(f.cleaned_data['yearly_bysetpos'])
                    rule_kwargs['byweekday'] = f.parse_weekdays(f.cleaned_data['yearly_byweekday'])
                    rule_kwargs['bymonth'] = int(f.cleaned_data['yearly_bymonth'])

            elif f.cleaned_data['freq'] == 'monthly':
                freq = MONTHLY

                if f.cleaned_data['monthly_same'] == "off":
                    rule_kwargs['bysetpos'] = int(f.cleaned_data['monthly_bysetpos'])
                    rule_kwargs['byweekday'] = f.parse_weekdays(f.cleaned_data['monthly_byweekday'])
            elif f.cleaned_data['freq'] == 'weekly':
                freq = WEEKLY

                if f.cleaned_data['weekly_byweekday']:
                    rule_kwargs['byweekday'] = [f.parse_weekdays(a) for a in f.cleaned_data['weekly_byweekday']]

            elif f.cleaned_data['freq'] == 'daily':
                freq = DAILY

            if f.cleaned_data['end'] == 'count':
                rule_kwargs['count'] = f.cleaned_data['count']
            else:
                rule_kwargs['until'] = f.cleaned_data['until']

            if f.cleaned_data['exclude']:
                s.exrule(rrule(freq, **rule_kwargs))
            else:
                s.rrule(rrule(freq, **rule_kwargs))

        return s

    @transaction.atomic
    def form_valid(self, form):
        tz = self.request.event.timezone
        subevents = []
        for rdate in self.get_rrule_set():
            for t in self.get_times():
                se = copy.copy(form.instance)

                se.date_from = make_aware(datetime.combine(rdate, t['time_from']), tz)

                if t.get('time_to'):
                    se.date_to = (
                        make_aware(datetime.combine(rdate, t['time_to']), tz)
                        if t.get('time_to') > t.get('time_from')
                        else make_aware(datetime.combine(rdate + timedelta(days=1), t['time_to']), tz)
                    )
                else:
                    se.date_to = None
                se.date_admission = (
                    make_aware(datetime.combine(rdate, t['time_admission']), tz)
                    if t.get('time_admission')
                    else None
                )
                se.presale_start = (
                    form.cleaned_data['rel_presale_start'].datetime(se)
                    if form.cleaned_data.get('rel_presale_start')
                    else None
                )
                se.presale_end = (
                    form.cleaned_data['rel_presale_end'].datetime(se)
                    if form.cleaned_data.get('rel_presale_end')
                    else None
                )
                se.save(clear_cache=False)
                subevents.append(se)

        data = dict(form.cleaned_data)
        for f in self.plugin_forms:
            data.update({
                k: (f.cleaned_data.get(k).name
                    if isinstance(f.cleaned_data.get(k), File)
                    else f.cleaned_data.get(k))
                for k in f.cleaned_data
            })
        log_entries = []
        for se in subevents:
            log_entries.append(se.log_action('pretix.subevent.added', data=data, user=self.request.user, save=False))

        to_save = []
        for f in self.meta_forms:
            if f.cleaned_data.get('value'):
                for se in subevents:
                    i = copy.copy(f.instance)
                    i.pk = None
                    i.subevent = se
                    to_save.append(i)
        SubEventMetaValue.objects.bulk_create(to_save)

        to_save_items = []
        to_save_variations = []
        for f in self.itemvar_forms:
            for se in subevents:
                i = copy.copy(f.instance)
                i.pk = None
                i.subevent = se
                if isinstance(i, SubEventItem):
                    to_save_items.append(i)
                else:
                    to_save_variations.append(i)
        SubEventItem.objects.bulk_create(to_save_items)
        SubEventItemVariation.objects.bulk_create(to_save_variations)

        to_save_items = []
        to_save_variations = []
        for f in self.formset.forms:
            if self.formset._should_delete_form(f) or not f.has_changed():
                continue

            change_data = {k: f.cleaned_data.get(k) for k in f.changed_data}
            for se in subevents:
                i = copy.copy(f.instance)
                i.pk = None
                i.subevent = se
                i.event = se.event
                i.save(clear_cache=False)
                selected_items = set(list(self.request.event.items.filter(id__in=[
                    i.split('-')[0] for i in f.cleaned_data.get('itemvars', [])
                ])))
                selected_variations = list(ItemVariation.objects.filter(item__event=self.request.event, id__in=[
                    i.split('-')[1] for i in f.cleaned_data.get('itemvars', []) if '-' in i
                ]))
                for _i in selected_items:
                    to_save_items.append(Quota.items.through(quota_id=i.pk, item_id=_i.pk))
                for _i in selected_variations:
                    to_save_variations.append(Quota.variations.through(quota_id=i.pk, itemvariation_id=_i.pk))

                change_data['id'] = i.pk
                log_entries.append(
                    i.log_action(action='pretix.event.quota.added', user=self.request.user,
                                 data=change_data, save=False)
                )
                log_entries.append(
                    se.log_action('pretix.subevent.quota.added', user=self.request.user, data=change_data, save=False)
                )
        Quota.items.through.objects.bulk_create(to_save_items)
        Quota.variations.through.objects.bulk_create(to_save_variations)

        to_save_products = []
        for f in self.cl_formset.forms:
            if self.cl_formset._should_delete_form(f) or not f.has_changed():
                continue
            change_data = {k: f.cleaned_data.get(k) for k in f.changed_data}
            for se in subevents:
                i = copy.copy(f.instance)
                i.subevent = se
                i.event = se.event
                i.save()
                for _i in f.cleaned_data.get('limit_products', []):
                    to_save_products.append(CheckinList.limit_products.through(checkinlist_id=i.pk, item_id=_i.pk))

                change_data['id'] = i.pk
                log_entries.append(
                    i.log_action(action='pretix.event.checkinlist.added', user=self.request.user, data=change_data,
                                 save=False)
                )
        CheckinList.limit_products.through.objects.bulk_create(to_save_products)

        for f in self.plugin_forms:
            f.is_valid()
            for se in subevents:
                f.subevent = se
                f.save()

        if connections['default'].features.can_return_rows_from_bulk_insert:
            LogEntry.objects.bulk_create(log_entries)
            LogEntry.bulk_postprocess(log_entries)
        else:
            for le in log_entries:
                le.save()
            LogEntry.bulk_postprocess(log_entries)

        self.request.event.cache.clear()
        messages.success(self.request, pgettext_lazy('subevent', '{} new dates have been created.').format(len(subevents)))
        return redirect(reverse('control:event.subevents', kwargs={
            'organizer': self.request.event.organizer.slug,
            'event': self.request.event.slug,
        }))

    def post(self, request, *args, **kwargs):
        form = self.get_form()
        self.object = SubEvent(event=self.request.event)

        if self.is_valid(form):
            return self.form_valid(form)

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