import logging
from uuid import uuid4

from django.conf import settings
from django.db import OperationalError, models, transaction
from django.utils import timezone

from .signals import message_sent
from .utils import AuditModel

log = logging.getLogger(__name__)


class Thread(AuditModel):
    """Main model where a thread is created. This model only contains a subject
    and a ManyToMany relationship with the users.

    Django by default creates an 'invisible' model when ManyToMany is declared
    but we can override the default and point to our own model.

    A `uuid` field is declared as a way to
    """
    uuid = models.UUIDField(blank=False, null=False, editable=False, default=uuid4)
    subject = models.CharField(max_length=150)
    users = models.ManyToManyField(settings.AUTH_USER_MODEL, through="UserThread")

    @classmethod
    def inbox(cls, user):
        """Returns the inbox of a given user"""
        return cls.objects.filter(userthread__user=user, userthread__deleted=False)

    @classmethod
    def deleted(cls, user):
        """Returns the deleted messages of a given user"""
        return cls.objects.filter(userthread__user=user, userthread__deleted=True)

    @classmethod
    def unread(cls, user):
        """Returns all the unread messages of a given user"""
        return cls.objects.filter(
            userthread__user=user,
            userthread__deleted=False,
            userthread__unread=True
        )

    @property
    def first_message(self):
        """Returns the first message"""
        return self.messages.all()[0]

    @property
    def latest_message(self):
        """Returs the last message"""
        return self.messages.order_by("-sent_at")[0]

    @classmethod
    def ordered(cls, objs):
        """
        Returns the iterable ordered the correct way, this is a class method
        because we don"t know what the type of the iterable will be.
        """
        objs = list(objs)
        objs.sort(key=lambda o: o.latest_message.sent_at, reverse=True)
        return objs

    @classmethod
    def get_thread_users(cls):
        """Returns all the users from the thread"""
        return cls.users.all()

    def earliest_message(self, user_to_exclude=None):
        """
        Returns the earliest message of the thread

        :param user_to_exclude: Returns a list of the messages excluding a given user. This is
        particulary useful for showing the earliest message sent in a thread between two different
        users
        """
        try:
            return self.messages.exclude(sender=user_to_exclude).earliest('sent_at')
        except Message.DoesNotExist:
            return

    def last_message(self):
        """
        Returns the latest message of the thread. Is the reverse of the `earliest_message`
        """
        try:
            return self.messages.all().latest('sent_at')
        except Message.DoesNotExist:
            return

    def last_message_excluding_user(self, user_to_exclude=None):
        """
        Returns the latest message of the thread. Is the reverse of the `earliest_message`

        :param user_to_exclude: Returns a list of the messages excluding a given user. This is
        particulary useful for showing the latest message sent in a thread between two different
        users.
        """
        queryset = self.messages.all()
        try:
            if user_to_exclude:
                queryset = queryset.exclude(sender=user_to_exclude)
            return queryset.latest('sent_at')
        except Message.DoesNotExist:
            return

    def unread_messages(self, user):
        """
        Gets the unread messages from User in a given Thread.

        Example:
            ```
            t = Thread.objects.first()
            user = User.objects.first()
            unread = t.uread_messages(user)
            ```
        """
        return self.userthread_set.filter(user=user, deleted=False, unread=True, thread=self)

    def is_user_first_message(self, user):
        """
        Checks if the user started the thread
        :return: Bool
        """
        try:
            message = self.messages.earliest('sent_at')
        except Message.DoesNotExist:
            return False
        return bool(message.sender.pk == user.pk)

    def __str__(self):
        return f"Subject: {self.subject}: {', '.join([str(user) for user in self.users.all()])}"


class UserThread(models.Model):
    """Maps the user and the thread. This model was used to override the default ManyToMany
    relationship table generated by django.
    """
    uuid = models.UUIDField(blank=False, null=False, default=uuid4, editable=False,)

    thread = models.ForeignKey(Thread, on_delete=models.CASCADE)
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)

    unread = models.BooleanField()
    deleted = models.BooleanField()

    def __str__(self):
        return f"Thread: {self.thread}, User: {self.user}"


class Message(models.Model):
    """
    Message model where creates threads, user threads and mapping between them.
    """
    uuid = models.UUIDField(blank=False, null=False, default=uuid4, editable=False)
    thread = models.ForeignKey(Thread, related_name="messages", on_delete=models.CASCADE)
    sender = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="sent_messages", on_delete=models.CASCADE)
    sent_at = models.DateTimeField(default=timezone.now)
    content = models.TextField()

    @classmethod
    def new_reply(cls, thread, user, content):
        """
        Create a new reply for an existing Thread. Mark thread as unread for all other participants,
        and mark thread as read by replier. We want an atomic operation as we can't afford having
        lost data between tables and causing problems with data integrity.
        """
        with transaction.atomic():
            try:
                msg = cls.objects.create(thread=thread, sender=user, content=content)
                thread.userthread_set.exclude(user=user).update(deleted=False, unread=True)
                thread.userthread_set.filter(user=user).update(deleted=False, unread=False)
                message_sent.send(sender=cls, message=msg, thread=thread, reply=True)
            except OperationalError as e:
                log.exception(e)
                return
        return msg

    @classmethod
    def new_message(cls, from_user, to_users, subject, content):
        """
        Create a new Message and Thread. Mark thread as unread for all recipients, and
        mark thread as read and deleted from inbox by creator. We want an atomic operation as we
        also can't afford having lost data between tables and causing problems with data integrity.
        """
        with transaction.atomic():
            try:
                thread = Thread.objects.create(subject=subject)
                for user in to_users:
                    thread.userthread_set.create(user=user, deleted=False, unread=True)
                thread.userthread_set.create(user=from_user, deleted=True, unread=False)
                msg = cls.objects.create(thread=thread, sender=from_user, content=content)
                message_sent.send(sender=cls, message=msg, thread=thread, reply=False)
            except OperationalError as e:
                log.exception(e)
                return
        return msg

    class Meta:
        ordering = ("sent_at",)

    def get_absolute_url(self):
        return self.thread.get_absolute_url()

    def __str__(self):
        return f"{self.uuid}"
