#
# 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 datetime, time, timedelta

from dateutil.tz import datetime_exists
from django.db import models
from django.db.models import Exists, OuterRef, Q
from django.utils import timezone
from django.utils.formats import date_format
from django.utils.timezone import make_aware, now
from django.utils.translation import gettext_lazy as _, ngettext
from django_scopes import ScopedManager
from i18nfield.fields import I18nCharField, I18nTextField

from pretix.base.email import get_email_context
from pretix.base.i18n import language
from pretix.base.models import (
    Checkin, Event, InvoiceAddress, Item, Order, OrderPosition, SubEvent,
    fields,
)
from pretix.base.models.base import LoggingMixin
from pretix.base.services.mail import SendMailException


class ScheduledMail(models.Model):
    STATE_SCHEDULED = 'scheduled'
    STATE_FAILED = 'failed'
    STATE_COMPLETED = 'completed'
    STATE_MISSED = 'missed'

    STATE_CHOICES = [
        (STATE_SCHEDULED, _('scheduled')),
        (STATE_FAILED, _('failed')),
        (STATE_COMPLETED, _('completed')),
        (STATE_MISSED, _('missed')),
    ]

    id = models.BigAutoField(primary_key=True)
    rule = models.ForeignKey("Rule", on_delete=models.CASCADE)
    subevent = models.ForeignKey(SubEvent, null=True, on_delete=models.CASCADE)
    event = models.ForeignKey(Event, on_delete=models.CASCADE)

    last_computed = models.DateTimeField(auto_now_add=True)
    computed_datetime = models.DateTimeField(db_index=True)

    state = models.CharField(max_length=100, choices=STATE_CHOICES, default=STATE_SCHEDULED)
    last_successful_order_id = models.BigIntegerField(null=True)

    class Meta:
        unique_together = ('rule', 'subevent'),

    def save(self, **kwargs):
        if not self.computed_datetime:
            self.recompute()
            if 'update_fields' in kwargs:
                kwargs['update_fields'] = {'computed_datetime', 'last_computed', 'state'}.union(kwargs['update_fields'])

        super().save(**kwargs)

    def recompute(self):
        if self.state == self.STATE_COMPLETED:
            return

        if self.rule.date_is_absolute:
            self.computed_datetime = self.rule.send_date
        else:
            e = self.subevent or self.event
            o_days = self.rule.send_offset_days
            if not self.rule.offset_is_after:
                o_days *= -1

            offset = timedelta(days=o_days)
            st = self.rule.send_offset_time
            base_time = (e.date_to or e.date_from) if self.rule.offset_to_event_end else e.date_from
            d = base_time.astimezone(self.event.timezone).date() + offset
            self.computed_datetime = make_aware(
                datetime.combine(d, time(hour=st.hour, minute=st.minute, second=st.second, microsecond=0, fold=1)),
                self.event.timezone,
            )
            if not datetime_exists(self.computed_datetime):
                self.computed_datetime = make_aware(
                    datetime.combine(d, time(hour=st.hour, minute=st.minute, second=st.second, microsecond=0)) + timedelta(hours=1),
                    self.event.timezone,
                )

        if self.computed_datetime > timezone.now() and self.state == self.STATE_MISSED:
            self.state = self.STATE_SCHEDULED
        self.last_computed = timezone.now()

    def send(self):
        if self.state not in (ScheduledMail.STATE_SCHEDULED, ScheduledMail.STATE_FAILED):
            raise ValueError("Should not be called in this state")

        e = self.event

        orders = e.orders.all()

        filter_orders_by_op = False
        op_qs = OrderPosition.objects.filter(
            order__event=self.event,
            canceled=False,
        )

        if self.subevent:
            filter_orders_by_op = True
            op_qs = op_qs.filter(subevent=self.subevent)
        elif e.has_subevents:
            return  # This rule should not even exist

        if not self.rule.all_products:
            filter_orders_by_op = True
            limit_products = self.rule.limit_products.values_list('pk', flat=True)
            op_qs = op_qs.filter(item_id__in=limit_products)

        if self.rule.checked_in_status == "no_checkin":
            filter_orders_by_op = True
            op_qs = op_qs.filter(~Exists(Checkin.objects.filter(position_id=OuterRef('pk'), list__consider_tickets_used=True)))
        elif self.rule.checked_in_status == "checked_in":
            filter_orders_by_op = True
            op_qs = op_qs.filter(Exists(Checkin.objects.filter(position_id=OuterRef('pk'), list__consider_tickets_used=True)))

        status_q = Q(status__in=self.rule.restrict_to_status)
        if 'n__pending_approval' in self.rule.restrict_to_status:
            status_q |= Q(status=Order.STATUS_PENDING, require_approval=True)
        if 'n__not_pending_approval_and_not_valid_if_pending' in self.rule.restrict_to_status:
            status_q |= Q(status=Order.STATUS_PENDING, require_approval=False, valid_if_pending=False)
        if 'n__valid_if_pending' in self.rule.restrict_to_status:
            status_q |= Q(status=Order.STATUS_PENDING, require_approval=False, valid_if_pending=True)
        if 'n__pending_overdue' in self.rule.restrict_to_status:
            status_q |= Q(status=Order.STATUS_PENDING, require_approval=False, valid_if_pending=False,
                          expires__lt=now())

        if self.last_successful_order_id:
            orders = orders.filter(
                pk__gt=self.last_successful_order_id
            )

        if filter_orders_by_op:
            orders = orders.filter(pk__in=op_qs.values_list('order_id', flat=True))
        orders = orders.filter(
            status_q,
        ).order_by('pk').select_related('invoice_address').prefetch_related('positions')

        send_to_orders = self.rule.send_to in (Rule.CUSTOMERS, Rule.BOTH)
        send_to_attendees = self.rule.send_to in (Rule.ATTENDEES, Rule.BOTH)

        for o in orders:
            with language(o.locale, e.settings.region):
                positions = list(o.positions.all())
                o_sent = False

                try:
                    ia = o.invoice_address
                except InvoiceAddress.DoesNotExist:
                    ia = InvoiceAddress(order=o)

                if send_to_orders and o.email:
                    email_ctx = get_email_context(event=e, order=o, invoice_address=ia)
                    try:
                        o.send_mail(self.rule.subject, self.rule.template, email_ctx,
                                    attach_ical=self.rule.attach_ical,
                                    log_entry_type='pretix.plugins.sendmail.rule.order.email.sent')
                        o_sent = True
                    except SendMailException:
                        ...  # ¯\_(ツ)_/¯

                if send_to_attendees:
                    if not self.rule.all_products:
                        positions = [p for p in positions if p.item_id in limit_products]
                    if self.subevent_id:
                        positions = [p for p in positions if p.subevent_id == self.subevent_id]

                    for p in positions:
                        try:
                            if p.attendee_email and (p.attendee_email != o.email or not o_sent):
                                email_ctx = get_email_context(event=e, order=o, invoice_address=ia, position=p)
                                p.send_mail(self.rule.subject, self.rule.template, email_ctx,
                                            attach_ical=self.rule.attach_ical,
                                            log_entry_type='pretix.plugins.sendmail.rule.order.position.email.sent')
                            elif not o_sent and o.email:
                                email_ctx = get_email_context(event=e, order=o, invoice_address=ia)
                                o.send_mail(self.rule.subject, self.rule.template, email_ctx,
                                            attach_ical=self.rule.attach_ical,
                                            log_entry_type='pretix.plugins.sendmail.rule.order.email.sent')
                                o_sent = True
                        except SendMailException:
                            ...  # ¯\_(ツ)_/¯

                self.last_successful_order_id = o.pk


class Rule(models.Model, LoggingMixin):
    CUSTOMERS = "orders"
    ATTENDEES = "attendees"
    BOTH = "both"

    SEND_TO_CHOICES = [
        (CUSTOMERS, _("Everyone who created a ticket order")),
        (ATTENDEES, _("Every attendee (falling back to the order contact when no attendee email address is given)")),
        (BOTH, _('Both (all order contact addresses and all attendee email addresses)'))
    ]

    CHECK_IN_STATUS_CHOICES = [
        (None, _("Everyone")),
        ("checked_in", _("Anyone who is or was checked in")),
        ("no_checkin", _("Anyone who never checked in before"))
    ]

    id = models.BigAutoField(primary_key=True)
    event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name='sendmail_rules')
    subevent = models.ForeignKey(SubEvent, null=True, on_delete=models.PROTECT)

    subject = I18nCharField(max_length=255, verbose_name=_('Subject'))
    template = I18nTextField(verbose_name=_('Message'))

    all_products = models.BooleanField(default=True, verbose_name=_('All products'))
    limit_products = models.ManyToManyField(Item, blank=True, verbose_name=_('Limit products'))

    restrict_to_status = fields.MultiStringField(
        verbose_name=_("Restrict to orders with status"),
        default=['p', 'n__valid_if_pending'],
    )

    checked_in_status = models.CharField(
        verbose_name=_("Restrict to check-in status"),
        default=None,
        choices=CHECK_IN_STATUS_CHOICES,
        max_length=10,
        null=True,
        blank=True,
    )

    attach_ical = models.BooleanField(
        default=False,
        verbose_name=_("Attach calendar files"),
    )

    # either send_date or send_offset_* have to be set
    send_date = models.DateTimeField(null=True, blank=True, verbose_name=_('Send date'))
    send_offset_days = models.IntegerField(null=True, blank=True, verbose_name=_('Number of days'))
    send_offset_time = models.TimeField(null=True, blank=True, verbose_name=_('Time of day'))

    date_is_absolute = models.BooleanField(default=True, blank=True)
    offset_to_event_end = models.BooleanField(default=False, blank=True)  # no verbose name because not actually
    offset_is_after = models.BooleanField(default=False, blank=True)      # displayed in any forms

    send_to = models.CharField(max_length=10, choices=SEND_TO_CHOICES, default=CUSTOMERS, verbose_name=_('Send email to'))

    enabled = models.BooleanField(
        default=True,
        verbose_name=_('Enabled'),
        help_text=_('Only enabled rules are actually sent')
    )

    objects = ScopedManager(organizer='event__organizer')

    def save(self, **kwargs):
        is_creation = not self.pk
        super().save(**kwargs)

        create_sms = []
        if self.event.has_subevents:
            if self.subevent:
                ScheduledMail.objects.get_or_create(rule=self, subevent=self.subevent, event=self.event)
            else:
                for se in self.event.subevents.annotate(has_sm=Exists(ScheduledMail.objects.filter(
                        subevent=OuterRef('pk'), rule=self))).filter(has_sm=False):
                    sm = ScheduledMail(rule=self, subevent=se, event=self.event)
                    sm.recompute()
                    create_sms.append(sm)
            ScheduledMail.objects.bulk_create(create_sms)
        else:
            ScheduledMail.objects.get_or_create(rule=self, event=self.event)

        if not is_creation:
            if self.subevent:
                keep_states = [ScheduledMail.STATE_COMPLETED]  # we keep rules where mails have already been sent
                ScheduledMail.objects.filter(
                    Q(rule=self),
                    ~Q(subevent=self.subevent),
                    ~Q(state__in=keep_states)
                ).delete()

            update_sms = []
            for sm in self.scheduledmail_set.prefetch_related('event').select_related('subevent'):
                if sm in create_sms:
                    continue
                previous = sm.computed_datetime
                sm.recompute()
                if sm.computed_datetime != previous:
                    update_sms.append(sm)

            ScheduledMail.objects.bulk_update(update_sms, ['computed_datetime', 'last_computed', 'state'], 100)

    @property
    def human_readable_time(self):
        if self.date_is_absolute:
            d = self.send_date.astimezone(self.event.timezone)
            return _('on {date} at {time}').format(date=date_format(d, 'SHORT_DATE_FORMAT'),
                                                   time=date_format(d, 'TIME_FORMAT'))
        else:
            if self.offset_to_event_end:
                if self.offset_is_after:
                    s = ngettext(
                        '%(count)d day after event end at %(time)s',
                        '%(count)d days after event end at %(time)s',
                        self.send_offset_days
                    ) % {
                        'count': self.send_offset_days,
                        'time': date_format(self.send_offset_time, 'TIME_FORMAT')
                    }
                else:
                    s = ngettext(
                        '%(count)d day before event end at %(time)s',
                        '%(count)d days before event end at %(time)s',
                        self.send_offset_days
                    ) % {
                        'count': self.send_offset_days,
                        'time': date_format(self.send_offset_time, 'TIME_FORMAT')
                    }
            else:
                if self.offset_is_after:
                    s = ngettext(
                        '%(count)d day after event start at %(time)s',
                        '%(count)d days after event start at %(time)s',
                        self.send_offset_days
                    ) % {
                        'count': self.send_offset_days,
                        'time': date_format(self.send_offset_time, 'TIME_FORMAT')
                    }
                else:
                    s = ngettext(
                        '%(count)d day before event start at %(time)s',
                        '%(count)d days before event start at %(time)s',
                        self.send_offset_days
                    ) % {
                        'count': self.send_offset_days,
                        'time': date_format(self.send_offset_time, 'TIME_FORMAT')
                    }
            return s
