# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later

from hashlib import md5

from django.conf import settings
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import PermissionDenied
from django.db import transaction
from django.db.models import F, Q
from django.http import HttpResponse
from django.urls import reverse_lazy
from django.utils import timezone
from django.utils.decorators import method_decorator
from django.utils.translation import gettext_lazy as _
from django.views import View
from django.views.decorators.cache import cache_page
from django.views.generic import DetailView, TemplateView, UpdateView
from django.views.generic.list import ListView
from django_tables2.views import MultiTableMixin, SingleTableMixin
from api.viewsets import is_regex
from note.models import Alias, NoteSpecial, NoteUser
from permission.backends import PermissionBackend
from permission.views import ProtectQuerysetMixin, ProtectedCreateView

from .forms import ActivityForm, GuestForm
from .models import Activity, Entry, Guest, Opener
from .tables import ActivityTable, EntryTable, GuestTable, OpenerTable


class ActivityCreateView(ProtectQuerysetMixin, ProtectedCreateView):
    """
    View to create a new Activity
    """
    model = Activity
    form_class = ActivityForm
    extra_context = {"title": _("Create new activity")}

    def get_sample_object(self):
        return Activity(
            name="",
            description="",
            creater=self.request.user,
            activity_type_id=1,
            organizer_id=1,
            attendees_club_id=1,
            date_start=timezone.now(),
            date_end=timezone.now(),
        )

    @transaction.atomic
    def form_valid(self, form):
        form.instance.creater = self.request.user
        return super().form_valid(form)

    def get_success_url(self, **kwargs):
        self.object.refresh_from_db()
        return reverse_lazy('activity:activity_detail', kwargs={"pk": self.object.pk})


class ActivityListView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, ListView):
    """
    Displays all Activities, and classify if they are on-going or upcoming ones.
    """
    model = Activity
    tables = [
        lambda data: ActivityTable(data, prefix="all-"),
        lambda data: ActivityTable(data, prefix="upcoming-"),
    ]
    extra_context = {"title": _("Activities")}

    def get_queryset(self, **kwargs):
        return super().get_queryset(**kwargs).distinct()

    def get_tables_data(self):
        # first table = all activities, second table = upcoming
        return [
            self.get_queryset().order_by("-date_start"),
            Activity.objects.filter(date_end__gt=timezone.now())
                            .filter(PermissionBackend.filter_queryset(self.request, Activity, "view"))
                            .distinct()
                            .order_by("date_start")
        ]

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)

        tables = context["tables"]
        for name, table in zip(["table", "upcoming"], tables):
            context[name] = table

        started_activities = self.get_queryset().filter(open=True, valid=True).distinct().all()
        context["started_activities"] = started_activities

        return context


class ActivityDetailView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, DetailView):
    """
    Shows details about one activity. Add guest to context
    """
    model = Activity
    context_object_name = "activity"
    extra_context = {"title": _("Activity detail")}

    tables = [
        lambda data: GuestTable(data, prefix="guests-"),
        lambda data: OpenerTable(data, prefix="opener-"),
    ]

    def get_tables_data(self):
        return [
            Guest.objects.filter(activity=self.object)
                         .filter(PermissionBackend.filter_queryset(self.request, Guest, "view")),
            self.object.opener.filter(activity=self.object)
                              .filter(PermissionBackend.filter_queryset(self.request, Opener, "view")),
        ]

    def get_context_data(self, **kwargs):
        context = super().get_context_data()

        tables = context["tables"]
        for name, table in zip(["guests", "opener"], tables):
            context[name] = table

        context["activity_started"] = timezone.now() > timezone.localtime(self.object.date_start)

        context["widget"] = {
            "name": "opener",
            "resetable": True,
            "attrs": {
                "class": "autocomplete form-control",
                "id": "opener",
                "api_url": "/api/note/alias/?note__polymorphic_ctype__model=noteuser",
                "name_field": "name",
                "placeholder": ""
            }
        }

        return context


class ActivityUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
    """
    Updates one Activity
    """
    model = Activity
    form_class = ActivityForm
    extra_context = {"title": _("Update activity")}

    def get_success_url(self, **kwargs):
        return reverse_lazy('activity:activity_detail', kwargs={"pk": self.kwargs["pk"]})


class ActivityInviteView(ProtectQuerysetMixin, ProtectedCreateView):
    """
    Invite a Guest, The rules to invites someone are defined in `forms:activity.GuestForm`
    """
    model = Guest
    form_class = GuestForm
    template_name = "activity/activity_form.html"

    def get_sample_object(self):
        """ Creates a standart Guest binds to the Activity"""
        activity = Activity.objects.get(pk=self.kwargs["pk"])
        return Guest(
            activity=activity,
            first_name="",
            last_name="",
            inviter=self.request.user.note,
        )

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        activity = context["form"].activity
        context["title"] = _('Invite guest to the activity "{}"').format(activity.name)
        return context

    def get_form(self, form_class=None):
        form = super().get_form(form_class)
        form.activity = Activity.objects.filter(PermissionBackend.filter_queryset(self.request, Activity, "view"))\
            .filter(pk=self.kwargs["pk"]).first()
        form.fields["inviter"].initial = self.request.user.note
        return form

    @transaction.atomic
    def form_valid(self, form):
        form.instance.activity = Activity.objects\
            .filter(PermissionBackend.filter_queryset(self.request, Activity, "view")).get(pk=self.kwargs["pk"])
        return super().form_valid(form)

    def get_success_url(self, **kwargs):
        return reverse_lazy('activity:activity_detail', kwargs={"pk": self.kwargs["pk"]})


class ActivityEntryView(LoginRequiredMixin, SingleTableMixin, TemplateView):
    """
    Manages entry to an activity
    """
    template_name = "activity/activity_entry.html"

    table_class = EntryTable

    def dispatch(self, request, *args, **kwargs):
        """
        Don't display the entry interface if the user has no right to see it (no right to add an entry for itself),
        it is closed or doesn't manage entries.
        """
        if not self.request.user.is_authenticated:
            return self.handle_no_permission()

        activity = Activity.objects.get(pk=self.kwargs["pk"])

        sample_entry = Entry(activity=activity, note=self.request.user.note)
        if not PermissionBackend.check_perm(self.request, "activity.add_entry", sample_entry):
            raise PermissionDenied(_("You are not allowed to display the entry interface for this activity."))

        if not activity.activity_type.manage_entries:
            raise PermissionDenied(_("This activity does not support activity entries."))

        if not activity.open:
            raise PermissionDenied(_("This activity is closed."))
        return super().dispatch(request, *args, **kwargs)

    def get_invited_guest(self, activity):
        """
        Retrieves all Guests to the activity
        """

        guest_qs = Guest.objects\
            .annotate(balance=F("inviter__balance"), note_name=F("inviter__user__username"))\
            .filter(activity=activity)\
            .filter(PermissionBackend.filter_queryset(self.request, Guest, "view"))\
            .order_by('last_name', 'first_name')

        if "search" in self.request.GET and self.request.GET["search"]:
            pattern = self.request.GET["search"]

            # Check if this is a valid regex. If not, we won't check regex
            valid_regex = is_regex(pattern)
            suffix = "__iregex" if valid_regex else "__istartswith"
            pattern = "^" + pattern if valid_regex and pattern[0] != "^" else pattern
            guest_qs = guest_qs.filter(
                Q(**{f"first_name{suffix}": pattern})
                | Q(**{f"last_name{suffix}": pattern})
                | Q(**{f"inviter__alias__name{suffix}": pattern})
                | Q(**{f"inviter__alias__normalized_name{suffix}": Alias.normalize(pattern)})
            )
        else:
            guest_qs = guest_qs.none()
        return guest_qs.distinct()

    def get_invited_note(self, activity):
        """
        Retrieves all Note that can attend the activity,
        they need to have an up-to-date membership in the attendees_club.
        """
        note_qs = Alias.objects.annotate(last_name=F("note__noteuser__user__last_name"),
                                         first_name=F("note__noteuser__user__first_name"),
                                         username=F("note__noteuser__user__username"),
                                         note_name=F("name"),
                                         balance=F("note__balance"))

        # Keep only users that have a note
        note_qs = note_qs.filter(note__noteuser__isnull=False)

        # Keep only valid members
        note_qs = note_qs.filter(
            note__noteuser__user__memberships__club=activity.attendees_club,
            note__noteuser__user__memberships__date_start__lte=timezone.now(),
            note__noteuser__user__memberships__date_end__gte=timezone.now()).exclude(note__inactivity_reason='forced')

        # Filter with permission backend
        note_qs = note_qs.filter(PermissionBackend.filter_queryset(self.request, Alias, "view"))

        if "search" in self.request.GET and self.request.GET["search"]:
            pattern = self.request.GET["search"]

            # Check if this is a valid regex. If not, we won't check regex
            valid_regex = is_regex(pattern)
            suffix = "__iregex" if valid_regex else "__icontains"
            note_qs = note_qs.filter(
                Q(**{f"note__noteuser__user__first_name{suffix}": pattern})
                | Q(**{f"note__noteuser__user__last_name{suffix}": pattern})
                | Q(**{f"name{suffix}": pattern})
                | Q(**{f"normalized_name{suffix}": Alias.normalize(pattern)})
            )
        else:
            note_qs = note_qs.none()

        # SQLite doesn't support distinct fields. For compatibility reason (in dev mode), the note list will only
        # have distinct aliases rather than distinct notes with a SQLite DB, but it can fill the result page.
        # In production mode, please use PostgreSQL.
        note_qs = note_qs.distinct('note__pk')[:20]\
            if settings.DATABASES[note_qs.db]["ENGINE"] == 'django.db.backends.postgresql' else note_qs.distinct()[:20]
        return note_qs

    def get_table_data(self):
        activity = Activity.objects.filter(PermissionBackend.filter_queryset(self.request, Activity, "view"))\
            .distinct().get(pk=self.kwargs["pk"])

        matched = []

        for guest in self.get_invited_guest(activity):
            guest.type = "Invité"
            matched.append(guest)

        for note in self.get_invited_note(activity):
            note.type = "Adhérent"
            note.activity = activity
            matched.append(note)

        return matched

    def get_context_data(self, **kwargs):
        """
        Query the list of Guest and Note to the activity and add information to makes entry with JS.
        """
        context = super().get_context_data(**kwargs)

        activity = Activity.objects.filter(PermissionBackend.filter_queryset(self.request, Activity, "view"))\
            .distinct().get(pk=self.kwargs["pk"])
        context["activity"] = activity

        context["entries"] = Entry.objects.filter(activity=activity)

        context["title"] = _('Entry for activity "{}"').format(activity.name)
        context["noteuser_ctype"] = ContentType.objects.get_for_model(NoteUser).pk
        context["notespecial_ctype"] = ContentType.objects.get_for_model(NoteSpecial).pk

        activities_open = Activity.objects.filter(open=True, activity_type__manage_entries=True).filter(
            PermissionBackend.filter_queryset(self.request, Activity, "view")).distinct().all()
        context["activities_open"] = [a for a in activities_open
                                      if PermissionBackend.check_perm(self.request,
                                                                      "activity.add_entry",
                                                                      Entry(activity=a, note=self.request.user.note,))]

        return context


# Cache for 1 hour
@method_decorator(cache_page(60 * 60), name='dispatch')
class CalendarView(View):
    """
    Render an ICS calendar with all valid activities.
    """

    def multilines(self, string, maxlength, offset=0):
        newstring = string[:maxlength - offset]
        string = string[maxlength - offset:]
        while string:
            newstring += "\r\n "
            newstring += string[:maxlength - 1]
            string = string[maxlength - 1:]
        return newstring

    def get(self, request, *args, **kwargs):
        ics = """BEGIN:VCALENDAR
VERSION: 2.0
PRODID:Note Kfet 2020
X-WR-CALNAME:Kfet Calendar
NAME:Kfet Calendar
CALSCALE:GREGORIAN
BEGIN:VTIMEZONE
TZID:Europe/Paris
X-LIC-LOCATION:Europe/Paris
BEGIN:DAYLIGHT
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
TZNAME:CEST
DTSTART:19700329T020000
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
END:DAYLIGHT
BEGIN:STANDARD
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
TZNAME:CET
DTSTART:19701025T030000
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
END:STANDARD
END:VTIMEZONE
"""
        for activity in Activity.objects.filter(valid=True).order_by("-date_start").all():
            ics += f"""BEGIN:VEVENT
DTSTAMP:{"{:%Y%m%dT%H%M%S}".format(activity.date_start)}Z
UID:{md5((activity.name + "$" + str(activity.id) + str(activity.date_start)).encode("UTF-8")).hexdigest()}
SUMMARY;CHARSET=UTF-8:{self.multilines(activity.name, 75, 22)}
DTSTART:{"{:%Y%m%dT%H%M%S}Z".format(activity.date_start)}
DTEND:{"{:%Y%m%dT%H%M%S}Z".format(activity.date_end)}
LOCATION:{self.multilines(activity.location, 75, 9) if activity.location else "Kfet"}
DESCRIPTION;CHARSET=UTF-8:""" + self.multilines(activity.description.replace("\n", "\\n"), 75, 26) + f"""
 -- {activity.organizer.name}
END:VEVENT
"""
        ics += "END:VCALENDAR"
        ics = ics.replace("\r", "").replace("\n", "\r\n")
        return HttpResponse(ics, content_type="text/calendar; charset=UTF-8")