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

from datetime import timedelta, date

from django.conf import settings
from django.contrib.auth import logout
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.models import User
from django.contrib.auth.views import LoginView
from django.db import transaction
from django.db.models import Q, F
from django.shortcuts import redirect
from django.urls import reverse_lazy
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django.views.generic import DetailView, UpdateView, TemplateView
from django.views.generic.edit import FormMixin
from django_tables2.views import MultiTableMixin, SingleTableMixin, SingleTableView
from django_tables2.export.views import ExportMixin
from rest_framework.authtoken.models import Token
from api.viewsets import is_regex
from note.models import Alias, NoteClub, NoteUser, Trust
from note.models.transactions import Transaction, SpecialTransaction
from note.tables import HistoryTable, AliasTable, TrustTable, TrustedTable
from note_kfet.middlewares import _set_current_request
from permission.backends import PermissionBackend
from permission.models import Role
from permission.views import ProtectQuerysetMixin, ProtectedCreateView
from family.models import Family
from django import forms

from .forms import UserForm, ProfileForm, ImageForm, ClubForm, MembershipForm, \
    CustomAuthenticationForm, MembershipRolesForm
from .models import Club, Membership
from .tables import ClubTable, UserTable, MembershipTable, ClubManagerTable


class CustomLoginView(LoginView):
    """
    Login view, where the user can select its permission mask.
    """
    form_class = CustomAuthenticationForm

    @transaction.atomic
    def form_valid(self, form):
        logout(self.request)
        self.request.user = form.get_user()
        _set_current_request(self.request)
        self.request.session['permission_mask'] = form.cleaned_data['permission_mask'].rank
        return super().form_valid(form)

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        user_agent = self.request.META.get('HTTP_USER_AGENT', '').lower()

        context['display_appstore_badge'] = 'iphone' in user_agent or 'android' not in user_agent
        context['display_playstore_badge'] = 'android' in user_agent or 'iphone' not in user_agent

        return context


class UserUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
    """
    Update the user information.
    On this view both `:models:member.User` and `:models:member.Profile` are updated through forms
    """
    model = User
    form_class = UserForm
    template_name = 'member/profile_update.html'
    context_object_name = 'user_object'
    extra_context = {"title": _("Update Profile")}

    profile_form = ProfileForm

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

        form = context['form']
        form.fields['username'].widget.attrs.pop("autofocus", None)
        form.fields['first_name'].widget.attrs.update({"autofocus": "autofocus"})
        form.fields['first_name'].required = True
        form.fields['last_name'].required = True
        form.fields['email'].required = True
        form.fields['email'].help_text = _("This address must be valid.")

        profile_form = self.profile_form(instance=context['user_object'].profile,
                                         data=self.request.POST if self.request.POST else None)

        if not self.object.profile.report_frequency:
            del profile_form.fields["last_report"]

        fields_to_check = list(profile_form.fields.keys())
        fields_modifiable = False

        # Delete the fields for which the user does not have the permission to modify
        for field_name in fields_to_check:
            if not PermissionBackend.check_perm(self.request, f"member.change_profile_{field_name}", context['user_object'].profile):
                profile_form.fields[field_name].widget = forms.HiddenInput()
            else:
                fields_modifiable = True

        if fields_modifiable:
            context['profile_form'] = profile_form

        return context

    @transaction.atomic
    def form_valid(self, form):
        """
        Check if ProfileForm is correct
        then check if username is not already taken by someone else or by the user,
        then check if email has changed, and if so ask for new validation.
        """

        profile_form = ProfileForm(
            data=self.request.POST,
            instance=self.object.profile,
        )
        profile_form.full_clean()
        if not profile_form.is_valid():
            return super().form_invalid(form)
        new_username = form.data['username']
        # Check if the new username is not already taken as an alias of someone else.
        note = NoteUser.objects.filter(
            alias__normalized_name=Alias.normalize(new_username))
        if note.exists() and note.get().user != self.object:
            form.add_error('username', _("An alias with a similar name already exists."))
            return super().form_invalid(form)
        # Check if the username is one of user's aliases.
        alias = Alias.objects.filter(name=new_username)
        if not alias.exists():
            similar = Alias.objects.filter(
                normalized_name=Alias.normalize(new_username))
            if similar.exists():
                similar.delete()
        olduser = User.objects.get(pk=form.instance.pk)

        user = form.save(commit=False)

        if olduser.email != user.email:
            # If the user changed her/his email, then it is unvalidated and a confirmation link is sent.
            user.profile.email_confirmed = False
            user.profile.send_email_validation_link()

        profile = profile_form.save(commit=False)
        profile.user = user
        profile.save()
        user.save()

        return super().form_valid(form)

    def get_success_url(self, **kwargs):
        url = 'member:user_detail' if self.object.profile.registration_valid else 'registration:future_user_detail'
        return reverse_lazy(url, args=(self.object.id,))


class UserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
    """
    Display all information about a user.
    """
    model = User
    context_object_name = "user_object"
    template_name = "member/profile_detail.html"
    extra_context = {"title": _("Profile detail")}

    def get_queryset(self, **kwargs):
        """
        We can't display information of a not registered user.
        """
        return super().get_queryset(**kwargs).filter(profile__registration_valid=True)

    def get_context_data(self, **kwargs):
        """
        Add history of transaction and list of membership of user.
        """
        context = super().get_context_data(**kwargs)
        user = context['user_object']
        context["note"] = user.note
        history_list = \
            Transaction.objects.all().filter(Q(source=user.note) | Q(destination=user.note))\
            .order_by("-created_at")\
            .filter(PermissionBackend.filter_queryset(self.request, Transaction, "view"))
        history_table = HistoryTable(history_list, prefix='transaction-')
        history_table.paginate(per_page=20, page=self.request.GET.get("transaction-page", 1))
        context['history_list'] = history_table

        club_list = Membership.objects.filter(user=user, date_end__gte=date.today() - timedelta(days=15))\
            .filter(PermissionBackend.filter_queryset(self.request, Membership, "view"))\
            .order_by("club__name", "-date_start")
        # Display only the most recent membership
        club_list = club_list.distinct("club__name")\
            if settings.DATABASES["default"]["ENGINE"] == 'django.db.backends.postgresql' else club_list
        membership_table = MembershipTable(data=club_list, prefix='membership-')
        membership_table.paginate(per_page=10, page=self.request.GET.get("membership-page", 1))
        context['club_list'] = membership_table

        # Check permissions to see if the authenticated user can lock/unlock the note
        with transaction.atomic():
            modified_note = NoteUser.objects.get(pk=user.note.pk)
            # Don't log these tests
            modified_note._no_signal = True
            modified_note.is_active = False
            modified_note.inactivity_reason = 'manual'
            context["can_lock_note"] = user.note.is_active and PermissionBackend\
                                           .check_perm(self.request, "note.change_noteuser_is_active", modified_note)
            old_note = NoteUser.objects.select_for_update().get(pk=user.note.pk)
            modified_note.inactivity_reason = 'forced'
            modified_note._force_save = True
            modified_note.save()
            context["can_force_lock"] = user.note.is_active and PermissionBackend\
                .check_perm(self.request, "note.change_noteuser_is_active", modified_note)
            old_note._force_save = True
            old_note._no_signal = True
            old_note.save()
            modified_note.refresh_from_db()
            modified_note.is_active = True
            context["can_unlock_note"] = not user.note.is_active and PermissionBackend\
                .check_perm(self.request, "note.change_noteuser_is_active", modified_note)
        if 'family' in settings.INSTALLED_APPS:
            context["family_app_installed"] = True
            families = Family.objects.filter(memberships__user=user).distinct()
            context["families"] = families

        return context


class UserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
    """
    Display user list, with a search bar
    """
    model = User
    table_class = UserTable
    template_name = 'member/user_list.html'
    extra_context = {"title": _("Search user")}

    def get_queryset(self, **kwargs):
        """
        Filter the user list with the given pattern.
        """
        qs = super().get_queryset().annotate(alias=F("note__alias__name"))\
            .annotate(normalized_alias=F("note__alias__normalized_name"))\
            .filter(profile__registration_valid=True)

        # Sqlite doesn't support order by in subqueries
        qs = qs.order_by("username").distinct("username")\
            if settings.DATABASES[qs.db]["ENGINE"] == 'django.db.backends.postgresql' else qs.distinct()

        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"
            prefix = "^" if valid_regex else ""
            qs = qs.filter(
                Q(**{f"username{suffix}": prefix + pattern})
            ).union(
                qs.filter(
                    (Q(**{f"alias{suffix}": prefix + pattern})
                     | Q(**{f"normalized_alias{suffix}": prefix + Alias.normalize(pattern)})
                     | Q(**{f"last_name{suffix}": prefix + pattern})
                     | Q(**{f"first_name{suffix}": prefix + pattern})
                     | Q(email__istartswith=pattern))
                    & ~Q(**{f"username{suffix}": prefix + pattern})
                ), all=True)
        else:
            qs = qs.none()

        return qs

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        pre_registered_users = User.objects.filter(PermissionBackend.filter_queryset(self.request, User, "view"))\
            .filter(profile__registration_valid=False)
        context["can_manage_registrations"] = pre_registered_users.exists()
        return context


class ProfileTrustView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, DetailView):
    """
    View and manage user trust relationships
    """
    model = User
    template_name = 'member/profile_trust.html'
    context_object_name = 'user_object'
    extra_context = {"title": _("Note friendships")}

    tables = [
        lambda data: TrustTable(data, prefix="trust-"),
        lambda data: TrustedTable(data, prefix="trusted-"),
    ]

    def get_tables_data(self):
        note = self.object.note
        return [
            note.trusting.filter(PermissionBackend.filter_queryset(self.request, Trust, "view")).distinct(),
            note.trusted.filter(PermissionBackend.filter_queryset(self.request, Trust, "view")).distinct(),
        ]

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

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

        context["can_create"] = PermissionBackend.check_perm(self.request, "note.add_trust", Trust(
            trusting=context["object"].note,
            trusted=context["object"].note
        ))
        context["widget"] = {
            "name": "trusted",
            "resetable": True,
            "attrs": {
                "class": "autocomplete form-control",
                "id": "trusted",
                "api_url": "/api/note/alias/?note__polymorphic_ctype__model=noteuser",
                "name_field": "name",
                "placeholder": ""
            }
        }
        return context


class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableMixin, DetailView):
    """
    View and manage user aliases.
    """
    model = User
    template_name = 'member/profile_alias.html'
    context_object_name = 'user_object'
    extra_context = {"title": _("Note aliases")}

    table_class = AliasTable
    context_table_name = "aliases"

    def get_table_data(self):
        return self.object.note.alias.filter(PermissionBackend.filter_queryset(self.request, Alias, "view")).distinct() \
                                     .order_by('normalized_name')

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context["can_create"] = PermissionBackend.check_perm(self.request, "note.add_alias", Alias(
            note=context["object"].note,
            name="",
            normalized_name="",
        ))
        return context


class PictureUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin, DetailView):
    """
    Update profile picture of the user note.
    """
    form_class = ImageForm
    extra_context = {"title": _("Update note picture")}

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['form'] = self.form_class(self.request.POST, self.request.FILES)
        return context

    def get_success_url(self):
        """Redirect to profile page after upload"""
        return reverse_lazy('member:user_detail', kwargs={'pk': self.object.id})

    def post(self, request, *args, **kwargs):
        form = self.get_form()
        self.object = self.get_object()
        return self.form_valid(form) if form.is_valid() else self.form_invalid(form)

    @transaction.atomic
    def form_valid(self, form):
        """Save image to note"""
        image = form.cleaned_data['image']

        if image is None:
            image = "pic/default.png"
        else:
            # Rename as a PNG or GIF
            extension = image.name.split(".")[-1]
            if extension == "gif":
                image.name = "{}_pic.gif".format(self.object.note.pk)
            else:
                image.name = "{}_pic.png".format(self.object.note.pk)

        # Save
        self.object.note.display_image = image
        self.object.note.save()
        return super().form_valid(form)


class ProfilePictureUpdateView(PictureUpdateView):
    model = User
    template_name = 'member/picture_update.html'
    context_object_name = 'user_object'


class ManageAuthTokens(LoginRequiredMixin, TemplateView):
    """
    Affiche le jeton d'authentification, et permet de le regénérer
    """
    model = Token
    template_name = "member/manage_auth_tokens.html"
    extra_context = {"title": _("Manage auth token")}

    def get(self, request, *args, **kwargs):
        if 'regenerate' in request.GET and Token.objects.filter(user=request.user).exists():
            Token.objects.get(user=self.request.user).delete()
            return redirect(reverse_lazy('member:auth_token') + "?show")

        return super().get(request, *args, **kwargs)

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['token'] = Token.objects.get_or_create(user=self.request.user)[0]
        return context


# ******************************* #
#              CLUB               #
# ******************************* #


class ClubCreateView(ProtectQuerysetMixin, ProtectedCreateView):
    """
    Create Club
    """
    model = Club
    form_class = ClubForm
    success_url = reverse_lazy('member:club_list')
    extra_context = {"title": _("Create new club")}

    def get_sample_object(self):
        return Club(
            name="",
            email="",
        )

    def get_success_url(self):
        self.object.refresh_from_db()
        return reverse_lazy("member:club_detail", kwargs={"pk": self.object.pk})


class ClubListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
    """
    List existing Clubs
    """
    model = Club
    table_class = ClubTable
    extra_context = {"title": _("Search club")}

    def get_queryset(self, **kwargs):
        """
        Filter the user list with the given pattern.
        """
        qs = super().get_queryset().distinct()
        if "search" in self.request.GET:
            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"
            prefix = "^" if valid_regex else ""

            qs = qs.filter(
                Q(**{f"name{suffix}": prefix + pattern})
                | Q(**{f"note__alias__name{suffix}": prefix + pattern})
                | Q(**{f"note__alias__normalized_name{suffix}": prefix + Alias.normalize(pattern)})
            )

        return qs

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context["can_add_club"] = PermissionBackend.check_perm(self.request, "member.add_club", Club(
            name="",
            email="club@example.com",
        ))
        return context


class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
    """
    Display details of a club
    """
    model = Club
    context_object_name = "club"
    extra_context = {"title": _("Club detail")}

    def get_context_data(self, **kwargs):
        """
        Add list of managers (peoples with Permission/Roles in this club), history of transactions and members list
        """
        context = super().get_context_data(**kwargs)

        club = self.object
        context["note"] = club.note

        if PermissionBackend.check_perm(self.request, "member.change_club_membership_start", club):
            club.update_membership_dates()

        # managers list
        managers = Membership.objects.filter(club=self.object, roles__name="Bureau de club",
                                             date_start__lte=date.today(), date_end__gte=date.today())\
            .order_by('user__last_name').all()
        context["managers"] = ClubManagerTable(data=managers, prefix="managers-")
        # transaction history
        club_transactions = Transaction.objects.all().filter(Q(source=club.note) | Q(destination=club.note))\
            .filter(PermissionBackend.filter_queryset(self.request, Transaction, "view"))\
            .order_by('-created_at')
        history_table = HistoryTable(club_transactions, prefix="history-")
        history_table.paginate(per_page=20, page=self.request.GET.get('history-page', 1))
        context['history_list'] = history_table
        # member list
        club_member = Membership.objects.filter(
            club=club,
            date_end__gte=date.today() - timedelta(days=15),
        ).filter(PermissionBackend.filter_queryset(self.request, Membership, "view"))\
            .order_by("user__username", "-date_start")
        # Display only the most recent membership
        club_member = club_member.distinct("user__username")\
            if settings.DATABASES["default"]["ENGINE"] == 'django.db.backends.postgresql' else club_member

        membership_table = MembershipTable(data=club_member, prefix="membership-")
        membership_table.paginate(per_page=5, page=self.request.GET.get('membership-page', 1))
        context['member_list'] = membership_table

        # Check if the user has the right to create a membership, to display the button.
        empty_membership = Membership(
            club=club,
            user=User.objects.first(),
            date_start=date.today(),
            date_end=date.today(),
            fee=0,
        )
        context["can_add_members"] = PermissionBackend()\
            .has_perm(self.request.user, "member.add_membership", empty_membership)

        # Check permissions to see if the authenticated user can lock/unlock the note
        with transaction.atomic():
            modified_note = NoteClub.objects.get(pk=club.note.pk)
            # Don't log these tests
            modified_note._no_signal = True
            modified_note.is_active = False
            modified_note.inactivity_reason = 'manual'
            context["can_lock_note"] = club.note.is_active and PermissionBackend \
                .check_perm(self.request, "note.change_noteclub_is_active", modified_note)
            old_note = NoteClub.objects.select_for_update().get(pk=club.note.pk)
            modified_note.inactivity_reason = 'forced'
            modified_note._force_save = True
            modified_note.save()
            context["can_force_lock"] = club.note.is_active and PermissionBackend \
                .check_perm(self.request, "note.change_noteclub_is_active", modified_note)
            old_note._force_save = True
            old_note._no_signal = True
            old_note.save()
            modified_note.refresh_from_db()
            modified_note.is_active = True
            context["can_unlock_note"] = not club.note.is_active and PermissionBackend \
                .check_perm(self.request, "note.change_noteclub_is_active", modified_note)

        return context


class ClubAliasView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableMixin, DetailView):
    """
    Manage aliases of a club.
    """
    model = Club
    template_name = 'member/club_alias.html'
    context_object_name = 'club'
    extra_context = {"title": _("Note aliases")}

    table_class = AliasTable
    context_table_name = "aliases"

    def get_table_data(self):
        return self.object.note.alias.filter(
            PermissionBackend.filter_queryset(self.request, Alias, "view")).distinct()

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

        context["can_create"] = PermissionBackend.check_perm(self.request, "note.add_alias", Alias(
            note=context["object"].note,
            name="",
            normalized_name="",
        ))
        return context


class ClubUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
    """
    Update the information of a club.
    """
    model = Club
    context_object_name = "club"
    form_class = ClubForm
    template_name = "member/club_form.html"
    extra_context = {"title": _("Update club")}

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

        # Don't update a WEI club through this view
        if "wei" in settings.INSTALLED_APPS:
            qs = qs.filter(weiclub=None)

        return qs

    def get_success_url(self):
        return reverse_lazy("member:club_detail", kwargs={"pk": self.object.pk})


class ClubPictureUpdateView(PictureUpdateView):
    """
    Update the profile picture of a club.
    """
    model = Club
    template_name = 'member/picture_update.html'
    context_object_name = 'club'

    def get_success_url(self):
        return reverse_lazy('member:club_detail', kwargs={'pk': self.object.id})


class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
    """
    Add a membership to a club.
    """
    model = Membership
    form_class = MembershipForm
    template_name = 'member/add_members.html'
    extra_context = {"title": _("Add new member to the club")}

    def get_sample_object(self):
        if "club_pk" in self.kwargs:
            club = Club.objects.get(pk=self.kwargs["club_pk"])
        else:
            club = Membership.objects.get(pk=self.kwargs["pk"]).club
        return Membership(
            user=self.request.user,
            club=club,
            fee=0,
            date_start=timezone.now(),
            date_end=timezone.now() + timedelta(days=1),
        )

    def get_context_data(self, **kwargs):
        """
        Membership can be created, or renewed
        In case of creation the url is /club/<club_pk>/add_member
        For a renewal it will be `club/renew_membership/<pk>`
        """
        context = super().get_context_data(**kwargs)
        form = context['form']

        if "club_pk" in self.kwargs:  # We create a new membership.
            club = Club.objects.filter(PermissionBackend.filter_queryset(self.request, Club, "view"))\
                .get(pk=self.kwargs["club_pk"], weiclub=None)
            form.fields['credit_amount'].initial = club.membership_fee_paid
            # Ensure that the user is member of the parent club and all its the family tree.
            c = club
            clubs_renewal = []
            additional_fee_renewal = 0
            while c.parent_club is not None:
                c = c.parent_club
                clubs_renewal.append(c)
                additional_fee_renewal += c.membership_fee_paid
            context["clubs_renewal"] = clubs_renewal
            context["additional_fee_renewal"] = additional_fee_renewal

            # If the concerned club is the BDE, then we add the option that Société générale pays the membership.
            if club.name != "BDE":
                del form.fields['soge']
            else:
                fee = 0
                bde = Club.objects.get(name="BDE")
                fee += bde.membership_fee_paid
                kfet = Club.objects.get(name="Kfet")
                fee += kfet.membership_fee_paid
                context["total_fee"] = "{:.02f}".format(fee / 100, )
        else:  # This is a renewal. Fields can be pre-completed.
            context["renewal"] = True

            old_membership = self.get_queryset().get(pk=self.kwargs["pk"])
            club = old_membership.club
            user = old_membership.user

            c = club
            clubs_renewal = []
            additional_fee_renewal = 0
            while c.parent_club is not None:
                c = c.parent_club
                # check if a valid membership exists for the parent club
                if c.membership_start and not Membership.objects.filter(
                        club=c,
                        user=user,
                        date_start__gte=c.membership_start,
                ).exists():
                    clubs_renewal.append(c)
                    additional_fee_renewal += c.membership_fee_paid if user.profile.paid else c.membership_fee_unpaid
            context["clubs_renewal"] = clubs_renewal
            context["additional_fee_renewal"] = additional_fee_renewal

            form.fields['user'].initial = user
            form.fields['user'].disabled = True
            form.fields['date_start'].initial = old_membership.date_end + timedelta(days=1)
            form.fields['credit_amount'].initial = (club.membership_fee_paid if user.profile.paid
                                                    else club.membership_fee_unpaid) + additional_fee_renewal
            form.fields['last_name'].initial = user.last_name
            form.fields['first_name'].initial = user.first_name

            # If this is a renewal of a BDE membership, Société générale can pays, if it has not been already done.
            if (club.name != "BDE" and club.name != "Kfet") or user.profile.soge:
                del form.fields['soge']
            else:
                fee = 0
                bde = Club.objects.get(name="BDE")
                if not Membership.objects.filter(
                    club=bde,
                    user=user,
                    date_start__gte=bde.membership_start,
                ).exists():
                    fee += bde.membership_fee_paid if user.profile.paid else bde.membership_fee_unpaid
                kfet = Club.objects.get(name="Kfet")
                if not Membership.objects.filter(
                    club=kfet,
                    user=user,
                    date_start__gte=bde.membership_start,
                ).exists():
                    fee += kfet.membership_fee_paid if user.profile.paid else kfet.membership_fee_unpaid
                context["total_fee"] = "{:.02f}".format(fee / 100, )

        context['club'] = club

        return context

    def perform_verifications(self, form, user, club, fee):
        """
        Make some additional verifications to check that the membership can be created.
        :return: True if the form is clean, False if there is an error.
        """
        error = False

        # Retrieve form data
        credit_type = form.cleaned_data["credit_type"]
        credit_amount = form.cleaned_data["credit_amount"]
        soge = form.cleaned_data["soge"] and not user.profile.soge and (club.name == "BDE" or club.name == "Kfet")

        if not credit_type:
            credit_amount = 0

        if not soge and user.note.balance + credit_amount < fee and not Membership.objects.filter(
                club__name="Kfet",
                user=user,
                date_start__lte=date.today(),
                date_end__gte=date.today(),
        ).exists():
            # Users without a valid Kfet membership can't have a negative balance.
            # TODO Send a notification to the user (with a mail?) to tell her/him to credit her/his note
            form.add_error('user',
                           _("This user don't have enough money to join this club, and can't have a negative balance."))
            error = True

        if Membership.objects.filter(
                user=form.instance.user,
                club=club,
                date_start__lte=form.instance.date_start,
                date_end__gte=form.instance.date_start,
        ).exists():
            form.add_error('user', _('User is already a member of the club'))
            error = True

        # Must join the parent club before joining this club, except for the Kfet club where it can be at the same time.
        if club.name != "Kfet" and club.parent_club and not Membership.objects.filter(
                user=form.instance.user,
                club=club.parent_club,
                date_start__gte=club.parent_club.membership_start,
        ).exists():
            form.add_error('user', _('User is not a member of the parent club') + ' ' + club.parent_club.name)
            error = True

        if club.membership_start and form.instance.date_start < club.membership_start:
            form.add_error('user', _("The membership must start after {:%m-%d-%Y}.")
                           .format(form.instance.club.membership_start))
            error = True

        if club.membership_end and form.instance.date_start > club.membership_end:
            form.add_error('user', _("The membership must begin before {:%m-%d-%Y}.")
                           .format(form.instance.club.membership_end))
            error = True

        if credit_amount and not SpecialTransaction.validate_payment_form(form):
            # Check that special information for payment are filled
            error = True

        return not error

    @transaction.atomic
    def form_valid(self, form):
        """
        Create membership, check that all is good, make transactions
        """
        # Get the club that is concerned by the membership
        if "club_pk" in self.kwargs:  # get from url of new membership
            club = Club.objects.filter(PermissionBackend.filter_queryset(self.request, Club, "view")) \
                .get(pk=self.kwargs["club_pk"])
            user = form.instance.user
            old_membership = None
        else:  # get from url for renewal
            old_membership = self.get_queryset().get(pk=self.kwargs["pk"])
            club = old_membership.club
            user = old_membership.user

        # Update club membership date
        if PermissionBackend.check_perm(self.request, "member.change_club_membership_start", club):
            club.update_membership_dates()

        form.instance.club = club

        # Get form data
        credit_type = form.cleaned_data["credit_type"]
        # but with this way users can customize their section as they want.
        credit_amount = form.cleaned_data["credit_amount"]
        last_name = form.cleaned_data["last_name"]
        first_name = form.cleaned_data["first_name"]
        bank = form.cleaned_data["bank"]
        soge = form.cleaned_data["soge"] and not user.profile.soge and (club.name == "BDE" or club.name == "Kfet")

        # If Société générale pays, then we store that information but the payment must be controlled by treasurers
        # later. The membership transaction will be invalidated.
        if soge:
            credit_type = None
            form.instance._soge = True

        if credit_type is None:
            credit_amount = 0

        fee = 0
        c = club
        # collect the fees required to be paid
        while c is not None and c.membership_start:
            if not Membership.objects.filter(
                    club=c,
                    user=user,
                    date_start__gte=c.membership_start,
            ).exists():
                fee += c.membership_fee_paid if user.profile.paid else c.membership_fee_unpaid
            c = c.parent_club

        # Make some verifications about the form, and if there is an error, then assume that the form is invalid
        if not self.perform_verifications(form, user, club, fee):
            return self.form_invalid(form)

        # Now, all is fine, the membership can be created.

        if club.name == "BDE" or club.name == "Kfet":
            # When we renew the BDE membership, we update the profile section
            # that should happens at least once a year.
            user.profile.section = user.profile.section_generated
            user.profile._force_save = True
            user.profile.save()

        # Credit note before the membership is created.
        if credit_amount > 0:
            transaction = SpecialTransaction(
                source=credit_type,
                destination=user.note,
                quantity=1,
                amount=credit_amount,
                reason="Crédit " + credit_type.special_type + " (Adhésion " + club.name + ")",
                last_name=last_name,
                first_name=first_name,
                bank=bank,
                valid=True,
            )
            transaction._force_save = True
            transaction.save()

        # Parent club memberships are automatically renewed / created.
        # For example, a Kfet membership creates a BDE membership if it does not exist.
        form.instance._force_renew_parent = True

        ret = super().form_valid(form)

        member_role = Role.objects.filter(Q(name="Adhérent⋅e BDE") | Q(name="Membre de club")).all() \
            if club.name == "BDE" else Role.objects.filter(Q(name="Adhérent⋅e Kfet") | Q(name="Membre de club")).all() \
            if club.name == "Kfet"else Role.objects.filter(name="Membre de club").all()
        # Set the same roles as before
        if old_membership:
            member_role = member_role.union(old_membership.roles.all())
        form.instance.roles.set(member_role)
        form.instance._force_save = True
        form.instance.save()

        # If Société générale pays, then we assume that this is the BDE membership, and we auto-renew the
        # Kfet membership.
        if soge and club.name == "BDE":
            kfet = Club.objects.get(name="Kfet")
            fee = kfet.membership_fee_paid if user.profile.paid else kfet.membership_fee_unpaid

            # Get current membership, to get the end date
            old_membership = Membership.objects.filter(
                club=kfet,
                user=user,
            ).order_by("-date_start")

            if not old_membership.filter(date_start__gte=kfet.membership_start).exists():
                # If the membership is not already renewed
                membership = Membership(
                    club=kfet,
                    user=user,
                    fee=fee,
                    date_start=max(old_membership.first().date_end + timedelta(days=1), kfet.membership_start)
                    if old_membership.exists() else form.instance.date_start,
                )
                membership._force_save = True
                membership._soge = True
                membership.save()
                membership.refresh_from_db()
                if old_membership.exists():
                    membership.roles.set(old_membership.get().roles.all())
                membership.roles.set(Role.objects.filter(Q(name="Adhérent⋅e Kfet") | Q(name="Membre de club")).all())
                membership.save()

        return ret

    def get_success_url(self):
        return reverse_lazy('member:user_detail', kwargs={'pk': self.object.user.id})


class ClubManageRolesView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
    """
    Manage the roles of a user in a club
    """
    model = Membership
    form_class = MembershipRolesForm
    template_name = 'member/add_members.html'
    extra_context = {"title": _("Manage roles of an user in the club")}

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        club = self.object.club
        context['club'] = club
        return context

    def get_form(self, form_class=None):
        form = super().get_form(form_class)

        club = self.object.club
        form.fields['roles'].queryset = Role.objects.filter(Q(weirole__isnull=not hasattr(club, 'weiclub'))
                                                            & (Q(for_club__isnull=True) | Q(for_club=club))).all()

        return form

    def get_success_url(self):
        return reverse_lazy('member:user_detail', kwargs={'pk': self.object.user.id})


class ClubMembersListView(ExportMixin, ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
    model = Membership
    table_class = MembershipTable
    template_name = "member/club_members.html"
    extra_context = {"title": _("Members of the club")}
    export_formats = ["csv"]

    def get_queryset(self, **kwargs):
        qs = super().get_queryset().filter(club_id=self.kwargs["pk"])

        if 'search' in self.request.GET:
            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"
            prefix = "^" if valid_regex else ""
            qs = qs.filter(
                Q(**{f"user__first_name{suffix}": prefix + pattern})
                | Q(**{f"user__last_name{suffix}": prefix + pattern})
                | Q(**{f"user__note__alias__normalized_name{suffix}": prefix + Alias.normalize(pattern)})
            )

        only_active = "only_active" not in self.request.GET or self.request.GET["only_active"] != '0'

        if only_active:
            qs = qs.filter(date_start__lte=timezone.now().today(), date_end__gte=timezone.now().today())

        if "roles" in self.request.GET:
            roles_str = self.request.GET["roles"].replace(' ', '').split(',') if self.request.GET["roles"] else ['0']
            roles_int = map(int, roles_str)
            qs = qs.filter(roles__in=roles_int)

        qs = qs.order_by('-date_start', 'user__username')

        return qs.distinct()

    def get_export_filename(self, export_format):
        return "members.csv"

    def get_export_content_type(self, export_format):
        if export_format == "csv":
            return "text/csv"
        return super().get_export_content_type(export_format)

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        club = Club.objects.filter(
            PermissionBackend.filter_queryset(self.request, Club, "view")
        ).get(pk=self.kwargs["pk"])
        context["club"] = club

        applicable_roles = Role.objects.filter(Q(weirole__isnull=not hasattr(club, 'weiclub'))
                                               & (Q(for_club__isnull=True) | Q(for_club=club))).all()
        context["applicable_roles"] = applicable_roles

        context["only_active"] = "only_active" not in self.request.GET or self.request.GET["only_active"] != '0'

        return context
