"""
This module defines the database models for mail handling.

"""
from django.db import models
from django.utils.translation import gettext as _
from model_utils.models import TimeStampedModel
from passlib.handlers.sha2_crypt import sha512_crypt

from domains.models import MailDomain
from osusers.models import User as OsUser


class ActivateAbleMixin(models.Model):
    """
    Mixin for model classes that can be active or inactive.

    """

    active = models.BooleanField(default=True)

    class Meta:
        abstract = True


class MailboxManager(models.Manager):
    """
    This is the default manager class for :py:class:`Mailbox`.
    """

    def get_next_mailbox_name(self, osuser):
        """
        Get the next available mailbox name for the given operating system
        user.

        :param osuser: a :py:class:`operating system user <osuser.models.User>`
            instance
        :return: name of the mailbox
        :rtype: str

        """
        count = 1
        mailboxformat = "{0}p{1:02d}"
        mailboxname = mailboxformat.format(osuser.username, count)

        for box in self.values("username").filter(osuser=osuser).order_by("username"):
            if box["username"] == mailboxname:
                count += 1
                mailboxname = mailboxformat.format(osuser.username, count)
            else:
                break
        return mailboxname

    def unused_or_own(self, mailaddress, osuser):
        """
        Returns a queryset that fetches those mailboxes that belong to the
        given operating system user and are either not used by any mail address
        or used by the given mailaddress itself.

        """
        return self.filter(
            models.Q(mailaddressmailbox__isnull=True)
            | models.Q(mailaddressmailbox__mailaddress=mailaddress),
            active=True,
            osuser=osuser,
        )

    def unused(self, osuser):
        """
        Returns a queryset that fetches unused mailboxes belonging to the
        given operating system user.

        """
        return self.filter(
            mailaddressmailbox__isnull=True,
            active=True,
            osuser=osuser,
        )

    def create_mailbox(self, osuser, password=None, commit=True):
        """
        Create a new mailbox for the given operating system user.

        :param osuser: a :py:class:`osuser.models.OsUser` instance
        :param password: an optional password
        :param commit: whether the mailbox should be commited to the database
        :return: mailbox instance
        :rtype: :py:class:`managemails.models.Mailbox`

        """
        mailbox = self.create(
            osuser=osuser, username=self.get_next_mailbox_name(osuser)
        )
        if password is not None:
            mailbox.set_password(password)
        return mailbox


class Mailbox(ActivateAbleMixin, TimeStampedModel):
    """
    This is the model class for a mailbox.

    """

    osuser = models.ForeignKey(OsUser, on_delete=models.CASCADE)
    username = models.CharField(max_length=128, unique=True)
    password = models.CharField(max_length=255)

    objects = MailboxManager()

    class Meta:
        ordering = ["osuser", "username"]
        verbose_name = _("Mailbox")
        verbose_name_plural = _("Mailboxes")

    def set_password(self, password):
        """
        Set the hashed password for the mailbox.

        :param str password: the clear text password

        """
        self.password = sha512_crypt.hash(password)

    def get_mailaddresses(self):
        """
        Get a list of mail addresses assigned to this mailbox.

        """
        addrs = [mbadr.mailaddress for mbadr in self.mailaddressmailbox_set.all()]
        return addrs

    mailaddresses = property(get_mailaddresses)

    def __str__(self):
        return self.username


class MailAddress(ActivateAbleMixin, TimeStampedModel, models.Model):
    """
    This is the model class for a mail address.

    """

    localpart = models.CharField(_("local part"), max_length=128)
    domain = models.ForeignKey(
        MailDomain, verbose_name=_("domain"), on_delete=models.CASCADE
    )

    class Meta:
        ordering = ["domain", "localpart"]
        unique_together = ("localpart", "domain")
        verbose_name = _("Mail address")
        verbose_name_plural = _("Mail addresses")

    def __str__(self):
        return "{0}@{1}".format(self.localpart, self.domain)

    def set_mailbox(self, mailbox, commit=True):
        """
        Set the target for this mail address to the given mailbox. This method
        takes care of removing forwarding addresses or changing existing
        mailbox relations.

        :param mailbox: :py:class:`Mailbox`
        :param boolean commit: whether to persist changes or not
        :return: :py:class:`MailAddressMailbox` instance

        """
        if self.pk is not None:
            if MailAddressMailbox.objects.filter(mailaddress=self).exists():
                mabox = MailAddressMailbox.objects.get(mailaddress=self)
                mabox.mailbox = mailbox
            else:
                mabox = MailAddressMailbox(mailaddress=self, mailbox=mailbox)
            if commit:
                mabox.save()
            for mafwd in MailAddressForward.objects.filter(mailaddress=self):
                mafwd.delete()
        else:
            if commit:
                self.save()
                mabox = MailAddressMailbox(mailaddress=self, mailbox=mailbox)
                mabox.save()
            else:
                mabox = MailAddressMailbox(mailaddress=self, mailbox=mailbox)
        return mabox

    def set_forward_addresses(self, addresses, commit=True):
        """
        Set the forwarding addresses for this mail address to the given
        addresses. This method takes care of removing other mail forwards and
        mailboxes.

        :param addresses: list of email address strings
        :param boolean commit: whether to persist changes or not
        :return: list of :py:class:`MailAddressForward` instances

        """
        retval = []
        if self.pk is not None:
            if MailAddressMailbox.objects.filter(mailaddress=self).exists():
                mabox = MailAddressMailbox.objects.get(mailaddress=self)
                mabox.delete()
            forwards = MailAddressForward.objects.filter(mailaddress=self).all()
            for item in forwards:
                if item.target not in addresses:
                    item.delete()
                else:
                    retval.append(item)
            for target in addresses:
                if target not in [item.target for item in forwards]:
                    mafwd = MailAddressForward(mailaddress=self, target=target)
                    if commit:
                        mafwd.save()
                    retval.append(mafwd)
        else:
            if commit:
                self.save()
            for target in addresses:
                mafwd = MailAddressForward(mailaddress=self, target=target)
                if commit:
                    mafwd.save()
                retval.append(mafwd)
        return retval


class MailAddressMailbox(TimeStampedModel, models.Model):
    """
    This is the model class to assign a mail address to a mailbox.

    """

    mailaddress = models.OneToOneField(
        MailAddress,
        verbose_name=_("mailaddress"),
        primary_key=True,
        on_delete=models.CASCADE,
    )
    mailbox = models.ForeignKey(
        Mailbox, verbose_name=_("mailbox"), on_delete=models.CASCADE
    )

    class Meta:
        unique_together = ("mailaddress", "mailbox")

    def __str__(self):
        return self.mailbox.username


class MailAddressForward(TimeStampedModel, models.Model):
    """
    This is a model class to map mail addresses to forwarding addresses.

    """

    mailaddress = models.ForeignKey(MailAddress, on_delete=models.CASCADE)
    target = models.EmailField(max_length=254)

    class Meta:
        unique_together = ("mailaddress", "target")
