#
# 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/>.
#
from datetime import timedelta
from typing import List, Optional

from dateutil.relativedelta import relativedelta
from django.core.exceptions import ValidationError
from django.utils.formats import date_format
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _

from pretix.base.models import (
    AbstractPosition, CartPosition, Customer, Event, Item, Membership, Order,
    OrderPosition, SubEvent,
)
from pretix.helpers import OF_SELF


def membership_validity(item: Item, subevent: Optional[SubEvent], event: Event):
    tz = event.timezone
    if item.grant_membership_duration_like_event:
        ev = subevent or event
        date_start = ev.date_from
        date_end = ev.date_to

        if not date_end:
            # Use end of day, if event end date is not set
            date_end = date_start.astimezone(tz).replace(hour=23, minute=59, second=59, microsecond=999999)

    else:
        # Always start at start of day
        date_start = now().astimezone(tz).replace(hour=0, minute=0, second=0, microsecond=0)
        date_end = date_start

        if item.grant_membership_duration_months:
            date_end -= timedelta(days=1)  # start on 25th gives end on 26th
            date_end += relativedelta(months=item.grant_membership_duration_months)  # start on 31th may give end on 28th

        if item.grant_membership_duration_days:
            date_end += timedelta(days=item.grant_membership_duration_days)
            if not item.grant_membership_duration_months:
                # Correct off-by-one due to first day
                date_end -= timedelta(days=1)

        # Always end at end of day
        date_end = date_end.astimezone(tz).replace(hour=23, minute=59, second=59, microsecond=999999)

    return date_start, date_end


def create_membership(customer: Customer, position: OrderPosition):
    item = position.item

    date_start, date_end = membership_validity(item, position.subevent, position.order.event)

    customer.memberships.create(
        membership_type=position.item.grant_membership_type,
        granted_in=position,
        date_start=date_start,
        date_end=date_end,
        attendee_name_parts=position.attendee_name_parts,
        testmode=position.order.testmode,
    )


def validate_memberships_in_order(customer: Customer, positions: List[AbstractPosition], event: Event, lock=False, ignored_order: Order = None, testmode=False,
                                  valid_from_not_chosen=False):
    """
    Validate that a set of cart or order positions. This currently does not validate

    :param customer: Customer to validate for
    :param positions: List of order or cart positions
    :param event: Event this all is computed in
    :param lock: Whether to place a SELECT FOR UPDATE lock on the selected memberships
    :param ignored_order: An order that should be ignored for usage counting
    :param testmode: If ``True``, only test mode memberships are allowed. If ``False``, test mode memberships are not allowed.
    :param valid_from_not_chosen: Set to ``True`` to indicate that the customer is in an early step of the checkout flow
                                  where the valid_from date is not selected yet. In this case, the valid_from date is not checked.
    """
    tz = event.timezone
    applicable_positions = [
        p for p in positions
        if p.item.require_membership or (p.variation and p.variation.require_membership)
    ]

    for p in positions:
        if p not in applicable_positions and p.used_membership_id:
            raise ValidationError(
                _('You selected a membership for the product "{product}" which does not require a membership.').format(
                    product=str(p.item.name) + (' – ' + str(p.variation.value) if p.variation else '')
                )
            )

    for p in applicable_positions:
        if not p.used_membership_id:
            raise ValidationError(
                _('You selected the product "{product}" which requires an active membership to '
                  'be selected.').format(
                    product=str(p.item.name) + (' – ' + str(p.variation.value) if p.variation else '')
                )
            )

    base_qs = Membership.objects.with_usages(ignored_order=ignored_order)

    if lock:
        base_qs = base_qs.select_for_update(of=OF_SELF)

    membership_cache = base_qs\
        .select_related('membership_type')\
        .prefetch_related('orderposition_set', 'orderposition_set__order', 'orderposition_set__order__event', 'orderposition_set__subevent')\
        .in_bulk([p.used_membership_id for p in applicable_positions])

    for m in membership_cache.values():
        qs = m.orderposition_set.filter(canceled=False).exclude(order__status=Order.STATUS_CANCELED)
        if ignored_order:
            qs = qs.exclude(order_id=ignored_order.pk)
        m._used_at_dates = [
            (op.subevent or op.order.event).date_from
            for op in qs if not op.valid_from or not op.valid_until
        ]
        m._used_for_ranges = [
            (op.valid_from, op.valid_until)
            for op in qs if op.valid_from or op.valid_until
        ]

    for p in applicable_positions:
        m = membership_cache[p.used_membership_id]
        if not customer or m.customer_id != customer.pk:
            raise ValidationError(
                _('You selected a membership that is connected to a different customer account.')
            )

        if m.canceled:
            raise ValidationError(
                _('You selected membership that has been canceled.')
            )

        if m.testmode and not testmode:
            raise ValidationError(
                _('You can not use a test mode membership for tickets that are not in test mode.')
            )
        elif not m.testmode and testmode:
            raise ValidationError(
                _('You need to add a test mode membership to the customer account to use it in test mode.')
            )

        ev = p.subevent or event

        if isinstance(p, (OrderPosition, CartPosition)):
            # override_ variants are for usage of fake cart in OrderChangeManager
            valid_from = getattr(p, 'override_valid_from', p.valid_from)
            valid_until = getattr(p, 'override_valid_until', p.valid_until)
        else:  # future safety, not technically defined on AbstractPosition
            valid_from = None
            valid_until = None

        if not m.is_valid(ev, valid_from, valid_from_not_chosen=p.item.validity_dynamic_start_choice and valid_from_not_chosen):
            if valid_from:
                raise ValidationError(
                    _('You selected a membership that is valid from {start} to {end}, but selected a ticket that '
                      'starts to be valid on {date}.').format(
                        start=date_format(m.date_start.astimezone(tz), 'SHORT_DATETIME_FORMAT'),
                        end=date_format(m.date_end.astimezone(tz), 'SHORT_DATETIME_FORMAT'),
                        date=date_format(valid_from.astimezone(tz), 'SHORT_DATETIME_FORMAT'),
                    )
                )
            else:
                raise ValidationError(
                    _('You selected a membership that is valid from {start} to {end}, but selected an event '
                      'taking place at {date}.').format(
                        start=date_format(m.date_start.astimezone(tz), 'SHORT_DATETIME_FORMAT'),
                        end=date_format(m.date_end.astimezone(tz), 'SHORT_DATETIME_FORMAT'),
                        date=date_format(ev.date_from.astimezone(tz), 'SHORT_DATETIME_FORMAT'),
                    )
                )

        if p.variation and p.variation.require_membership:
            types = p.variation.require_membership_types.all()
        else:
            types = p.item.require_membership_types.all()

        if not types.filter(pk=m.membership_type_id).exists():
            raise ValidationError(
                _('You selected a membership of type "{type}", which is not allowed for the product "{product}".').format(
                    product=str(p.item.name) + (' – ' + str(p.variation.value) if p.variation else ''),
                    type=m.membership_type.name
                )
            )

        if m.membership_type.max_usages is not None:
            if m.usages >= m.membership_type.max_usages:
                raise ValidationError(
                    _('You are trying to use a membership of type "{type}" more than {number} times, which is the maximum amount.').format(
                        type=m.membership_type.name,
                        number=m.usages,
                    )
                )
            m.usages += 1

        if not m.membership_type.allow_parallel_usage:
            if (valid_from or valid_until) and not (p.item.validity_dynamic_start_choice and valid_from_not_chosen):
                for used_range in m._used_for_ranges:
                    if valid_from and valid_from > used_range[1]:
                        continue
                    if valid_until and valid_until < used_range[0]:
                        continue
                    raise ValidationError(
                        _('You are trying to use a membership of type "{type}" for a ticket valid from {valid_from} '
                          'until {valid_until}, however you already used the same membership for a different ticket '
                          'that overlaps with this time frame ({conflict_from} – {conflict_until}).').format(
                            type=m.membership_type.name,
                            valid_from=date_format(valid_from.astimezone(tz), 'SHORT_DATETIME_FORMAT') if valid_from else _('start'),
                            valid_until=date_format(valid_until.astimezone(tz), 'SHORT_DATETIME_FORMAT') if valid_until else _('open end'),
                            conflict_from=date_format(used_range[0].astimezone(tz), 'SHORT_DATETIME_FORMAT') if used_range[0] else _('start'),
                            conflict_until=date_format(used_range[1].astimezone(tz), 'SHORT_DATETIME_FORMAT') if used_range[1] else _('open end'),
                        )
                    )

                m._used_for_ranges.append((p.valid_from, p.valid_until))

            if not valid_from or not valid_until:
                df = ev.date_from
                if any(abs(df - d) < timedelta(minutes=1) for d in m._used_at_dates):
                    raise ValidationError(
                        _('You are trying to use a membership of type "{type}" for an event taking place at {date}, '
                          'however you already used the same membership for a different ticket at the same time.').format(
                            type=m.membership_type.name,
                            date=date_format(ev.date_from.astimezone(tz), 'SHORT_DATETIME_FORMAT'),
                        )
                    )
                m._used_at_dates.append(ev.date_from)
