import calendar
import random
from datetime import timedelta

from django.conf import settings
from django.contrib.auth.models import User
from django.contrib.sites.models import Site
from django.core import mail
from django.core.mail import EmailMessage, send_mass_mail
from django.db import models
from django.db.models import Count, Exists
from django.template import loader
from django.utils import formats, timezone


def get_generic_user():
    generic, _ = User.objects.get_or_create(
        username="bda_generic",
        defaults={"email": "bda@ens.fr", "first_name": "Bureau des arts"},
    )
    return generic


class Tirage(models.Model):
    title = models.CharField("Titre", max_length=300)
    ouverture = models.DateTimeField("Date et heure d'ouverture du tirage")
    fermeture = models.DateTimeField("Date et heure de fermerture du tirage")
    tokens = models.TextField("Graine(s) du tirage", blank=True)
    active = models.BooleanField("Tirage actif", default=False)
    appear_catalogue = models.BooleanField(
        "Tirage à afficher dans le catalogue", default=False
    )
    enable_do_tirage = models.BooleanField("Le tirage peut être lancé", default=False)
    archived = models.BooleanField("Archivé", default=False)

    def __str__(self):
        return "%s - %s" % (
            self.title,
            formats.localize(timezone.template_localtime(self.fermeture)),
        )


class Salle(models.Model):
    name = models.CharField("Nom", max_length=300)
    address = models.TextField("Adresse")

    def __str__(self):
        return self.name


class CategorieSpectacle(models.Model):
    name = models.CharField("Nom", max_length=100, unique=True)

    def __str__(self):
        return self.name

    class Meta:
        verbose_name = "Catégorie"


class Spectacle(models.Model):
    title = models.CharField("Titre", max_length=300)
    category = models.ForeignKey(
        CategorieSpectacle, on_delete=models.CASCADE, blank=True, null=True
    )
    date = models.DateTimeField("Date & heure")
    location = models.ForeignKey(Salle, on_delete=models.CASCADE)
    vips = models.TextField("Personnalités", blank=True)
    description = models.TextField("Description", blank=True)
    slots_description = models.TextField("Description des places", blank=True)
    image = models.ImageField("Image", blank=True, null=True, upload_to="imgs/shows/")
    ext_link = models.CharField(
        "Lien vers le site du spectacle", blank=True, max_length=500
    )
    price = models.FloatField("Prix d'une place")
    slots = models.IntegerField("Places")
    tirage = models.ForeignKey(Tirage, on_delete=models.CASCADE)
    listing = models.BooleanField("Les places sont sur listing")
    rappel_sent = models.DateTimeField("Mail de rappel envoyé", blank=True, null=True)

    class Meta:
        verbose_name = "Spectacle"
        ordering = ("date", "title")

    def timestamp(self):
        return "%d" % calendar.timegm(self.date.utctimetuple())

    def __str__(self):
        return "%s - %s, %s, %.02f€" % (
            self.title,
            formats.localize(timezone.template_localtime(self.date)),
            self.location,
            self.price,
        )

    def getImgUrl(self):
        """
        Cette fonction permet d'obtenir l'URL de l'image, si elle existe
        """
        try:
            return self.image.url
        except Exception:
            return None

    def send_rappel(self):
        """
        Envoie un mail de rappel à toutes les personnes qui ont une place pour
        ce spectacle.
        """
        # On récupère la liste des participants + le BdA
        members = list(
            User.objects.filter(participant__attributions=self)
            .annotate(nb_attr=Count("id"))
            .order_by()
        )
        bda_generic = get_generic_user()
        bda_generic.nb_attr = 1
        members.append(bda_generic)
        # On écrit un mail personnalisé à chaque participant
        mails = [
            (
                str(self),
                loader.render_to_string(
                    "bda/mails/rappel.txt",
                    context={"member": member, "nb_attr": member.nb_attr, "show": self},
                ),
                settings.MAIL_DATA["rappels"]["FROM"],
                [member.email],
            )
            for member in members
        ]
        send_mass_mail(mails)
        # On enregistre le fait que l'envoi a bien eu lieu
        self.rappel_sent = timezone.now()
        self.save()
        # On renvoie la liste des destinataires
        return members

    @property
    def is_past(self):
        return self.date < timezone.now()


class Quote(models.Model):
    spectacle = models.ForeignKey(Spectacle, on_delete=models.CASCADE)
    text = models.TextField("Citation")
    author = models.CharField("Auteur", max_length=200)


PAYMENT_TYPES = (
    ("cash", "Cash"),
    ("cb", "CB"),
    ("cheque", "Chèque"),
    ("autre", "Autre"),
)


class Attribution(models.Model):
    participant = models.ForeignKey("Participant", on_delete=models.CASCADE)
    spectacle = models.ForeignKey(
        Spectacle, on_delete=models.CASCADE, related_name="attribues"
    )
    given = models.BooleanField("Donnée", default=False)
    paid = models.BooleanField("Payée", default=False)
    paymenttype = models.CharField(
        "Moyen de paiement", max_length=6, choices=PAYMENT_TYPES, blank=True
    )

    def __str__(self):
        return "%s -- %s, %s" % (
            self.participant.user,
            self.spectacle.title,
            self.spectacle.date,
        )


class ParticipantPaidQueryset(models.QuerySet):
    """
    Un manager qui annote le queryset avec un champ `paid`,
    indiquant si un participant a payé toutes ses attributions.
    """

    def annotate_paid(self):
        # OuterRef permet de se référer à un champ d'un modèle non encore fixé
        # Voir:
        # https://docs.djangoproject.com/en/2.2/ref/models/expressions/#django.db.models.OuterRef
        unpaid = Attribution.objects.filter(
            participant=models.OuterRef("pk"), paid=False
        )
        return self.annotate(paid=~Exists(unpaid))


class Participant(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    choices = models.ManyToManyField(
        Spectacle, through="ChoixSpectacle", related_name="chosen_by"
    )
    attributions = models.ManyToManyField(
        Spectacle, through="Attribution", related_name="attributed_to"
    )
    tirage = models.ForeignKey(
        Tirage, on_delete=models.CASCADE, limit_choices_to={"archived": False}
    )
    accepte_charte = models.BooleanField("A accepté la charte BdA", default=False)
    choicesrevente = models.ManyToManyField(
        Spectacle, related_name="subscribed", blank=True
    )

    objects = ParticipantPaidQueryset.as_manager()

    def __str__(self):
        return "%s - %s" % (self.user, self.tirage.title)

    class Meta:
        ordering = ("-tirage", "user__last_name", "user__first_name")
        constraints = [
            models.UniqueConstraint(fields=("tirage", "user"), name="unique_tirage"),
        ]


DOUBLE_CHOICES = (
    ("1", "1 place"),
    ("double", "2 places si possible, 1 sinon"),
    ("autoquit", "2 places sinon rien"),
)


class ChoixSpectacle(models.Model):
    participant = models.ForeignKey(Participant, on_delete=models.CASCADE)
    spectacle = models.ForeignKey(
        Spectacle, on_delete=models.CASCADE, related_name="participants"
    )
    priority = models.PositiveIntegerField("Priorité")
    double_choice = models.CharField(
        "Nombre de places", default="1", choices=DOUBLE_CHOICES, max_length=10
    )

    def get_double(self):
        return self.double_choice != "1"

    double = property(get_double)

    def get_autoquit(self):
        return self.double_choice == "autoquit"

    autoquit = property(get_autoquit)

    def __str__(self):
        return "Vœux de %s pour %s" % (
            self.participant.user.get_full_name(),
            self.spectacle.title,
        )

    class Meta:
        ordering = ("priority",)
        unique_together = (("participant", "spectacle"),)
        verbose_name = "voeu"
        verbose_name_plural = "voeux"


class SpectacleRevente(models.Model):
    attribution = models.OneToOneField(
        Attribution, on_delete=models.CASCADE, related_name="revente"
    )
    date = models.DateTimeField("Date de mise en vente", default=timezone.now)
    confirmed_entry = models.ManyToManyField(
        Participant, related_name="entered", blank=True
    )
    seller = models.ForeignKey(
        Participant,
        on_delete=models.CASCADE,
        verbose_name="Vendeur",
        related_name="original_shows",
    )
    soldTo = models.ForeignKey(
        Participant,
        on_delete=models.CASCADE,
        verbose_name="Vendue à",
        blank=True,
        null=True,
    )

    notif_sent = models.BooleanField("Notification envoyée", default=False)

    notif_time = models.DateTimeField(
        "Moment d'envoi de la notification", blank=True, null=True
    )

    tirage_done = models.BooleanField("Tirage effectué", default=False)

    shotgun = models.BooleanField("Disponible immédiatement", default=False)
    ####
    # Some class attributes
    ###
    # TODO : settings ?

    # Temps minimum entre le tirage et le spectacle
    min_margin = timedelta(days=5)

    # Temps entre la création d'une revente et l'envoi du mail
    remorse_time = timedelta(hours=1)

    # Temps min/max d'attente avant le tirage
    max_wait_time = timedelta(days=3)
    min_wait_time = timedelta(days=1)

    @property
    def real_notif_time(self):
        if self.notif_time:
            return self.notif_time
        else:
            return self.date + self.remorse_time

    @property
    def date_tirage(self):
        """Renvoie la date du tirage au sort de la revente."""

        remaining_time = (
            self.attribution.spectacle.date - self.real_notif_time - self.min_margin
        )

        delay = min(remaining_time, self.max_wait_time)

        return self.real_notif_time + delay

    @property
    def is_urgent(self):
        """
        Renvoie True iff la revente doit être mise au shotgun directement.
        Plus précisément, on doit avoir min_margin + min_wait_time de marge.
        """
        spectacle_date = self.attribution.spectacle.date
        return spectacle_date <= timezone.now() + self.min_margin + self.min_wait_time

    @property
    def can_notif(self):
        return timezone.now() >= self.date + self.remorse_time

    def __str__(self):
        return "%s -- %s" % (self.seller, self.attribution.spectacle.title)

    class Meta:
        verbose_name = "Revente"

    def reset(self, new_date=timezone.now()):
        """Réinitialise la revente pour permettre une remise sur le marché"""
        self.seller = self.attribution.participant
        self.date = new_date
        self.confirmed_entry.clear()
        self.soldTo = None
        self.notif_sent = False
        self.notif_time = None
        self.tirage_done = False
        self.shotgun = False
        self.save()

    def send_notif(self):
        """
        Envoie une notification pour indiquer la mise en vente d'une place sur
        BdA-Revente à tous les intéressés.
        """
        inscrits = self.attribution.spectacle.subscribed.select_related("user")
        mails = [
            (
                "BdA-Revente : {}".format(self.attribution.spectacle.title),
                loader.render_to_string(
                    "bda/mails/revente-new.txt",
                    context={
                        "member": participant.user,
                        "show": self.attribution.spectacle,
                        "revente": self,
                        "site": Site.objects.get_current(),
                    },
                ),
                settings.MAIL_DATA["revente"]["FROM"],
                [participant.user.email],
            )
            for participant in inscrits
        ]
        send_mass_mail(mails)
        self.notif_sent = True
        self.notif_time = timezone.now()
        self.save()

    def mail_shotgun(self):
        """
        Envoie un mail à toutes les personnes intéréssées par le spectacle pour
        leur indiquer qu'il est désormais disponible au shotgun.
        """
        inscrits = self.attribution.spectacle.subscribed.select_related("user")
        mails = [
            (
                "BdA-Revente : {}".format(self.attribution.spectacle.title),
                loader.render_to_string(
                    "bda/mails/revente-shotgun.txt",
                    context={
                        "member": participant.user,
                        "show": self.attribution.spectacle,
                        "site": Site.objects.get_current(),
                    },
                ),
                settings.MAIL_DATA["revente"]["FROM"],
                [participant.user.email],
            )
            for participant in inscrits
        ]
        send_mass_mail(mails)
        self.notif_sent = True
        self.notif_time = timezone.now()
        # Flag inutile, sauf si l'horloge interne merde
        self.tirage_done = True
        self.shotgun = True
        self.save()

    def tirage(self, send_mails=True):
        """
        Lance le tirage au sort associé à la revente. Un gagnant est choisi
        parmis les personnes intéressées par le spectacle. Les personnes sont
        ensuites prévenues par mail du résultat du tirage.
        """
        inscrits = list(self.confirmed_entry.all())
        spectacle = self.attribution.spectacle
        seller = self.seller
        winner = None

        if inscrits:
            # Envoie un mail au gagnant et au vendeur
            winner = random.choice(inscrits)
            self.soldTo = winner
            if send_mails:
                mails = []

                context = {
                    "acheteur": winner.user,
                    "vendeur": seller.user,
                    "show": spectacle,
                }

                subject = "BdA-Revente : {}".format(spectacle.title)

                mails.append(
                    EmailMessage(
                        subject=subject,
                        body=loader.render_to_string(
                            "bda/mails/revente-tirage-winner.txt",
                            context=context,
                        ),
                        from_email=settings.MAIL_DATA["revente"]["FROM"],
                        to=[winner.user.email],
                    )
                )
                mails.append(
                    EmailMessage(
                        subject=subject,
                        body=loader.render_to_string(
                            "bda/mails/revente-tirage-seller.txt",
                            context=context,
                        ),
                        from_email=settings.MAIL_DATA["revente"]["FROM"],
                        to=[seller.user.email],
                        reply_to=[winner.user.email],
                    ),
                )

                # Envoie un mail aux perdants
                for inscrit in inscrits:
                    if inscrit != winner:
                        new_context = dict(context)
                        new_context["acheteur"] = inscrit.user

                        mails.append(
                            EmailMessage(
                                subject=subject,
                                body=loader.render_to_string(
                                    "bda/mails/revente-tirage-loser.txt",
                                    context=new_context,
                                ),
                                from_email=settings.MAIL_DATA["revente"]["FROM"],
                                to=[inscrit.user.email],
                            ),
                        )

                mail_conn = mail.get_connection()
                mail_conn.send_messages(mails)
        # Si personne ne veut de la place, elle part au shotgun
        else:
            self.shotgun = True
        self.tirage_done = True
        self.save()
        return winner