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

import os
import shutil
import subprocess
from hashlib import md5
from tempfile import mkdtemp

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.core.mail import EmailMultiAlternatives
from django.db import transaction
from django.db.models import F, Q
from django.db.models.functions.text import Lower
from django.http import HttpResponse, JsonResponse, HttpResponseRedirect
from django.urls import reverse, reverse_lazy
from django.utils import timezone
from django.utils.decorators import method_decorator
from django.utils.translation import gettext_lazy as _
from django.template.loader import render_to_string
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 note_kfet.settings import BASE_DIR
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, EmailForm
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-"),
        lambda data: ActivityTable(data, prefix="search-"),
    ]
    extra_context = {"title": _("Activities")}

    def get_queryset(self, **kwargs):
        """
        Filter the user list with the given pattern.
        """
        return super().get_queryset().distinct()

    def get_tables_data(self):
        # first table = all activities, second table = upcoming, third table = search

        # table search
        qs = self.get_queryset().order_by('-date_start')
        if "search" in self.request.GET and self.request.GET['search']:
            pattern = self.request.GET['search']

            # check regex
            valid_regex = is_regex(pattern)
            suffix = '__iregex' if valid_regex else '__istartswith'
            prefix = '^' if valid_regex else ''
            qs = qs.filter(Q(**{f'name{suffix}': prefix + pattern})
                           | Q(**{f'organizer__name{suffix}': prefix + pattern})
                           | Q(**{f'organizer__note__alias__name{suffix}': prefix + pattern}))
        else:
            qs = qs.none()
        search_table = qs.filter(PermissionBackend.filter_queryset(self.request, Activity, 'view'))

        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"),
            search_table,
        ]

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

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

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

        entries_count = {}
        show_entries = {}
        for activity in started_activities:
            if activity.activity_type.manage_entries:
                entries = Entry.objects.filter(activity=activity)
                entries_count[activity] = entries.count()

                show_entries[activity] = True
        context["entries_count"] = entries_count
        context["show_entries"] = show_entries

        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")}
    export_formats = ["csv"]

    tables = [
        GuestTable,
        OpenerTable,
    ]

    def get_tables(self):
        tables = super().get_tables()
        tables[0].prefix = "guests"
        tables[1].prefix = "opener"
        return tables

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

    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": ""
            }
        }
        if self.object.activity_type.manage_entries:
            entries = Entry.objects.filter(activity=self.object)
            context["entries_count"] = {self.object: entries.count()}

            context["show_entries"] = {self.object: timezone.now() > timezone.localtime(self.object.date_start)}
        else:
            context["entries_count"] = {self.object: 0}
            context["show_entries"] = {self.object: False}

        guests = Guest.objects.filter(activity=self.object)
        guests_view = guests.filter(PermissionBackend.filter_queryset(self.request, Guest, "view"))
        if guests.exists() and guests.count() == guests_view.count():
            context["export"] = True
            context["email_form"] = EmailForm
        if 'mail' in self.request.GET:
            context["mail"] = self.request.GET['mail'].split(',')
        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 ActivityDeleteView(View):
    """
    Deletes an Activity
    """
    def delete(self, request, pk):
        try:
            activity = Activity.objects.get(pk=pk)
            activity.delete()
            return JsonResponse({"message": "Activity deleted"})
        except Activity.DoesNotExist:
            return JsonResponse({"error": "Activity not found"}, status=404)

    def dispatch(self, *args, **kwargs):
        """
        Don't display the delete button if the user has no right to delete.
        """
        if not self.request.user.is_authenticated:
            return self.handle_no_permission()

        activity = Activity.objects.get(pk=self.kwargs["pk"])
        if not PermissionBackend.check_perm(self.request, "activity.delete_activity", activity):
            raise PermissionDenied(_("You are not allowed to delete this activity."))

        if activity.valid:
            raise PermissionDenied(_("This activity is valid."))
        return super().dispatch(*args, **kwargs)


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="",
            school="",
            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")).distinct().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


class GuestListRenderView(LoginRequiredMixin, View):
    """
    Render a generated PDF with the given information and a LaTeX template
    """

    def get_queryset(self, **kwargs):
        qs = Guest.objects.filter(PermissionBackend.filter_queryset(self.request, Guest, "view"))
        qs = qs.filter(activity__pk=self.kwargs["activity_pk"]).order_by(
            Lower('last_name'),
            Lower('first_name'),
            'id',
        )

        return qs.distinct()

    def get(self, request, **kwargs):
        pdf = self.generate_pdf(request)
        return self.view_pdf(request, pdf)

    def post(self, request, **kwargs):
        recipients = []
        emails = request.POST['emails'].split(',')
        trust_address = os.getenv('TRUSTED_ACTIVITY_MAIL', '').split(',')
        for email_address in emails:
            if email_address in trust_address:
                recipients.append(email_address)
        # don't send email if no recipient
        if not recipients:
            raise PermissionDenied(_("Emails are not trusted!"))
        pdf = self.generate_pdf(request)
        self.send_pdf(request, recipients, pdf)
        url = reverse('activity:activity_detail', kwargs={"pk": self.kwargs["activity_pk"]})
        url += '?mail='
        for email in recipients:
            url += email + ','
        url = url[:-1]  # delete last comma
        return HttpResponseRedirect(url)

    def generate_pdf(self, request, **kwargs):
        qs = self.get_queryset()

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

        if not qs.exists() or qs.count() != Guest.objects.filter(activity=activity).count():
            raise PermissionDenied(_("You are not allowed to export the guest list for this activity."))

        # Fill the template with the information
        tex = render_to_string("activity/guestlist_sample.tex", dict(guests=qs.all(), activity=activity, total=qs.count()))

        try:
            os.mkdir(BASE_DIR + "/tmp")
        except FileExistsError:
            pass
        # We render the file in a temporary directory
        tmp_dir = mkdtemp(prefix=BASE_DIR + "/tmp/")

        try:
            with open("{}/guest-list.tex".format(tmp_dir), "wb") as f:
                f.write(tex.encode("UTF-8"))
            del tex

            with open(os.devnull, "wb") as devnull:
                error = subprocess.Popen(
                    ["/usr/bin/xelatex", "-interaction=nonstopmode", "{}/guest-list.tex".format(tmp_dir)],
                    cwd=tmp_dir,
                    stderr=devnull,
                    stdout=devnull,
                ).wait()

            if error:
                with open("{}/guest-list.log".format(tmp_dir), "r") as f:
                    log = f.read()
                raise IOError("An error attempted while generating a Guest list (code=" + str(error) + ")\n\n" + log)

            with open("{}/guest-list.pdf".format(tmp_dir), 'rb') as f:
                pdf = f.read()
            return pdf

        except IOError as e:
            raise e
        finally:
            # Delete all temporary files
            shutil.rmtree(tmp_dir)

    def view_pdf(self, request, pdf):
        response = HttpResponse(pdf, content_type="application/pdf")
        response['Content-Disposition'] = "inline;filename=Liste des invité·e·s.pdf"
        return response

    def send_pdf(self, request, recipients, pdf):
        user_identity = request.user.first_name.capitalize() + ' ' + request.user.last_name.upper()
        activity = Activity.objects.get(pk=self.kwargs["activity_pk"])
        subject = _(f"Guest list of the activity {activity.name} share by {user_identity}")
        # add the user in cc
        cc = [request.user.email]
        context = {'activity': activity, 'user_identity': user_identity}
        message = render_to_string("activity/guest_list.txt", context=context)
        html_message = render_to_string("activity/guest_list.html", context=context)
        if os.getenv('ACTIVITY_EMAIL_MANAGER', ''):
            cc.append(os.getenv('ACTIVITY_EMAIL_MANAGER'))
        email = EmailMultiAlternatives(
            subject=subject,
            to=recipients,
            cc=cc,
            body=message,
        )
        email.attach("Liste des invité·e·s.pdf", pdf)
        email.attach_alternative(html_message, "text/html")
        email.send()
        return


# 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")
