import logging
import time
import django

from django.conf import settings
from django.contrib.sites.models import Site
from django.contrib.sites.managers import CurrentSiteManager
from django.core.mail import EmailMultiAlternatives
from django.db import models
from django.template.loader import select_template
from django.utils.encoding import python_2_unicode_compatible
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ugettext
from django.utils.timezone import now

from sorl.thumbnail import ImageField
from distutils.version import LooseVersion


from .compat import get_context, reverse
from .utils import (
    make_activation_code, get_default_sites, ACTIONS
)

logger = logging.getLogger(__name__)

AUTH_USER_MODEL = getattr(settings, 'AUTH_USER_MODEL', 'auth.User')


@python_2_unicode_compatible
class Newsletter(models.Model):
    site = models.ManyToManyField(Site, default=get_default_sites)

    title = models.CharField(
        max_length=200, verbose_name=_('newsletter title')
    )
    slug = models.SlugField(db_index=True, unique=True)

    email = models.EmailField(
        verbose_name=_('e-mail'), help_text=_('Sender e-mail')
    )
    sender = models.CharField(
        max_length=200, verbose_name=_('sender'), help_text=_('Sender name')
    )

    visible = models.BooleanField(
        default=True, verbose_name=_('visible'), db_index=True
    )

    send_html = models.BooleanField(
        default=True, verbose_name=_('send html'),
        help_text=_('Whether or not to send HTML versions of e-mails.')
    )

    objects = models.Manager()

    # Automatically filter the current site
    on_site = CurrentSiteManager()

    def get_templates(self, action):
        """
        Return a subject, text, HTML tuple with e-mail templates for
        a particular action. Returns a tuple with subject, text and e-mail
        template.
        """

        assert action in ACTIONS + ('message', ), 'Unknown action: %s' % action

        # Common substitutions for filenames
        tpl_subst = {
            'action': action,
            'newsletter': self.slug
        }

        # Common root path for all the templates
        tpl_root = 'newsletter/message/'

        subject_template = select_template([
            tpl_root + '%(newsletter)s/%(action)s_subject.txt' % tpl_subst,
            tpl_root + '%(action)s_subject.txt' % tpl_subst,
        ])

        text_template = select_template([
            tpl_root + '%(newsletter)s/%(action)s.txt' % tpl_subst,
            tpl_root + '%(action)s.txt' % tpl_subst,
        ])

        if self.send_html:
            html_template = select_template([
                tpl_root + '%(newsletter)s/%(action)s.html' % tpl_subst,
                tpl_root + '%(action)s.html' % tpl_subst,
            ])
        else:
            # HTML templates are not required
            html_template = None

        return (subject_template, text_template, html_template)

    def __str__(self):
        return self.title

    class Meta:
        verbose_name = _('newsletter')
        verbose_name_plural = _('newsletters')

    def get_absolute_url(self):
        return reverse('newsletter_detail', kwargs={'newsletter_slug': self.slug})

    def subscribe_url(self):
        return reverse('newsletter_subscribe_request', kwargs={'newsletter_slug': self.slug})

    def unsubscribe_url(self):
        return reverse('newsletter_unsubscribe_request', kwargs={'newsletter_slug': self.slug})

    def update_url(self):
        return reverse('newsletter_update_request', kwargs={'newsletter_slug': self.slug})

    def archive_url(self):
        return reverse('newsletter_archive', kwargs={'newsletter_slug': self.slug})

    def get_sender(self):
        return get_address(self.sender, self.email)

    def get_subscriptions(self):
        logger.debug(u'Looking up subscribers for %s', self)

        return Subscription.objects.filter(newsletter=self, subscribed=True)

    @classmethod
    def get_default(cls):
        try:
            return cls.objects.all()[0].pk
        except IndexError:
            return None


@python_2_unicode_compatible
class Subscription(models.Model):
    user = models.ForeignKey(
        AUTH_USER_MODEL, blank=True, null=True, verbose_name=_('user'),
        on_delete=models.CASCADE
    )

    name_field = models.CharField(
        db_column='name', max_length=30, blank=True, null=True,
        verbose_name=_('name'), help_text=_('optional')
    )

    def get_name(self):
        if self.user:
            return self.user.get_full_name()
        return self.name_field

    def set_name(self, name):
        if not self.user:
            self.name_field = name
    name = property(get_name, set_name)

    email_field = models.EmailField(
        db_column='email', verbose_name=_('e-mail'), db_index=True,
        blank=True, null=True
    )

    def get_email(self):
        if self.user:
            return self.user.email
        return self.email_field

    def set_email(self, email):
        if not self.user:
            self.email_field = email
    email = property(get_email, set_email)

    def update(self, action):
        """
        Update subscription according to requested action:
        subscribe/unsubscribe/update/, then save the changes.
        """

        assert action in ('subscribe', 'update', 'unsubscribe')

        # If a new subscription or update, make sure it is subscribed
        # Else, unsubscribe
        if action == 'subscribe' or action == 'update':
            self.subscribed = True
        else:
            self.unsubscribed = True

        logger.debug(
            _(u'Updated subscription %(subscription)s to %(action)s.'),
            {
                'subscription': self,
                'action': action
            }
        )

        # This triggers the subscribe() and/or unsubscribe() methods, taking
        # care of stuff like maintaining the (un)subscribe date.
        self.save()

    def _subscribe(self):
        """
        Internal helper method for managing subscription state
        during subscription.
        """
        logger.debug(u'Subscribing subscription %s.', self)

        self.subscribe_date = now()
        self.subscribed = True
        self.unsubscribed = False

    def _unsubscribe(self):
        """
        Internal helper method for managing subscription state
        during unsubscription.
        """
        logger.debug(u'Unsubscribing subscription %s.', self)

        self.subscribed = False
        self.unsubscribed = True
        self.unsubscribe_date = now()

    def save(self, *args, **kwargs):
        """
        Perform some basic validation and state maintenance of Subscription.
        TODO: Move this code to a more suitable place (i.e. `clean()`) and
        cleanup the code. Refer to comment below and
        https://docs.djangoproject.com/en/dev/ref/models/instances/#django.db.models.Model.clean
        """
        assert self.user or self.email_field, \
            _('Neither an email nor a username is set. This asks for '
              'inconsistency!')
        assert ((self.user and not self.email_field) or
                (self.email_field and not self.user)), \
            _('If user is set, email must be null and vice versa.')

        # This is a lame way to find out if we have changed but using Django
        # API internals is bad practice. This is necessary to discriminate
        # from a state where we have never been subscribed but is mostly for
        # backward compatibility. It might be very useful to make this just
        # one attribute 'subscribe' later. In this case unsubscribed can be
        # replaced by a method property.

        if self.pk:
            assert(Subscription.objects.filter(pk=self.pk).count() == 1)

            subscription = Subscription.objects.get(pk=self.pk)
            old_subscribed = subscription.subscribed
            old_unsubscribed = subscription.unsubscribed

            # If we are subscribed now and we used not to be so, subscribe.
            # If we user to be unsubscribed but are not so anymore, subscribe.
            if ((self.subscribed and not old_subscribed) or
               (old_unsubscribed and not self.unsubscribed)):
                self._subscribe()

                assert not self.unsubscribed
                assert self.subscribed

            # If we are unsubcribed now and we used not to be so, unsubscribe.
            # If we used to be subscribed but are not subscribed anymore,
            # unsubscribe.
            elif ((self.unsubscribed and not old_unsubscribed) or
                  (old_subscribed and not self.subscribed)):
                self._unsubscribe()

                assert not self.subscribed
                assert self.unsubscribed
        else:
            if self.subscribed:
                self._subscribe()
            elif self.unsubscribed:
                self._unsubscribe()

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

    ip = models.GenericIPAddressField(_("IP address"), blank=True, null=True)

    newsletter = models.ForeignKey(
        Newsletter, verbose_name=_('newsletter'), on_delete=models.CASCADE
    )

    create_date = models.DateTimeField(editable=False, default=now)

    activation_code = models.CharField(
        verbose_name=_('activation code'), max_length=40,
        default=make_activation_code
    )

    subscribed = models.BooleanField(
        default=False, verbose_name=_('subscribed'), db_index=True
    )
    subscribe_date = models.DateTimeField(
        verbose_name=_("subscribe date"), null=True, blank=True
    )

    # This should be a pseudo-field, I reckon.
    unsubscribed = models.BooleanField(
        default=False, verbose_name=_('unsubscribed'), db_index=True
    )
    unsubscribe_date = models.DateTimeField(
        verbose_name=_("unsubscribe date"), null=True, blank=True
    )

    def __str__(self):
        if self.name:
            return _(u"%(name)s <%(email)s> to %(newsletter)s") % {
                'name': self.name,
                'email': self.email,
                'newsletter': self.newsletter
            }

        else:
            return _(u"%(email)s to %(newsletter)s") % {
                'email': self.email,
                'newsletter': self.newsletter
            }

    class Meta:
        verbose_name = _('subscription')
        verbose_name_plural = _('subscriptions')
        unique_together = ('user', 'email_field', 'newsletter')

    def get_recipient(self):
        return get_address(self.name, self.email)

    def send_activation_email(self, action):
        assert action in ACTIONS, 'Unknown action: %s' % action

        (subject_template, text_template, html_template) = \
            self.newsletter.get_templates(action)

        variable_dict = {
            'subscription': self,
            'site': Site.objects.get_current(),
            'newsletter': self.newsletter,
            'date': self.subscribe_date,
            'STATIC_URL': settings.STATIC_URL,
            'MEDIA_URL': settings.MEDIA_URL
        }

        unescaped_context = get_context(variable_dict, autoescape=False)

        subject = subject_template.render(unescaped_context).strip()
        text = text_template.render(unescaped_context)

        message = EmailMultiAlternatives(
            subject, text,
            from_email=self.newsletter.get_sender(),
            to=[self.email]
        )

        if html_template:
            escaped_context = get_context(variable_dict)

            message.attach_alternative(
                html_template.render(escaped_context), "text/html"
            )

        message.send()

        logger.debug(
            u'Activation email sent for action "%(action)s" to %(subscriber)s '
            u'with activation code "%(action_code)s".', {
                'action_code': self.activation_code,
                'action': action,
                'subscriber': self
            }
        )

    def subscribe_activate_url(self):
        return reverse('newsletter_update_activate', kwargs={
            'newsletter_slug': self.newsletter.slug,
            'email': self.email,
            'action': 'subscribe',
            'activation_code': self.activation_code
        })

    def unsubscribe_activate_url(self):
        return reverse('newsletter_update_activate', kwargs={
            'newsletter_slug': self.newsletter.slug,
            'email': self.email,
            'action': 'unsubscribe',
            'activation_code': self.activation_code
        })

    def update_activate_url(self):
        return reverse('newsletter_update_activate', kwargs={
            'newsletter_slug': self.newsletter.slug,
            'email': self.email,
            'action': 'update',
            'activation_code': self.activation_code
        })


@python_2_unicode_compatible
class Article(models.Model):
    """
    An Article within a Message which will be send through a Submission.
    """

    sortorder = models.PositiveIntegerField(
        help_text=_('Sort order determines the order in which articles are '
                    'concatenated in a post.'),
        verbose_name=_('sort order'), blank=True
    )

    title = models.CharField(max_length=200, verbose_name=_('title'))
    text = models.TextField(verbose_name=_('text'))

    url = models.URLField(
        verbose_name=_('link'), blank=True, null=True
    )

    # Make this a foreign key for added elegance
    image = ImageField(
        upload_to='newsletter/images/%Y/%m/%d', blank=True, null=True,
        verbose_name=_('image')
    )

    # Message this article is associated with
    # TODO: Refactor post to message (post is legacy notation).
    post = models.ForeignKey(
        'Message', verbose_name=_('message'), related_name='articles',
        on_delete=models.CASCADE
    )

    class Meta:
        ordering = ('sortorder',)
        verbose_name = _('article')
        verbose_name_plural = _('articles')
        unique_together = ('post', 'sortorder')

    def __str__(self):
        return self.title

    def save(self, **kwargs):
        if self.sortorder is None:
            # If saving a new object get the next available Article ordering
            # as to assure uniqueness.
            self.sortorder = self.post.get_next_article_sortorder()

        super(Article, self).save()


def get_default_newsletter():
    return Newsletter.get_default()

@python_2_unicode_compatible
class Message(models.Model):
    """ Message as sent through a Submission. """

    title = models.CharField(max_length=200, verbose_name=_('title'))
    slug = models.SlugField(verbose_name=_('slug'))

    newsletter = models.ForeignKey(
        Newsletter, verbose_name=_('newsletter'), on_delete=models.CASCADE, default=get_default_newsletter
    )

    date_create = models.DateTimeField(
        verbose_name=_('created'), auto_now_add=True, editable=False
    )
    date_modify = models.DateTimeField(
        verbose_name=_('modified'), auto_now=True, editable=False
    )

    class Meta:
        verbose_name = _('message')
        verbose_name_plural = _('messages')
        unique_together = ('slug', 'newsletter')

    def __str__(self):
        try:
            return _(u"%(title)s in %(newsletter)s") % {
                'title': self.title,
                'newsletter': self.newsletter
            }
        except Newsletter.DoesNotExist:
            logger.warning('No newsletter has been set for this message yet.')
            return self.title

    def get_next_article_sortorder(self):
        """ Get next available sortorder for Article. """

        next_order = self.articles.aggregate(
            models.Max('sortorder')
        )['sortorder__max']

        if next_order:
            return next_order + 10
        else:
            return 10

    @cached_property
    def _templates(self):
        """Return a (subject_template, text_template, html_template) tuple."""
        return self.newsletter.get_templates('message')

    @property
    def subject_template(self):
        return self._templates[0]

    @property
    def text_template(self):
        return self._templates[1]

    @property
    def html_template(self):
        return self._templates[2]

    @classmethod
    def get_default(cls):
        try:
            return cls.objects.order_by('-date_create').all()[0]
        except IndexError:
            return None


@python_2_unicode_compatible
class Submission(models.Model):
    """
    Submission represents a particular Message as it is being submitted
    to a list of Subscribers. This is where actual queueing and submission
    happen.
    """
    class Meta:
        verbose_name = _('submission')
        verbose_name_plural = _('submissions')

    def __str__(self):
        return _(u"%(newsletter)s on %(publish_date)s") % {
            'newsletter': self.message,
            'publish_date': self.publish_date
        }

    @cached_property
    def extra_headers(self):
        return {
            'List-Unsubscribe': 'http://%s%s' % (
                Site.objects.get_current().domain,
                reverse('newsletter_unsubscribe_request',
                        args=[self.message.newsletter.slug])
            ),
        }

    def submit(self):
        subscriptions = self.subscriptions.filter(subscribed=True)

        logger.info(
            ugettext(u"Submitting %(submission)s to %(count)d people"),
            {'submission': self, 'count': subscriptions.count()}
        )

        assert self.publish_date < now(), \
            'Something smells fishy; submission time in future.'

        self.sending = True
        self.save()

        try:
            for idx, subscription in enumerate(subscriptions, start=1):
                if hasattr(settings, 'NEWSLETTER_EMAIL_DELAY'):
                    time.sleep(settings.NEWSLETTER_EMAIL_DELAY)
                if hasattr(settings, 'NEWSLETTER_BATCH_SIZE') and settings.NEWSLETTER_BATCH_SIZE > 0:
                    if idx % settings.NEWSLETTER_BATCH_SIZE == 0:
                        time.sleep(settings.NEWSLETTER_BATCH_DELAY)
                self.send_message(subscription)
            self.sent = True

        finally:
            self.sending = False
            self.save()

    def send_message(self, subscription):
        variable_dict = {
            'subscription': subscription,
            'site': Site.objects.get_current(),
            'submission': self,
            'message': self.message,
            'newsletter': self.newsletter,
            'date': self.publish_date,
            'STATIC_URL': settings.STATIC_URL,
            'MEDIA_URL': settings.MEDIA_URL
        }

        unescaped_context = get_context(variable_dict, autoescape=False)

        subject = self.message.subject_template.render(
            unescaped_context).strip()
        text = self.message.text_template.render(unescaped_context)

        message = EmailMultiAlternatives(
            subject, text,
            from_email=self.newsletter.get_sender(),
            to=[subscription.get_recipient()],
            headers=self.extra_headers,
        )

        if self.message.html_template:
            escaped_context = get_context(variable_dict)

            message.attach_alternative(
                self.message.html_template.render(escaped_context),
                "text/html"
            )

        try:
            logger.debug(
                ugettext(u'Submitting message to: %s.'),
                subscription
            )

            message.send()

        except Exception as e:
            # TODO: Test coverage for this branch.
            logger.error(
                ugettext(u'Message %(subscription)s failed '
                         u'with error: %(error)s'),
                {'subscription': subscription,
                 'error': e}
            )

    @classmethod
    def submit_queue(cls):
        todo = cls.objects.filter(
            prepared=True, sent=False, sending=False,
            publish_date__lt=now()
        )

        for submission in todo:
            submission.submit()

    @classmethod
    def from_message(cls, message):
        logger.debug(ugettext('Submission of message %s'), message)
        submission = cls()
        submission.message = message
        submission.newsletter = message.newsletter
        submission.save()
        try:
            submission.subscriptions.set(message.newsletter.get_subscriptions())
        except AttributeError:  # Django < 1.10
            submission.subscriptions = message.newsletter.get_subscriptions()
        return submission

    def save(self, **kwargs):
        """ Set the newsletter from associated message upon saving. """
        assert self.message.newsletter

        self.newsletter = self.message.newsletter

        return super(Submission, self).save()

    

    def get_absolute_url(self):
        assert self.newsletter.slug
        assert self.message.slug

        return reverse(
            'newsletter_archive_detail', kwargs={
                'newsletter_slug': self.newsletter.slug,
                'year': self.publish_date.year,
                'month': self.publish_date.month,
                'day': self.publish_date.day,
                'slug': self.message.slug
            }
        )

    newsletter = models.ForeignKey(
        Newsletter, verbose_name=_('newsletter'), editable=False,
        on_delete=models.CASCADE
    )
    message = models.ForeignKey(
        Message, verbose_name=_('message'), editable=True, null=False,
        on_delete=models.CASCADE
    )

    subscriptions = models.ManyToManyField(
        'Subscription',
        help_text=_('If you select none, the system will automatically find '
                    'the subscribers for you.'),
        blank=True, db_index=True, verbose_name=_('recipients'),
        limit_choices_to={'subscribed': True}
    )

    publish_date = models.DateTimeField(
        verbose_name=_('publication date'), blank=True, null=True,
        default=now, db_index=True
    )
    publish = models.BooleanField(
        default=True, verbose_name=_('publish'),
        help_text=_('Publish in archive.'), db_index=True
    )

    prepared = models.BooleanField(
        default=False, verbose_name=_('prepared'),
        db_index=True, editable=False
    )
    sent = models.BooleanField(
        default=False, verbose_name=_('sent'),
        db_index=True, editable=False
    )
    sending = models.BooleanField(
        default=False, verbose_name=_('sending'),
        db_index=True, editable=False
    )

def get_address(name, email):
    # Converting name to ascii for compatibility with django < 1.9.
    # Remove this when django 1.8 is no longer supported.
    if LooseVersion(django.get_version()) < LooseVersion('1.9'):
        name = name.encode('ascii', 'ignore').decode('ascii').strip()
    if name:
        return u'%s <%s>' % (name, email)
    else:
        return u'%s' % email