#
# 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/>.
#

# This file is based on an earlier version of pretix which was released under the Apache License 2.0. The full text of
# the Apache License 2.0 can be obtained at <http://www.apache.org/licenses/LICENSE-2.0>.
#
# This file may have since been changed and any changes are released under the terms of AGPLv3 as described above. A
# full history of changes and contributors is available at <https://github.com/pretix/pretix>.
#
# This file contains Apache-licensed contributions copyrighted by: Sohalt, Tobias Kunze, jasonwaiting@live.hk
#
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under the License.

from datetime import timedelta
from decimal import Decimal
from zoneinfo import ZoneInfo

from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.db.models import (
    Count, IntegerField, Max, Min, OuterRef, Prefetch, Q, Subquery, Sum,
)
from django.db.models.functions import Coalesce, Greatest
from django.dispatch import receiver
from django.http import JsonResponse
from django.shortcuts import render
from django.template.loader import get_template
from django.urls import reverse
from django.utils import formats
from django.utils.formats import date_format
from django.utils.html import escape
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _, ngettext, pgettext

from pretix.base.decimal import round_decimal
from pretix.base.models import (
    Item, ItemCategory, ItemVariation, Order, OrderPosition, OrderRefund,
    Question, Quota, SubEvent, Voucher, WaitingListEntry,
)
from pretix.base.services.quotas import QuotaAvailability
from pretix.base.timeline import timeline_for_event
from pretix.control.forms.event import CommentForm
from pretix.control.signals import (
    event_dashboard_widgets, user_dashboard_widgets,
)
from pretix.helpers.daterange import daterange

from ...base.models.orders import CancellationRequest
from ..logdisplay import OVERVIEW_BANLIST

NUM_WIDGET = '<div class="numwidget"><span class="num">{num}</span><span class="text">{text}</span></div>'


@receiver(signal=event_dashboard_widgets)
def base_widgets(sender, subevent=None, lazy=False, **kwargs):
    if not lazy:
        prodc = Item.objects.filter(
            event=sender, active=True,
        ).filter(
            (Q(available_until__isnull=True) | Q(available_until__gte=now())) &
            (Q(available_from__isnull=True) | Q(available_from__lte=now()))
        ).count()

        if subevent:
            opqs = OrderPosition.objects.filter(subevent=subevent)
        else:
            opqs = OrderPosition.objects

        tickc = opqs.filter(
            order__event=sender, item__admission=True,
            order__status__in=(Order.STATUS_PAID, Order.STATUS_PENDING),
        ).count()

        paidc = opqs.filter(
            order__event=sender, item__admission=True,
            order__status=Order.STATUS_PAID,
        ).count()

        if subevent:
            rev = opqs.filter(
                order__event=sender, order__status=Order.STATUS_PAID
            ).aggregate(
                sum=Sum('price')
            )['sum'] or Decimal('0.00')
        else:
            rev = Order.objects.filter(
                event=sender,
                status=Order.STATUS_PAID
            ).aggregate(sum=Sum('total'))['sum'] or Decimal('0.00')

    return [
        {
            'content': None if lazy else NUM_WIDGET.format(num=tickc, text=_('Attendees (ordered)')),
            'lazy': 'attendees-ordered',
            'display_size': 'small',
            'priority': 100,
            'url': reverse('control:event.orders', kwargs={
                'event': sender.slug,
                'organizer': sender.organizer.slug
            }) + ('?subevent={}'.format(subevent.pk) if subevent else '')
        },
        {
            'content': None if lazy else NUM_WIDGET.format(num=paidc, text=_('Attendees (paid)')),
            'lazy': 'attendees-paid',
            'display_size': 'small',
            'priority': 100,
            'url': reverse('control:event.orders.overview', kwargs={
                'event': sender.slug,
                'organizer': sender.organizer.slug
            }) + ('?subevent={}'.format(subevent.pk) if subevent else '')
        },
        {
            'content': None if lazy else NUM_WIDGET.format(
                num=formats.localize(round_decimal(rev, sender.currency)), text=_('Total revenue ({currency})').format(currency=sender.currency)),
            'lazy': 'total-revenue',
            'display_size': 'small',
            'priority': 100,
            'url': reverse('control:event.orders.overview', kwargs={
                'event': sender.slug,
                'organizer': sender.organizer.slug
            }) + ('?subevent={}'.format(subevent.pk) if subevent else '')
        },
        {
            'content': None if lazy else NUM_WIDGET.format(num=prodc, text=_('Active products')),
            'lazy': 'active-products',
            'display_size': 'small',
            'priority': 100,
            'url': reverse('control:event.items', kwargs={
                'event': sender.slug,
                'organizer': sender.organizer.slug
            })
        },
    ]


@receiver(signal=event_dashboard_widgets)
def waitinglist_widgets(sender, subevent=None, lazy=False, **kwargs):
    widgets = []

    wles = WaitingListEntry.objects.filter(event=sender, subevent=subevent, voucher__isnull=True)
    if wles.exists():
        if not lazy:
            quota_cache = {}
            happy = 0
            tuples = wles.values('item', 'variation').order_by().annotate(cnt=Count('id'))

            items = {
                i.pk: i for i in sender.items.filter(id__in=[t['item'] for t in tuples]).prefetch_related(
                    Prefetch('quotas',
                             to_attr='_subevent_quotas',
                             queryset=sender.quotas.using(settings.DATABASE_REPLICA).filter(subevent=subevent)),
                )
            }
            vars = {
                i.pk: i for i in ItemVariation.objects.filter(
                    item__event=sender, id__in=[t['variation'] for t in tuples if t['variation']]
                ).prefetch_related(
                    Prefetch('quotas',
                             to_attr='_subevent_quotas',
                             queryset=sender.quotas.using(settings.DATABASE_REPLICA).filter(subevent=subevent)),
                )
            }

            for wlt in tuples:
                item = items.get(wlt['item'])
                variation = vars.get(wlt['variation'])
                if not item:
                    continue
                quotas = (
                    variation._get_quotas(subevent=subevent)
                    if variation
                    else item._get_quotas(subevent=subevent)
                )
                row = (
                    variation.check_quotas(subevent=subevent, count_waitinglist=False, _cache=quota_cache)
                    if variation
                    else item.check_quotas(subevent=subevent, count_waitinglist=False, _cache=quota_cache)
                )
                if row[1] is None:
                    happy += wlt['cnt']
                elif row[1] > 0:
                    happy += min(wlt['cnt'], row[1])
                    for q in quotas:
                        if q.size is not None:
                            quota_cache[q.pk] = (quota_cache[q.pk][0], quota_cache[q.pk][1] - min(wlt['cnt'], row[1]))

        widgets.append({
            'content': None if lazy else NUM_WIDGET.format(
                num=str(happy), text=_('available to give to people on waiting list')
            ),
            'lazy': 'waitinglist-avail',
            'priority': 50,
            'url': reverse('control:event.orders.waitinglist', kwargs={
                'event': sender.slug,
                'organizer': sender.organizer.slug,
            })
        })
        widgets.append({
            'content': None if lazy else NUM_WIDGET.format(num=str(wles.count()), text=_('total waiting list length')),
            'lazy': 'waitinglist-length',
            'display_size': 'small',
            'priority': 50,
            'url': reverse('control:event.orders.waitinglist', kwargs={
                'event': sender.slug,
                'organizer': sender.organizer.slug,
            })
        })

    return widgets


@receiver(signal=event_dashboard_widgets)
def quota_widgets(sender, subevent=None, lazy=False, **kwargs):
    widgets = []
    quotas = sender.quotas.filter(subevent=subevent)

    qa = QuotaAvailability()
    if quotas:
        qa.queue(*quotas)
        qa.compute(allow_cache=True)

    for q in quotas:
        if not lazy:
            status, left = qa.results[q] if q in qa.results else q.availability(allow_cache=True)
        widgets.append({
            'content': None if lazy else NUM_WIDGET.format(
                num='{}/{}'.format(left, q.size) if q.size is not None else '\u221e',
                text=_('{quota} left').format(quota=escape(q.name))
            ),
            'lazy': 'quota-{}'.format(q.pk),
            'display_size': 'small',
            'priority': 50,
            'url': reverse('control:event.items.quotas.show', kwargs={
                'event': sender.slug,
                'organizer': sender.organizer.slug,
                'quota': q.id
            })
        })
    return widgets


@receiver(signal=event_dashboard_widgets)
def shop_state_widget(sender, **kwargs):
    return [{
        'display_size': 'small',
        'priority': 1000,
        'content': '<div class="shopstate">{t1}<br><span class="{cls}"><span class="fa {icon}"></span> {state}</span>{t2}</div>'.format(
            t1=_('Your ticket shop is'), t2=_('Click here to change'),
            state=_('live') if sender.live and not sender.testmode else (
                _('live and in test mode') if sender.live else (
                    _('not yet public') if not sender.testmode else (
                        _('in private test mode')
                    )
                )
            ),
            icon='fa-check-circle' if sender.live and not sender.testmode else (
                'fa-warning' if sender.live else (
                    'fa-times-circle' if not sender.testmode else (
                        'fa-times-circle'
                    )
                )
            ),
            cls='live' if sender.live else 'off'
        ),
        'url': reverse('control:event.live', kwargs={
            'event': sender.slug,
            'organizer': sender.organizer.slug
        })
    }]


@receiver(signal=event_dashboard_widgets)
def checkin_widget(sender, subevent=None, lazy=False, **kwargs):
    widgets = []
    qs = sender.checkin_lists.filter(subevent=subevent)
    for cl in qs:
        widgets.append({
            'content': None if lazy else NUM_WIDGET.format(
                num='{}/{}'.format(cl.inside_count, cl.position_count),
                text=_('Present – {list}').format(list=escape(cl.name))
            ),
            'lazy': 'checkin-{}'.format(cl.pk),
            'display_size': 'small',
            'priority': 50,
            'url': reverse('control:event.orders.checkinlists.show', kwargs={
                'event': sender.slug,
                'organizer': sender.organizer.slug,
                'list': cl.pk
            })
        })
    return widgets


@receiver(signal=event_dashboard_widgets)
def welcome_wizard_widget(sender, **kwargs):
    template = get_template('pretixcontrol/event/dashboard_widget_welcome.html')
    ctx = {
        'title': _('Welcome to pretix!')
    }
    kwargs = {'event': sender.slug, 'organizer': sender.organizer.slug}

    if not sender.items.exists():
        ctx.update({
            'subtitle': _('Get started with our setup tool'),
            'text': _('To start selling tickets, you need to create products or quotas. The fastest way to create '
                      'this is to use our setup tool.'),
            'button_text': _('Set up event'),
            'button_url': reverse('control:event.quick', kwargs=kwargs)
        })
    else:
        return []
    return [{
        'display_size': 'full',
        'priority': 2000,
        'content': template.render(ctx)
    }]


def event_index(request, organizer, event):
    subevent = None
    if request.GET.get("subevent", "") != "" and request.event.has_subevents:
        i = request.GET.get("subevent", "")
        try:
            subevent = request.event.subevents.get(pk=i)
        except SubEvent.DoesNotExist:
            pass

    can_view_orders = request.user.has_event_permission(request.organizer, request.event, 'can_view_orders',
                                                        request=request)
    can_change_event_settings = request.user.has_event_permission(request.organizer, request.event,
                                                                  'can_change_event_settings', request=request)

    widgets = []
    if can_view_orders:
        for r, result in event_dashboard_widgets.send(sender=request.event, subevent=subevent, lazy=True):
            widgets.extend(result)

    ctx = {
        'widgets': rearrange(widgets),
        'subevent': subevent,
        'comment_form': CommentForm(initial={'comment': request.event.comment}, readonly=not can_change_event_settings),
    }

    ctx['has_overpaid_orders'] = can_view_orders and Order.annotate_overpayments(request.event.orders).filter(
        Q(~Q(status=Order.STATUS_CANCELED) & Q(pending_sum_t__lt=0))
        | Q(Q(status=Order.STATUS_CANCELED) & Q(pending_sum_rc__lt=0))
    ).exists()
    ctx['has_pending_orders_with_full_payment'] = can_view_orders and Order.annotate_overpayments(request.event.orders).filter(
        Q(status__in=(Order.STATUS_EXPIRED, Order.STATUS_PENDING)) & Q(pending_sum_t__lte=0) & Q(require_approval=False)
    ).exists()
    ctx['has_pending_refunds'] = can_view_orders and OrderRefund.objects.filter(
        order__event=request.event,
        state__in=(OrderRefund.REFUND_STATE_CREATED, OrderRefund.REFUND_STATE_EXTERNAL)
    ).exists()
    ctx['has_pending_approvals'] = can_view_orders and request.event.orders.filter(
        status=Order.STATUS_PENDING,
        require_approval=True
    ).exists()
    ctx['has_cancellation_requests'] = can_view_orders and CancellationRequest.objects.filter(
        order__event=request.event
    ).exists()

    ctx['timeline'] = [
        {
            'date': t.datetime.astimezone(request.event.timezone).date(),
            'entry': t,
            'time': t.datetime.astimezone(request.event.timezone)
        }
        for t in timeline_for_event(request.event, subevent)
    ]
    ctx['today'] = now().astimezone(request.event.timezone).date()
    ctx['nearly_now'] = now().astimezone(request.event.timezone) - timedelta(seconds=20)
    resp = render(request, 'pretixcontrol/event/index.html', ctx)
    # resp['Content-Security-Policy'] = "style-src 'unsafe-inline'"
    return resp


def event_index_widgets_lazy(request, organizer, event):
    subevent = None
    if request.GET.get("subevent", "") != "" and request.event.has_subevents:
        i = request.GET.get("subevent", "")
        try:
            subevent = request.event.subevents.get(pk=i)
        except SubEvent.DoesNotExist:
            pass

    widgets = []
    for r, result in event_dashboard_widgets.send(sender=request.event, subevent=subevent, lazy=False):
        widgets.extend(result)

    return JsonResponse({'widgets': widgets})


def event_index_log_lazy(request, organizer, event):
    qs = request.event.logentry_set.all().select_related('user', 'content_type', 'api_token', 'oauth_application',
                                                         'device').order_by('-datetime')
    qs = qs.exclude(action_type__in=OVERVIEW_BANLIST)

    can_view_orders = request.user.has_event_permission(request.organizer, request.event, 'can_view_orders',
                                                        request=request)
    can_change_event_settings = request.user.has_event_permission(request.organizer, request.event,
                                                                  'can_change_event_settings', request=request)
    can_view_vouchers = request.user.has_event_permission(request.organizer, request.event, 'can_view_vouchers',
                                                          request=request)

    if not can_view_orders:
        qs = qs.exclude(content_type=ContentType.objects.get_for_model(Order))
    if not can_view_vouchers:
        qs = qs.exclude(content_type=ContentType.objects.get_for_model(Voucher))
    if not can_change_event_settings:
        allowed_types = [
            ContentType.objects.get_for_model(Voucher),
            ContentType.objects.get_for_model(Order)
        ]
        if request.user.has_event_permission(request.organizer, request.event, 'can_change_items', request=request):
            allowed_types += [
                ContentType.objects.get_for_model(Item),
                ContentType.objects.get_for_model(ItemCategory),
                ContentType.objects.get_for_model(Quota),
                ContentType.objects.get_for_model(Question),
            ]
        qs = qs.filter(content_type__in=allowed_types)

    return render(
        request,
        'pretixcontrol/event/logs_embed.html',
        {
            'logs': qs[:5]
        }
    )


def annotated_event_query(request, lazy=False):
    active_orders = Order.objects.filter(
        event=OuterRef('pk'),
        status__in=[Order.STATUS_PENDING, Order.STATUS_PAID]
    ).order_by().values('event').annotate(
        c=Count('*')
    ).values(
        'c'
    )

    qs = request.user.get_events_with_any_permission(request)
    if not lazy:
        qs = qs.annotate(
            order_count=Subquery(active_orders, output_field=IntegerField()),
        )
    qs = qs.annotate(
        min_from=Min('subevents__date_from'),
        max_from=Max('subevents__date_from'),
        max_to=Max('subevents__date_to'),
        max_fromto=Greatest(Max('subevents__date_to'), Max('subevents__date_from')),
    ).annotate(
        order_to=Coalesce('max_fromto', 'max_to', 'max_from', 'date_to', 'date_from'),
    )
    return qs


def widgets_for_event_qs(request, qs, user, nmax, lazy=False):
    widgets = []

    # Get set of events where we have the permission to show the # of orders
    if not lazy:
        events_with_orders = set(qs.filter(
            Q(organizer_id__in=user.teams.filter(all_events=True, can_view_orders=True).values_list('organizer', flat=True))
            | Q(id__in=user.teams.filter(can_view_orders=True).values_list('limit_events__id', flat=True))
        ).values_list('id', flat=True))

    tpl = """
        <a href="{url}" class="event">
            <div class="name">{event}</div>
            <div class="daterange">{daterange}</div>
            <div class="times">{times}</div>
        </a>
        <div class="bottomrow">
            {orders}
            <a href="{url}" class="status-{statusclass}">
                {status}
            </a>
        </div>
    """

    if lazy:
        events = qs[:nmax]
    else:
        events = qs.prefetch_related(
            '_settings_objects', 'organizer___settings_objects'
        ).select_related('organizer')[:nmax]
    for event in events:
        if not lazy:
            tzname = event.cache.get_or_set('timezone', lambda: event.settings.timezone)
            tz = ZoneInfo(tzname)
            if event.has_subevents:
                if event.min_from is None:
                    dr = pgettext("subevent", "No dates")
                else:
                    dr = daterange(
                        (event.min_from).astimezone(tz),
                        (event.max_fromto or event.max_to or event.max_from).astimezone(tz)
                    )
            else:
                if event.date_to:
                    dr = daterange(event.date_from.astimezone(tz), event.date_to.astimezone(tz))
                else:
                    dr = date_format(event.date_from.astimezone(tz), "DATE_FORMAT")

            if not event.live:
                status = ('warning', _('Shop disabled'))
            elif event.presale_has_ended:
                status = ('default', _('Sale over'))
            elif not event.presale_is_running:
                status = ('default', _('Soon'))
            else:
                status = ('success', _('On sale'))

        widgets.append({
            'content': tpl.format(
                event=escape(event.name),
                times=_('Event series') if event.has_subevents else (
                    ((date_format(event.date_admission.astimezone(tz), 'TIME_FORMAT') + ' / ')
                     if event.date_admission and event.date_admission != event.date_from else '')
                    + (date_format(event.date_from.astimezone(tz), 'TIME_FORMAT') if event.date_from else '')
                ) + (
                    ' <span class="fa fa-globe text-muted" data-toggle="tooltip" title="{}"></span>'.format(tzname)
                    if tzname != request.timezone and not event.has_subevents else ''
                ),
                url=reverse('control:event.index', kwargs={
                    'event': event.slug,
                    'organizer': event.organizer.slug
                }),
                orders=(
                    '<a href="{orders_url}" class="orders">{orders_text}</a>'.format(
                        orders_url=reverse('control:event.orders', kwargs={
                            'event': event.slug,
                            'organizer': event.organizer.slug
                        }),
                        orders_text=ngettext('{num} order', '{num} orders', event.order_count or 0).format(
                            num=event.order_count or 0
                        )
                    ) if user.has_active_staff_session(request.session.session_key) or event.pk in events_with_orders else ''
                ),
                daterange=dr,
                status=status[1],
                statusclass=status[0],
            ) if not lazy else '',
            'display_size': 'small',
            'lazy': 'event-{}'.format(event.pk),
            'priority': 100,
            'container_class': 'widget-container widget-container-event',
        })
        """
            {% if not e.live %}
                <span class="label label-danger">{% trans "Shop disabled" %}</span>
            {% elif e.presale_has_ended %}
                <span class="label label-warning">{% trans "Presale over" %}</span>
            {% elif not e.presale_is_running %}
                <span class="label label-warning">{% trans "Presale not started" %}</span>
            {% else %}
                <span class="label label-success">{% trans "On sale" %}</span>
            {% endif %}
        """
    return widgets


def user_index_widgets_lazy(request):
    widgets = []
    widgets += widgets_for_event_qs(
        request,
        annotated_event_query(request).filter(
            Q(has_subevents=False) &
            Q(
                Q(Q(date_to__isnull=True) & Q(date_from__gte=now()))
                | Q(Q(date_to__isnull=False) & Q(date_to__gte=now()))
            )
        ).order_by('date_from', 'order_to', 'pk'),
        request.user,
        7
    )
    widgets += widgets_for_event_qs(
        request,
        annotated_event_query(request).filter(
            Q(has_subevents=False) &
            Q(
                Q(Q(date_to__isnull=True) & Q(date_from__lt=now()))
                | Q(Q(date_to__isnull=False) & Q(date_to__lt=now()))
            )
        ).order_by('-order_to', 'pk'),
        request.user,
        8
    )
    widgets += widgets_for_event_qs(
        request,
        annotated_event_query(request).filter(
            has_subevents=True
        ).order_by('-order_to', 'pk'),
        request.user,
        8
    )
    return JsonResponse({'widgets': widgets})


def user_index(request):
    widgets = []
    for r, result in user_dashboard_widgets.send(request, user=request.user):
        widgets.extend(result)

    ctx = {
        'widgets': rearrange(widgets),
        'can_create_event': request.user.teams.filter(can_create_events=True).exists(),
        'upcoming': widgets_for_event_qs(
            request,
            annotated_event_query(request, lazy=True).filter(
                Q(has_subevents=False) &
                Q(
                    Q(Q(date_to__isnull=True) & Q(date_from__gte=now()))
                    | Q(Q(date_to__isnull=False) & Q(date_to__gte=now()))
                )
            ).order_by('date_from', 'order_to', 'pk'),
            request.user,
            7,
            lazy=True
        ),
        'past': widgets_for_event_qs(
            request,
            annotated_event_query(request, lazy=True).filter(
                Q(has_subevents=False) &
                Q(
                    Q(Q(date_to__isnull=True) & Q(date_from__lt=now()))
                    | Q(Q(date_to__isnull=False) & Q(date_to__lt=now()))
                )
            ).order_by('-order_to', 'pk'),
            request.user,
            8,
            lazy=True
        ),
        'series': widgets_for_event_qs(
            request,
            annotated_event_query(request, lazy=True).filter(
                has_subevents=True
            ).order_by('-order_to', 'pk'),
            request.user,
            8,
            lazy=True
        ),
    }
    return render(request, 'pretixcontrol/dashboard.html', ctx)


def rearrange(widgets: list):
    """
    Sort widget boxes according to priority.
    """
    mapping = {
        'small': 1,
        'big': 2,
        'full': 3,
    }

    def sort_key(element):
        return (
            element.get('priority', 1),
            mapping.get(element.get('display_size', 'small'), 1),
        )

    return sorted(widgets, key=sort_key, reverse=True)
