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

import datetime
import os

from django.conf import settings
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from django.db import models, transaction
from django.db.models import Q
from django.template import loader
from django.urls import reverse, reverse_lazy
from django.utils import timezone
from django.utils.encoding import force_bytes
from django.utils.http import urlsafe_base64_encode
from django.utils.translation import gettext_lazy as _
from phonenumber_field.modelfields import PhoneNumberField
from permission.models import Role
from registration.tokens import email_validation_token
from note.models import MembershipTransaction


class Profile(models.Model):
    """
    An user profile

    We do not want to patch the Django Contrib :model:`auth.User`model;
    so this model add an user profile with additional information.
    """
    user = models.OneToOneField(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
    )

    phone_number = PhoneNumberField(
        verbose_name=_('phone number'),
        max_length=50,
        blank=True,
        null=True,
    )

    section = models.CharField(
        verbose_name=_('section'),
        help_text=_('e.g. "1A0", "9A♥", "SAPHIRE"'),
        max_length=255,
        blank=True,
        default="",
    )

    department = models.CharField(
        max_length=8,
        verbose_name=_("department"),
        choices=[
            ('A0', _("Informatics (A0)")),
            ('A1', _("Mathematics (A1)")),
            ('A2', _("Physics (A2)")),
            ("A'2", _("Applied physics (A'2)")),
            ("A''2", _("Chemistry (A''2)")),
            ('A3', _("Biology (A3)")),
            ('B1234', _("SAPHIRE (B1234)")),
            ('B1', _("Mechanics (B1)")),
            ('B2', _("Civil engineering (B2)")),
            ('B3', _("Mechanical engineering (B3)")),
            ('B4', _("EEA (B4)")),
            ('C', _("Design (C)")),
            ('D2', _("Economy-management (D2)")),
            ('D3', _("Social sciences (D3)")),
            ('E', _("English (E)")),
            ('EXT', _("External (EXT)")),
        ]
    )

    promotion = models.PositiveSmallIntegerField(
        null=True,
        default=datetime.date.today().year if datetime.date.today().month >= 8 else datetime.date.today().year - 1,
        verbose_name=_("promotion"),
        help_text=_("Year of entry to the school (None if not ENS student)"),
    )

    address = models.CharField(
        verbose_name=_('address'),
        max_length=255,
        blank=True,
        default="",
    )

    paid = models.BooleanField(
        verbose_name=_("paid"),
        help_text=_("Tells if the user receive a salary."),
        default=False,
    )

    ml_events_registration = models.CharField(
        blank=True,
        default='',
        max_length=2,
        choices=[
            ('', _("No")),
            ('fr', _("Yes (receive them in french)")),
            ('en', _("Yes (receive them in english)")),
        ],
        verbose_name=_("Register on the mailing list to stay informed of the events of the campus (1 mail/week)"),
    )

    ml_sport_registration = models.BooleanField(
        default=False,
        verbose_name=_("Register on the mailing list to stay informed of the sport events of the campus (1 mail/week)"),
    )

    ml_art_registration = models.BooleanField(
        default=False,
        verbose_name=_("Register on the mailing list to stay informed of the art events of the campus (1 mail/week)"),
    )

    report_frequency = models.PositiveSmallIntegerField(
        verbose_name=_("report frequency (in days)"),
        default=0,
    )

    last_report = models.DateTimeField(
        verbose_name=_("last report date"),
        default=timezone.now,
    )

    email_confirmed = models.BooleanField(
        verbose_name=_("email confirmed"),
        default=False,
    )

    registration_valid = models.BooleanField(
        verbose_name=_("registration valid"),
        default=False,
    )

    VSS_charter_read = models.BooleanField(
        verbose_name=_("VSS charter read"),
        default=False
    )

    class Meta:
        verbose_name = _('user profile')
        verbose_name_plural = _('user profile')
        indexes = [models.Index(fields=['user'])]

    def __str__(self):
        return str(self.user)

    def get_absolute_url(self):
        return reverse('member:user_detail', args=(self.user_id,))

    @property
    def ens_year(self):
        """
        Number of years since the 1st august of the entry year, rounded up.
        """
        if self.promotion is None:
            return 0
        today = datetime.date.today()
        years = today.year - self.promotion
        if today.month >= 8:
            years += 1
        return years

    @property
    def section_generated(self):
        return str(self.ens_year) + self.department

    @property
    def soge(self):
        if "treasury" in settings.INSTALLED_APPS:
            from treasury.models import SogeCredit
            return SogeCredit.objects.filter(user=self.user, credit_transaction__isnull=False).exists()
        return False

    def send_email_validation_link(self):
        subject = "[Note Kfet] " + str(_("Activate your Note Kfet account"))
        token = email_validation_token.make_token(self.user)
        uid = urlsafe_base64_encode(force_bytes(self.user_id))
        message = loader.render_to_string('registration/mails/email_validation_email.txt',
                                          {
                                              'user': self.user,
                                              'domain': os.getenv("NOTE_URL", "note.example.com"),
                                              'token': token,
                                              'uid': uid,
                                          })
        html = loader.render_to_string('registration/mails/email_validation_email.html',
                                       {
                                           'user': self.user,
                                           'domain': os.getenv("NOTE_URL", "note.example.com"),
                                           'token': token,
                                           'uid': uid,
                                       })
        self.user.email_user(subject, message, html_message=html)


class Club(models.Model):
    """
    A club is a group of people, whose membership is handle by their
    :model:`member.Membership`, and gives access to right defined by a :model:`member.Role`.
    """
    name = models.CharField(
        verbose_name=_('name'),
        max_length=255,
        unique=True,
    )

    email = models.EmailField(
        verbose_name=_('email'),
    )

    parent_club = models.ForeignKey(
        'self',
        null=True,
        blank=True,
        on_delete=models.PROTECT,
        verbose_name=_('parent club'),
    )

    # Memberships

    # When set to False, the membership system won't be used.
    # Useful to create notes for activities or departments.
    require_memberships = models.BooleanField(
        default=True,
        verbose_name=_("require memberships"),
        help_text=_("Uncheck if this club don't require memberships."),
    )

    membership_fee_paid = models.PositiveIntegerField(
        default=0,
        verbose_name=_('membership fee (paid students)'),
    )

    membership_fee_unpaid = models.PositiveIntegerField(
        default=0,
        verbose_name=_('membership fee (unpaid students)'),
    )

    membership_duration = models.PositiveIntegerField(
        blank=True,
        null=True,
        verbose_name=_('membership duration'),
        help_text=_('The longest time (in days) a membership can last '
                    '(NULL = infinite).'),
    )

    membership_start = models.DateField(
        blank=True,
        null=True,
        verbose_name=_('membership start'),
        help_text=_('Date from which the members can renew their membership.'),
    )

    membership_end = models.DateField(
        blank=True,
        null=True,
        verbose_name=_('membership end'),
        help_text=_('Maximal date of a membership, after which members must renew it.'),
    )

    add_registration_form = models.BooleanField(
        verbose_name=_("add to registration form"),
        default=False,
    )

    class Meta:
        verbose_name = _("club")
        verbose_name_plural = _("clubs")

    def __str__(self):
        return self.name

    @transaction.atomic
    def save(self, force_insert=False, force_update=False, using=None,
             update_fields=None):
        if not self.require_memberships:
            self.membership_fee_paid = 0
            self.membership_fee_unpaid = 0
            self.membership_duration = None
            self.membership_start = None
            self.membership_end = None
        super().save(force_insert, force_update, update_fields)

    def get_absolute_url(self):
        return reverse_lazy('member:club_detail', args=(self.pk,))

    def update_membership_dates(self):
        """
        This function is called each time the club detail view is displayed.
        Update the year of the membership dates.
        """
        if not self.membership_start or not self.membership_end:
            return

        today = datetime.date.today()

        # Avoid any problems on February 29
        if self.membership_start.month == 2 and self.membership_start.day == 29:
            self.membership_start -= datetime.timedelta(days=1)
        if self.membership_end.month == 2 and self.membership_end.day == 29:
            self.membership_end += datetime.timedelta(days=1)

        while today >= datetime.date(self.membership_start.year + 1,
                                     self.membership_start.month, self.membership_start.day):
            if self.membership_start:
                self.membership_start = datetime.date(self.membership_start.year + 1,
                                                      self.membership_start.month, self.membership_start.day)
            if self.membership_end:
                self.membership_end = datetime.date(self.membership_end.year + 1,
                                                    self.membership_end.month, self.membership_end.day)
            self._force_save = True
            self.save(force_update=True)


class Membership(models.Model):
    """
    Register the membership of a user to a club, including roles and membership duration.

    """
    user = models.ForeignKey(
        User,
        on_delete=models.PROTECT,
        related_name="memberships",
        verbose_name=_("user"),
    )

    club = models.ForeignKey(
        Club,
        on_delete=models.PROTECT,
        verbose_name=_("club"),
    )

    roles = models.ManyToManyField(
        "permission.Role",
        related_name="memberships",
        verbose_name=_("roles"),
    )

    date_start = models.DateField(
        default=datetime.date.today,
        verbose_name=_('membership starts on'),
    )

    date_end = models.DateField(
        verbose_name=_('membership ends on'),
        null=True,
    )

    fee = models.PositiveIntegerField(
        verbose_name=_('fee'),
    )

    class Meta:
        verbose_name = _('membership')
        verbose_name_plural = _('memberships')
        indexes = [models.Index(fields=['user'])]

    def __str__(self):
        return _("Membership of {user} for the club {club}").format(user=self.user.username, club=self.club.name, )

    @transaction.atomic
    def save(self, *args, **kwargs):
        """
        Calculate fee and end date before saving the membership and creating the transaction if needed.
        """
        # Ensure that club membership dates are valid
        old_membership_start = self.club.membership_start
        self.club.update_membership_dates()
        if self.club.membership_start != old_membership_start:
            self.club.save()

        created = not self.pk
        if not created:
            for role in self.roles.all():
                club = role.for_club
                if club is not None:
                    if club.pk != self.club_id:
                        raise ValidationError(_('The role {role} does not apply to the club {club}.')
                                              .format(role=role.name, club=club.name))
        else:
            if Membership.objects.filter(
                    user=self.user,
                    club=self.club,
                    date_start__lte=self.date_start,
                    date_end__gte=self.date_start,
            ).exists():
                raise ValidationError(_('User is already a member of the club'))

            if self.club.parent_club is not None:
                # Check that the user is already a member of the parent club if the membership is created
                if not Membership.objects.filter(
                    user=self.user,
                    club=self.club.parent_club,
                    date_start__gte=self.club.parent_club.membership_start,
                ).exists():
                    if hasattr(self, '_force_renew_parent') and self._force_renew_parent:
                        self.renew_parent()
                    else:
                        raise ValidationError(_('User is not a member of the parent club')
                                              + ' ' + self.club.parent_club.name)

            self.fee = self.club.membership_fee_paid if self.user.profile.paid else self.club.membership_fee_unpaid

            self.date_end = self.date_start + datetime.timedelta(days=self.club.membership_duration) \
                if self.club.membership_duration is not None else self.date_start + datetime.timedelta(days=424242)
            if self.club.membership_end is not None and self.date_end > self.club.membership_end:
                self.date_end = self.club.membership_end

        super().save(*args, **kwargs)

        self.make_transaction()

    @property
    def valid(self):
        """
        A membership is valid if today is between the start and the end date.
        """
        if self.date_end is not None:
            return self.date_start.toordinal() <= datetime.datetime.now().toordinal() <= self.date_end.toordinal()
        else:
            return self.date_start.toordinal() <= datetime.datetime.now().toordinal()

    def renew(self):
        """
        If the current membership comes to expiration, create a new membership that starts immediately after this one.
        """
        if not Membership.objects.filter(
                user=self.user,
                club=self.club,
                date_start__gte=self.club.membership_start,
        ).exists():
            # Membership is not renewed yet
            new_membership = Membership(
                user=self.user,
                club=self.club,
                date_start=max(self.date_end + datetime.timedelta(days=1), self.club.membership_start),
            )
            if hasattr(self, '_force_renew_parent') and self._force_renew_parent:
                new_membership._force_renew_parent = True
            if hasattr(self, '_force_save') and self._force_save:
                new_membership._force_save = True
            new_membership.save()
            new_membership.roles.set(self.roles.all())
            new_membership.save()

    def renew_parent(self):
        """
        Ensure that the parent membership is renewed, and renew/create it if needed.
        """
        parent_membership = Membership.objects.filter(
            user=self.user,
            club=self.club.parent_club,
        ).order_by("-date_start")
        if parent_membership.exists():
            # Renew the previous membership of the parent club
            parent_membership = parent_membership.first()
            parent_membership._force_renew_parent = True
            if hasattr(self, '_force_save'):
                parent_membership._force_save = True
            parent_membership.renew()
        else:
            # Create a new membership in the parent club
            parent_membership = Membership(
                user=self.user,
                club=self.club.parent_club,
                date_start=self.date_start,
            )
            parent_membership._force_renew_parent = True
            if hasattr(self, '_force_save'):
                parent_membership._force_save = True
            parent_membership.save()
            parent_membership.refresh_from_db()

            if self.club.parent_club.name == "BDE":
                parent_membership.roles.set(
                    Role.objects.filter(Q(name="Adhérent⋅e BDE") | Q(name="Membre de club")).all())
            elif self.club.parent_club.name == "Kfet":
                parent_membership.roles.set(
                    Role.objects.filter(Q(name="Adhérent⋅e Kfet") | Q(name="Membre de club")).all())
            else:
                parent_membership.roles.set(Role.objects.filter(name="Membre de club").all())
            parent_membership.save()

    def make_transaction(self):
        """
        Create Membership transaction associated to this membership.
        """
        if not self.fee or MembershipTransaction.objects.filter(membership=self).exists():
            return

        if self.fee:
            transaction = MembershipTransaction(
                membership=self,
                source=self.user.note,
                destination=self.club.note,
                quantity=1,
                amount=self.fee,
                reason="Adhésion " + self.club.name,
            )
            transaction._force_save = True
            if hasattr(self, '_soge') and "treasury" in settings.INSTALLED_APPS\
                    and (self.club.name == "BDE" or self.club.name == "Kfet"
                         or ("wei" in settings.INSTALLED_APPS and hasattr(self.club, "weiclub") and self.club.weiclub)):
                # If the soge pays, then the transaction is unvalidated in a first time, then submitted for control
                # to treasurers.
                transaction.valid = False
                from treasury.models import SogeCredit
                if SogeCredit.objects.filter(user=self.user).exists():
                    soge_credit = SogeCredit.objects.get(user=self.user)
                else:
                    soge_credit = SogeCredit(user=self.user)
                    soge_credit._force_save = True
                    soge_credit.save(force_insert=True)
                    soge_credit.refresh_from_db()
                transaction.save(force_insert=True)
                transaction.refresh_from_db()
                soge_credit.transactions.add(transaction)
                soge_credit.save()
            else:
                transaction.save(force_insert=True)
