import calendar
import random
from datetime import timedelta

from custommail.models import CustomMail
from custommail.shortcuts import send_mass_custom_mail
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.db import models
from django.db.models import Count, Exists
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)

    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
        datatuple = [
            (
                "bda-rappel",
                {"member": member, "nb_attr": member.nb_attr, "show": self},
                settings.MAIL_DATA["rappels"]["FROM"],
                [member.email],
            )
            for member in members
        ]
        send_mass_custom_mail(datatuple)
        # 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)
    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)


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")
        datatuple = [
            (
                "bda-revente",
                {
                    "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_custom_mail(datatuple)
        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")
        datatuple = [
            (
                "bda-shotgun",
                {
                    "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_custom_mail(datatuple)
        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,
                }

                c_mails_qs = CustomMail.objects.filter(
                    shortname__in=[
                        "bda-revente-winner",
                        "bda-revente-loser",
                        "bda-revente-seller",
                    ]
                )

                c_mails = {cm.shortname: cm for cm in c_mails_qs}

                mails.append(
                    c_mails["bda-revente-winner"].get_message(
                        context,
                        from_email=settings.MAIL_DATA["revente"]["FROM"],
                        to=[winner.user.email],
                    )
                )

                mails.append(
                    c_mails["bda-revente-seller"].get_message(
                        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(
                            c_mails["bda-revente-loser"].get_message(
                                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