#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program.  If not, see
# <https://www.gnu.org/licenses/>.
#
import calendar
from datetime import date, datetime, time, timedelta, timezone
from itertools import groupby
from typing import Optional, Tuple

from django import forms
from django.core.exceptions import ValidationError
from django.utils.formats import date_format
from django.utils.timezone import make_aware, now
from django.utils.translation import gettext_lazy, pgettext_lazy
from rest_framework import serializers

from pretix.helpers.daterange import daterange


def _quarter_start(ref_d):
    return ref_d.replace(day=1, month=1 + (ref_d.month - 1) // 3 * 3)


def _week_start(ref_d):
    return ref_d - timedelta(ref_d.weekday())


REPORTING_DATE_TIMEFRAMES = (
    # (identifier, label, start_inclusive, end_inclusive, includes_future, optgroup, describe)
    (
        'days_today',
        pgettext_lazy('reporting_timeframe', 'Today'),
        lambda ref_d: ref_d,
        lambda ref_d, start_d: start_d,
        False,
        pgettext_lazy('reporting_timeframe', 'by day'),
        daterange
    ),
    (
        'days_yesterday',
        pgettext_lazy('reporting_timeframe', 'Yesterday'),
        lambda ref_d: ref_d - timedelta(days=1),
        lambda ref_d, start_d: start_d,
        False,
        pgettext_lazy('reporting_timeframe', 'by day'),
        daterange
    ),
    (
        'days_last7',
        pgettext_lazy('reporting_timeframe', 'Last 7 days'),
        lambda ref_d: ref_d - timedelta(days=6),
        lambda ref_d, start_d: start_d + timedelta(days=6),
        False,
        pgettext_lazy('reporting_timeframe', 'by day'),
        daterange
    ),
    (
        'days_last14',
        pgettext_lazy('reporting_timeframe', 'Last 14 days'),
        lambda ref_d: ref_d - timedelta(days=13),
        lambda ref_d, start_d: start_d + timedelta(days=13),
        False,
        pgettext_lazy('reporting_timeframe', 'by day'),
        daterange
    ),
    (
        'days_tomorrow',
        pgettext_lazy('reporting_timeframe', 'Tomorrow'),
        lambda ref_d: ref_d + timedelta(days=1),
        lambda ref_d, start_d: start_d,
        True,
        pgettext_lazy('reporting_timeframe', 'by day'),
        daterange
    ),
    (
        'days_next7',
        pgettext_lazy('reporting_timeframe', 'Next 7 days'),
        lambda ref_d: ref_d + timedelta(days=1),
        lambda ref_d, start_d: start_d + timedelta(days=6),
        True,
        pgettext_lazy('reporting_timeframe', 'by day'),
        daterange
    ),
    (
        'days_next14',
        pgettext_lazy('reporting_timeframe', 'Next 14 days'),
        lambda ref_d: ref_d + timedelta(days=1),
        lambda ref_d, start_d: start_d + timedelta(days=13),
        True,
        pgettext_lazy('reporting_timeframe', 'by day'),
        daterange
    ),
    (
        'week_this',
        pgettext_lazy('reporting_timeframe', 'Current week'),
        lambda ref_d: _week_start(ref_d),
        lambda ref_d, start_d: start_d + timedelta(days=6),
        True,
        pgettext_lazy('reporting_timeframe', 'by week'),
        lambda start_d, end_d: date_format(start_d, 'WEEK_FORMAT') + ' - ' + daterange(start_d, end_d),
    ),
    (
        'week_to_date',
        pgettext_lazy('reporting_timeframe', 'Current week to date'),
        lambda ref_d: _week_start(ref_d),
        lambda ref_d, start_d: ref_d,
        False,
        pgettext_lazy('reporting_timeframe', 'by week'),
        lambda start_d, end_d: date_format(start_d, 'WEEK_FORMAT') + ' - ' + daterange(start_d, end_d),
    ),
    (
        'week_previous',
        pgettext_lazy('reporting_timeframe', 'Previous week'),
        lambda ref_d: _week_start(ref_d) - timedelta(days=7),
        lambda ref_d, start_d: start_d + timedelta(days=6),
        False,
        pgettext_lazy('reporting_timeframe', 'by week'),
        lambda start_d, end_d: date_format(start_d, 'WEEK_FORMAT') + ' - ' + daterange(start_d, end_d),
    ),
    (
        'week_next',
        pgettext_lazy('reporting_timeframe', 'Next week'),
        lambda ref_d: _week_start(ref_d + timedelta(days=7)),
        lambda ref_d, start_d: start_d + timedelta(days=6),
        True,
        pgettext_lazy('reporting_timeframe', 'by week'),
        lambda start_d, end_d: date_format(start_d, 'WEEK_FORMAT') + ' - ' + daterange(start_d, end_d),
    ),
    (
        'month_this',
        pgettext_lazy('reporting_timeframe', 'Current month'),
        lambda ref_d: ref_d.replace(day=1),
        lambda ref_d, start_d: start_d.replace(day=calendar.monthrange(start_d.year, start_d.month)[1]),
        True,
        pgettext_lazy('reporting_timeframe', 'by month'),
        lambda start_d, end_d: date_format(start_d, 'YEAR_MONTH_FORMAT'),
    ),
    (
        'month_to_date',
        pgettext_lazy('reporting_timeframe', 'Current month to date'),
        lambda ref_d: ref_d.replace(day=1),
        lambda ref_d, start_d: ref_d,
        False,
        pgettext_lazy('reporting_timeframe', 'by month'),
        lambda start_d, end_d: date_format(start_d, 'YEAR_MONTH_FORMAT'),
    ),
    (
        'month_previous',
        pgettext_lazy('reporting_timeframe', 'Previous month'),
        lambda ref_d: (ref_d.replace(day=1) - timedelta(days=1)).replace(day=1),
        lambda ref_d, start_d: start_d.replace(day=calendar.monthrange(start_d.year, start_d.month)[1]),
        False,
        pgettext_lazy('reporting_timeframe', 'by month'),
        lambda start_d, end_d: date_format(start_d, 'YEAR_MONTH_FORMAT'),
    ),
    (
        'month_next',
        pgettext_lazy('reporting_timeframe', 'Next month'),
        lambda ref_d: ref_d.replace(day=calendar.monthrange(ref_d.year, ref_d.month)[1]) + timedelta(days=1),
        lambda ref_d, start_d: start_d.replace(day=calendar.monthrange(start_d.year, start_d.month)[1]),
        True,
        pgettext_lazy('reporting_timeframe', 'by month'),
        lambda start_d, end_d: date_format(start_d, 'YEAR_MONTH_FORMAT'),
    ),
    (
        'quarter_this',
        pgettext_lazy('reporting_timeframe', 'Current quarter'),
        lambda ref_d: _quarter_start(ref_d),
        lambda ref_d, start_d: start_d.replace(day=calendar.monthrange(start_d.year, start_d.month + 2)[1], month=start_d.month + 2),
        True,
        pgettext_lazy('reporting_timeframe', 'by quarter'),
        lambda start_d, end_d: f"Q{(start_d.month - 1) // 3 + 1}/{start_d.year}",
    ),
    (
        'quarter_to_date',
        pgettext_lazy('reporting_timeframe', 'Current quarter to date'),
        lambda ref_d: _quarter_start(ref_d),
        lambda ref_d, start_d: ref_d,
        False,
        pgettext_lazy('reporting_timeframe', 'by quarter'),
        lambda start_d, end_d: f"Q{(start_d.month - 1) // 3 + 1}/{start_d.year}",
    ),
    (
        'quarter_previous',
        pgettext_lazy('reporting_timeframe', 'Previous quarter'),
        lambda ref_d: _quarter_start(_quarter_start(ref_d) - timedelta(days=1)),
        lambda ref_d, start_d: start_d.replace(day=calendar.monthrange(start_d.year, start_d.month + 2)[1], month=start_d.month + 2),
        False,
        pgettext_lazy('reporting_timeframe', 'by quarter'),
        lambda start_d, end_d: f"Q{(start_d.month - 1) // 3 + 1}/{start_d.year}",
    ),
    (
        'quarter_next',
        pgettext_lazy('reporting_timeframe', 'Next quarter'),
        lambda ref_d: ref_d.replace(
            day=calendar.monthrange(ref_d.year, _quarter_start(ref_d).month + 2)[1], month=_quarter_start(ref_d).month + 2
        ) + timedelta(days=1),
        lambda ref_d, start_d: start_d.replace(day=calendar.monthrange(start_d.year, start_d.month + 2)[1], month=start_d.month + 2),
        True,
        pgettext_lazy('reporting_timeframe', 'by quarter'),
        lambda start_d, end_d: f"Q{(start_d.month - 1) // 3 + 1}/{start_d.year}",
    ),
    (
        'year_this',
        pgettext_lazy('reporting_timeframe', 'Current year'),
        lambda ref_d: ref_d.replace(day=1, month=1),
        lambda ref_d, start_d: start_d.replace(day=31, month=12),
        True,
        pgettext_lazy('reporting_timeframe', 'by year'),
        lambda start_d, end_d: str(start_d.year),
    ),
    (
        'year_to_date',
        pgettext_lazy('reporting_timeframe', 'Current year to date'),
        lambda ref_d: ref_d.replace(day=1, month=1),
        lambda ref_d, start_d: ref_d,
        False,
        pgettext_lazy('reporting_timeframe', 'by year'),
        lambda start_d, end_d: str(start_d.year),
    ),
    (
        'year_previous',
        pgettext_lazy('reporting_timeframe', 'Previous year'),
        lambda ref_d: (ref_d.replace(day=1, month=1) - timedelta(days=1)).replace(day=1, month=1),
        lambda ref_d, start_d: start_d.replace(day=31, month=12),
        False,
        pgettext_lazy('reporting_timeframe', 'by year'),
        lambda start_d, end_d: str(start_d.year),
    ),
    (
        'year_next',
        pgettext_lazy('reporting_timeframe', 'Next year'),
        lambda ref_d: ref_d.replace(day=1, month=1, year=ref_d.year + 1),
        lambda ref_d, start_d: start_d.replace(day=31, month=12),
        True,
        pgettext_lazy('reporting_timeframe', 'by year'),
        lambda start_d, end_d: str(start_d.year),
    ),
    (
        'future',
        pgettext_lazy('reporting_timeframe', 'All future (excluding today)'),
        lambda ref_d: ref_d + timedelta(days=1),
        lambda ref_d, start_d: None,
        True,
        pgettext_lazy('reporting_timeframe', 'Other'),
        lambda start_d, end_d: date_format(start_d, "SHORT_DATE_FORMAT") + ' – ',
    ),
    (
        'past',
        pgettext_lazy('reporting_timeframe', 'All past (including today)'),
        lambda ref_d: None,
        lambda ref_d, start_d: ref_d,
        True,  # technically false, but only makes sense to have in a selection that also allows the future, otherwise redundant
        pgettext_lazy('reporting_timeframe', 'Other'),
        lambda start_d, end_d: ' – ' + date_format(end_d, "SHORT_DATE_FORMAT"),
    ),
)


class DateFrameWidget(forms.MultiWidget):
    template_name = 'pretixbase/forms/widgets/dateframe.html'

    def __init__(self, *args, **kwargs):
        self.timeframe_choices = kwargs.pop('timeframe_choices')
        widgets = (
            forms.Select(choices=self.timeframe_choices),
            forms.DateInput(attrs={'class': 'datepickerfield', 'placeholder': pgettext_lazy('timeframe', 'Start')}),
            forms.DateInput(attrs={'class': 'datepickerfield', 'placeholder': pgettext_lazy('timeframe', 'End')}),
        )
        super().__init__(widgets=widgets, *args, **kwargs)

    def decompress(self, value):
        if not value:
            return ['unset', None, None]
        if '/' in value:
            return [
                'custom',
                date.fromisoformat(value.split('/', 1)[0]) if value.split('/', 1)[0] else None,
                date.fromisoformat(value.split('/', 1)[-1]) if value.split('/', 1)[-1] else None,
            ]
        return [value, None, None]

    def get_context(self, name, value, attrs):
        ctx = super().get_context(name, value, attrs)
        ctx['required'] = self.timeframe_choices[0][0] == 'unset'
        ctx['widget']['subwidgets'][1]['attrs'].pop('required', None)
        ctx['widget']['subwidgets'][2]['attrs'].pop('required', None)
        return ctx


def _describe_timeframe(label, start, end, future, describe):
    d_start = start(now())
    d_end = end(now(), d_start)
    details = describe(d_start, d_end)
    return f'{label} ({details})'


class DateFrameField(forms.MultiValueField):
    default_error_messages = {
        **forms.MultiValueField.default_error_messages,
        'inconsistent': gettext_lazy('The end date must be after the start date.'),
    }

    def __init__(self, *args, **kwargs):
        include_future_frames = kwargs.pop('include_future_frames')

        top_choices = [('custom', gettext_lazy('Custom timeframe'))]
        if not kwargs.get('required', True):
            top_choices.insert(0, ('unset', pgettext_lazy('reporting_timeframe', 'All time')))

        _choices = []
        for grouper, group in groupby(REPORTING_DATE_TIMEFRAMES, key=lambda i: i[5]):
            options = [
                (identifier, _describe_timeframe(label, start, end, future, describe))
                for identifier, label, start, end, future, group, describe in group
                if include_future_frames or not future
            ]
            if options:
                _choices.append((grouper, options))

        timeframe_choices = [
            ('', top_choices)
        ] + _choices

        fields = (
            forms.ChoiceField(
                choices=timeframe_choices,
                required=True
            ),
            forms.DateField(
                required=False
            ),
            forms.DateField(
                required=False
            ),
        )
        if 'widget' not in kwargs:
            kwargs['widget'] = DateFrameWidget(timeframe_choices=timeframe_choices)
        kwargs.pop('max_length', 0)
        kwargs.pop('empty_value', 0)
        super().__init__(
            fields=fields, require_all_fields=False, *args, **kwargs
        )

    def compress(self, data_list):
        if not data_list:
            return None
        if data_list[0] == 'unset':
            return None
        elif data_list[0] == 'custom':
            return f'{data_list[1].isoformat() if data_list[1] else ""}/{data_list[2].isoformat() if data_list[2] else ""}'
        else:
            return data_list[0]

    def has_changed(self, initial, data):
        if initial is None:
            initial = self.widget.decompress(initial)
        return super().has_changed(initial, data)

    def clean(self, value):
        if not value:
            return None
        if value[0] == 'custom':
            if not value[1] and not value[2]:
                raise ValidationError(self.error_messages['incomplete'])
            if value[1] and value[2] and self.fields[2].to_python(value[2]) < self.fields[1].to_python(value[1]):
                raise ValidationError(self.error_messages['inconsistent'])
        return super().clean(value)


class SerializerDateFrameField(serializers.CharField):

    def to_internal_value(self, data):
        if data is None:
            return None
        try:
            resolve_timeframe_to_dates_inclusive(now(), data, timezone.utc)
        except:
            raise ValidationError("Invalid date frame")
        else:
            return data

    def to_representation(self, value):
        if value is None:
            return None
        return value


def resolve_timeframe_to_dates_inclusive(ref_dt, frame, timezone) -> Tuple[Optional[date], Optional[date]]:
    """
    Given a serialized timeframe, evaluate it relative to `ref_dt` and return a tuple of dates
    where the first element ist the first possible date value within the timeframe and the second
    element is the last possible date value in the timeframe.
    Both returned values may be ``None`` for an unlimited interval.
    """
    if isinstance(ref_dt, datetime):
        ref_dt = ref_dt.astimezone(timezone).date()
    if "/" in frame:
        start, end = frame.split("/", 1)
        return date.fromisoformat(start) if start else None, date.fromisoformat(end) if end else None
    for idf, label, start, end, includes_future, *args in REPORTING_DATE_TIMEFRAMES:
        if frame == idf:
            d_start = start(ref_dt)
            d_end = end(ref_dt, d_start)
            return d_start, d_end
    raise ValueError(f"Invalid timeframe '{frame}'")


def resolve_timeframe_to_datetime_start_inclusive_end_exclusive(ref_dt, frame, timezone) -> Tuple[Optional[date], Optional[date]]:
    """
    Given a serialized timeframe, evaluate it relative to `ref_dt` and return a tuple of datetimes
    where the first element ist the first possible datetime within the timeframe and the second
    element is the first possible datetime value *not* in the timeframe.
    Both returned values may be ``None`` for an unlimited interval.
    """
    d_start, d_end = resolve_timeframe_to_dates_inclusive(ref_dt, frame, timezone)
    dt_start = make_aware(datetime.combine(d_start, time(0, 0, 0)), timezone) if d_start else None
    dt_end = make_aware(datetime.combine(d_end + timedelta(days=1), time(0, 0, 0)), timezone) if d_end else None
    return dt_start, dt_end
