from datetime import datetime, timedelta
from urllib.parse import urlencode

from django import forms
from django.forms import formset_factory
from django.forms.utils import ErrorDict
from django.urls import reverse
from django.utils.dates import MONTHS, WEEKDAYS
from django.utils.functional import cached_property
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from i18nfield.forms import I18nInlineFormSet

from pretix.base.forms import I18nModelForm
from pretix.base.forms.widgets import DatePickerWidget, TimePickerWidget
from pretix.base.models.event import SubEvent, SubEventMetaValue
from pretix.base.models.items import SubEventItem
from pretix.base.reldate import RelativeDateTimeField
from pretix.base.templatetags.money import money_filter
from pretix.control.forms import SplitDateTimeField, SplitDateTimePickerWidget
from pretix.helpers.money import change_decimal_field


class SubEventForm(I18nModelForm):
    def __init__(self, *args, **kwargs):
        self.event = kwargs['event']
        instance = kwargs.get('instance')
        if instance and not instance.pk:
            kwargs['initial'].setdefault('name', self.event.name)
            kwargs['initial'].setdefault('location', self.event.location)
            kwargs['initial'].setdefault('geo_lat', self.event.geo_lat)
            kwargs['initial'].setdefault('geo_lon', self.event.geo_lon)
        super().__init__(*args, **kwargs)
        self.fields['location'].widget.attrs['rows'] = '3'

    class Meta:
        model = SubEvent
        localized_fields = '__all__'
        fields = [
            'name',
            'active',
            'is_public',
            'date_from',
            'date_to',
            'date_admission',
            'presale_start',
            'presale_end',
            'location',
            'frontpage_text',
            'geo_lat',
            'geo_lon',
        ]
        field_classes = {
            'date_from': SplitDateTimeField,
            'date_to': SplitDateTimeField,
            'date_admission': SplitDateTimeField,
            'presale_start': SplitDateTimeField,
            'presale_end': SplitDateTimeField,
        }
        widgets = {
            'date_from': SplitDateTimePickerWidget(),
            'date_to': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_date_from_0'}),
            'date_admission': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_date_from_0'}),
            'presale_start': SplitDateTimePickerWidget(),
            'presale_end': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_presale_start_0'}),
        }


class SubEventBulkForm(SubEventForm):
    rel_presale_start = RelativeDateTimeField(
        label=_('Start of presale'),
        help_text=_('Optional. No products will be sold before this date.'),
        required=False,
        limit_choices=('date_from', 'date_to'),
    )
    rel_presale_end = RelativeDateTimeField(
        label=_('End of presale'),
        help_text=_('Optional. No products will be sold after this date. If you do not set this value, the presale '
                    'will end after the end date of your event.'),
        required=False,
        limit_choices=('date_from', 'date_to'),
    )

    def __init__(self, *args, **kwargs):
        self.event = kwargs['event']
        super().__init__(*args, **kwargs)
        self.fields['location'].widget.attrs['rows'] = '3'
        del self.fields['date_from']
        del self.fields['date_to']
        del self.fields['date_admission']


class NullBooleanSelect(forms.NullBooleanSelect):
    def __init__(self, attrs=None):
        choices = (
            ('unknown', _('Keep the current values')),
            ('true', _('Yes')),
            ('false', _('No')),
        )
        super(forms.NullBooleanSelect, self).__init__(attrs, choices)


class SubEventBulkEditForm(I18nModelForm):
    def __init__(self, *args, **kwargs):
        self.mixed_values = kwargs.pop('mixed_values')
        self.queryset = kwargs.pop('queryset')
        super().__init__(*args, **kwargs)
        self.fields['location'].widget.attrs['rows'] = '3'

        for k in ('name', 'location', 'frontpage_text'):
            # i18n fields
            if k in self.mixed_values:
                self.fields[k].widget.attrs['placeholder'] = '[{}]'.format(_('Selection contains various values'))
            else:
                self.fields[k].widget.attrs['placeholder'] = ''
            self.fields[k].one_required = False

        for k in ('geo_lat', 'geo_lon'):
            # scalar fields
            if k in self.mixed_values:
                self.fields[k].widget.attrs['placeholder'] = '[{}]'.format(_('Selection contains various values'))
            else:
                self.fields[k].widget.attrs['placeholder'] = ''
            self.fields[k].widget.is_required = False
            self.fields[k].required = False

        for k in ('date_from', 'date_to', 'date_admission', 'presale_start', 'presale_end'):
            self.fields[k + '_day'] = forms.DateField(
                label=self._meta.model._meta.get_field(k).verbose_name,
                help_text=self._meta.model._meta.get_field(k).help_text,
                widget=DatePickerWidget(),
                required=False,
            )
            self.fields[k + '_time'] = forms.TimeField(
                label=self._meta.model._meta.get_field(k).verbose_name,
                help_text=self._meta.model._meta.get_field(k).help_text,
                widget=TimePickerWidget(),
                required=False,
            )

    class Meta:
        model = SubEvent
        localized_fields = '__all__'
        fields = [
            'name',
            'location',
            'frontpage_text',
            'geo_lat',
            'geo_lon',
            'is_public',
            'active',
        ]
        field_classes = {
        }
        widgets = {
        }

    def save(self, commit=True):
        objs = list(self.queryset)
        fields = set()

        check_map = {
            'geo_lat': '__geo',
            'geo_lon': '__geo',
        }
        for k in self.fields:
            cb_val = self.prefix + check_map.get(k, k)
            if cb_val not in self.data.getlist('_bulk'):
                continue

            if k.endswith('_day'):
                for obj in objs:
                    oldval = getattr(obj, k.replace('_day', ''))
                    cval = self.cleaned_data[k]
                    if cval is None:
                        newval = None
                        if not self._meta.model._meta.get_field(k.replace('_day', '')).null:
                            continue
                    elif oldval:
                        oldval = oldval.astimezone(self.event.timezone)
                        newval = oldval.replace(
                            year=cval.year,
                            month=cval.month,
                            day=cval.day,
                        )
                    else:
                        # If there is no previous date/time set, we'll just set to midnight
                        # If the user also selected a time, this will be overridden anyways
                        newval = datetime(
                            year=cval.year,
                            month=cval.month,
                            day=cval.day,
                            tzinfo=self.event.timezone
                        )
                    setattr(obj, k.replace('_day', ''), newval)
                fields.add(k.replace('_day', ''))
            elif k.endswith('_time'):
                for obj in objs:
                    # If there is no previous date/time set and only a time is changed not the
                    # date, we instead use the date of the event
                    oldval = getattr(obj, k.replace('_time', '')) or obj.date_from
                    cval = self.cleaned_data[k]
                    if cval is None:
                        continue
                    oldval = oldval.astimezone(self.event.timezone)
                    newval = oldval.replace(
                        hour=cval.hour,
                        minute=cval.minute,
                        second=cval.second,
                    )
                    setattr(obj, k.replace('_time', ''), newval)
                fields.add(k.replace('_time', ''))
            else:
                fields.add(k)
                for obj in objs:
                    setattr(obj, k, self.cleaned_data[k])

        if fields:
            SubEvent.objects.bulk_update(objs, fields, 200)

    def full_clean(self):
        if len(self.data) == 0:
            # form wasn't submitted
            self._errors = ErrorDict()
            return
        super().full_clean()


class SubEventItemOrVariationFormMixin:
    def __init__(self, *args, **kwargs):
        self.item = kwargs.pop('item')
        self.variation = kwargs.pop('variation', None)
        super().__init__(*args, **kwargs)
        change_decimal_field(self.fields['price'], self.item.event.currency)


class SubEventItemForm(SubEventItemOrVariationFormMixin, forms.ModelForm):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields['price'].widget.attrs['placeholder'] = money_filter(self.item.default_price, self.item.event.currency, hide_currency=True)
        self.fields['price'].label = str(self.item)

    class Meta:
        model = SubEventItem
        fields = ['price', 'disabled']
        widgets = {
            'price': forms.TextInput
        }


class SubEventItemVariationForm(SubEventItemOrVariationFormMixin, forms.ModelForm):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields['price'].widget.attrs['placeholder'] = money_filter(self.variation.price, self.item.event.currency, hide_currency=True)
        self.fields['price'].label = '{} – {}'.format(str(self.item), self.variation.value)

    class Meta:
        model = SubEventItem
        fields = ['price', 'disabled']
        widgets = {
            'price': forms.TextInput
        }


class QuotaFormSet(I18nInlineFormSet):

    def __init__(self, *args, **kwargs):
        self.event = kwargs.pop('event', None)
        self.locales = self.event.settings.get('locales')
        super().__init__(*args, **kwargs)

    @cached_property
    def items(self):
        return self.event.items.prefetch_related('variations').all()

    def _construct_form(self, i, **kwargs):
        kwargs['locales'] = self.locales
        kwargs['event'] = self.event
        kwargs['items'] = self.items
        kwargs['items'] = self.items
        return super()._construct_form(i, **kwargs)

    @property
    def empty_form(self):
        form = self.form(
            auto_id=self.auto_id,
            prefix=self.add_prefix('__prefix__'),
            empty_permitted=True,
            use_required_attribute=False,
            locales=self.locales,
            event=self.event,
            items=self.items
        )
        self.add_fields(form, None)
        return form


class SubEventMetaValueForm(forms.ModelForm):

    def __init__(self, *args, **kwargs):
        self.property = kwargs.pop('property')
        self.default = kwargs.pop('default', None)
        self.disabled = kwargs.pop('disabled', False)
        super().__init__(*args, **kwargs)
        if self.property.allowed_values:
            self.fields['value'] = forms.ChoiceField(
                label=self.property.name,
                choices=[
                    ('', _('Default ({value})').format(value=self.default or self.property.default) if self.default or self.property.default else ''),
                ] + [(a.strip(), a.strip()) for a in self.property.allowed_values.splitlines()],
            )
        else:
            self.fields['value'].label = self.property.name
            self.fields['value'].widget.attrs['placeholder'] = self.default or self.property.default
            self.fields['value'].widget.attrs['data-typeahead-url'] = (
                reverse('control:events.meta.typeahead') + '?' + urlencode({
                    'property': self.property.name,
                    'organizer': self.property.organizer.slug,
                })
            )
        self.fields['value'].required = False
        if self.disabled:
            self.fields['value'].widget.attrs['readonly'] = 'readonly'

    def clean_slug(self):
        if self.disabled:
            return self.instance.value if self.instance else None
        return self.cleaned_data['slug']

    class Meta:
        model = SubEventMetaValue
        fields = ['value']
        widgets = {
            'value': forms.TextInput
        }


class CheckinListFormSet(I18nInlineFormSet):

    def __init__(self, *args, **kwargs):
        self.event = kwargs.pop('event', None)
        self.locales = self.event.settings.get('locales')
        super().__init__(*args, **kwargs)

    @cached_property
    def items(self):
        return self.event.items.prefetch_related('variations').all()

    def _construct_form(self, i, **kwargs):
        kwargs['event'] = self.event
        return super()._construct_form(i, **kwargs)

    @property
    def empty_form(self):
        form = self.form(
            auto_id=self.auto_id,
            prefix=self.add_prefix('__prefix__'),
            empty_permitted=True,
            use_required_attribute=False,
            event=self.event,
        )
        self.add_fields(form, None)
        return form


class RRuleForm(forms.Form):
    # TODO: calendar.setfirstweekday
    exclude = forms.BooleanField(
        label=_('Exclude these dates instead of adding them.'),
        required=False
    )
    freq = forms.ChoiceField(
        choices=[
            ('yearly', _('year(s)')),
            ('monthly', _('month(s)')),
            ('weekly', _('week(s)')),
            ('daily', _('day(s)')),
        ],
        initial='weekly'
    )
    interval = forms.IntegerField(
        label=_('Interval'),
        initial=1
    )
    dtstart = forms.DateField(
        label=_('Start date'),
        widget=forms.DateInput(
            attrs={
                'class': 'datepickerfield',
                'required': 'required'
            }
        ),
        initial=lambda: now().date()
    )

    end = forms.ChoiceField(
        choices=[
            ('count', ''),
            ('until', ''),
        ],
        initial='count',
        widget=forms.RadioSelect
    )
    count = forms.IntegerField(
        label=_('Number of repetitions'),
        initial=10
    )
    until = forms.DateField(
        widget=forms.DateInput(
            attrs={
                'class': 'datepickerfield',
                'required': 'required'
            }
        ),
        label=_('Last date'),
        required=True,
        initial=lambda: now() + timedelta(days=365)
    )

    yearly_bysetpos = forms.ChoiceField(
        choices=[
            ('1', pgettext_lazy('rrule', 'first')),
            ('2', pgettext_lazy('rrule', 'second')),
            ('3', pgettext_lazy('rrule', 'third')),
            ('-1', pgettext_lazy('rrule', 'last')),
        ],
        required=False
    )
    yearly_same = forms.ChoiceField(
        choices=[
            ('on', ''),
            ('off', ''),
        ],
        initial='on',
        widget=forms.RadioSelect
    )
    yearly_byweekday = forms.ChoiceField(
        choices=[
            ('MO', WEEKDAYS[0]),
            ('TU', WEEKDAYS[1]),
            ('WE', WEEKDAYS[2]),
            ('TH', WEEKDAYS[3]),
            ('FR', WEEKDAYS[4]),
            ('SA', WEEKDAYS[5]),
            ('SU', WEEKDAYS[6]),
            ('MO,TU,WE,TH,FR,SA,SU', _('Day')),
            ('MO,TU,WE,TH,FR', _('Weekday')),
            ('SA,SU', _('Weekend day')),
        ],
        required=False
    )
    yearly_bymonth = forms.ChoiceField(
        choices=[
            (str(i), MONTHS[i]) for i in range(1, 13)
        ],
        required=False
    )

    monthly_same = forms.ChoiceField(
        choices=[
            ('on', ''),
            ('off', ''),
        ],
        initial='on',
        widget=forms.RadioSelect
    )
    monthly_bysetpos = forms.ChoiceField(
        choices=[
            ('1', pgettext_lazy('rrule', 'first')),
            ('2', pgettext_lazy('rrule', 'second')),
            ('3', pgettext_lazy('rrule', 'third')),
            ('-1', pgettext_lazy('rrule', 'last')),
        ],
        required=False
    )
    monthly_byweekday = forms.ChoiceField(
        choices=[
            ('MO', WEEKDAYS[0]),
            ('TU', WEEKDAYS[1]),
            ('WE', WEEKDAYS[2]),
            ('TH', WEEKDAYS[3]),
            ('FR', WEEKDAYS[4]),
            ('SA', WEEKDAYS[5]),
            ('SU', WEEKDAYS[6]),
            ('MO,TU,WE,TH,FR,SA,SU', _('Day')),
            ('MO,TU,WE,TH,FR', _('Weekday')),
            ('SA,SU', _('Weekend day')),
        ],
        required=False
    )

    weekly_byweekday = forms.MultipleChoiceField(
        choices=[
            ('MO', WEEKDAYS[0]),
            ('TU', WEEKDAYS[1]),
            ('WE', WEEKDAYS[2]),
            ('TH', WEEKDAYS[3]),
            ('FR', WEEKDAYS[4]),
            ('SA', WEEKDAYS[5]),
            ('SU', WEEKDAYS[6]),
        ],
        required=False,
        widget=forms.CheckboxSelectMultiple
    )

    def parse_weekdays(self, value):
        m = {
            'MO': 0,
            'TU': 1,
            'WE': 2,
            'TH': 3,
            'FR': 4,
            'SA': 5,
            'SU': 6
        }
        if ',' in value:
            return [m.get(a) for a in value.split(',')]
        else:
            return m.get(value)


RRuleFormSet = formset_factory(
    RRuleForm,
    can_order=False, can_delete=True, extra=1
)


class TimeForm(forms.Form):
    time_from = forms.TimeField(
        label=_('Event start time'),
        widget=forms.TimeInput(attrs={'class': 'timepickerfield'}),
        required=True
    )
    time_to = forms.TimeField(
        label=_('Event end time'),
        widget=forms.TimeInput(attrs={'class': 'timepickerfield'}),
        required=False
    )
    time_admission = forms.TimeField(
        label=_('Admission time'),
        widget=forms.TimeInput(attrs={'class': 'timepickerfield'}),
        required=False
    )


TimeFormSet = formset_factory(
    TimeForm,
    min_num=1,
    can_order=False, can_delete=True, extra=1, validate_min=True
)
