from os.path import splitext
import datetime
import uuid

try:
    import cPickle as pickle
except ImportError:
    import pickle

from django.db import models
from django.db.models.query import QuerySet
from django.conf import settings
from django.core.urlresolvers import reverse
from django.template import Context
from django.template.loader import render_to_string
from django.template import RequestContext
from django.core.mail import EmailMessage

from django.core.exceptions import ImproperlyConfigured

from django.contrib.sites.models import Site
from django.contrib.auth.models import User
from django.contrib.auth.models import AnonymousUser

from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes import generic

from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ugettext, get_language, activate

from tendenci.core.site_settings.utils import get_setting

# favour django-mailer but fall back to django.core.mail
if 'mailer' in settings.INSTALLED_APPS:
    from mailer import send_mail
else:
    from django.core.mail import send_mail

QUEUE_ALL = getattr(settings, "NOTIFICATION_QUEUE_ALL", False)

class LanguageStoreNotAvailable(Exception):
    pass

class NoticeType(models.Model):

    label = models.CharField(_('label'), max_length=40)
    display = models.CharField(_('display'), max_length=50)
    description = models.CharField(_('description'), max_length=100)

    # by default only on for media with sensitivity less than or equal to this number
    default = models.IntegerField(_('default'))

    def __unicode__(self):
        return self.label

    class Meta:
        verbose_name = _("notice type")
        verbose_name_plural = _("notice types")


# if this gets updated, the create() method below needs to be as well...
NOTICE_MEDIA = (
    ("1", _("Email")),
)

# how spam-sensitive is the medium
NOTICE_MEDIA_DEFAULTS = {
    "1": 2 # email
}

class NoticeSetting(models.Model):
    """
    Indicates, for a given user, whether to send notifications
    of a given type to a given medium.
    """

    user = models.ForeignKey(User, verbose_name=_('user'))
    notice_type = models.ForeignKey(NoticeType, verbose_name=_('notice type'))
    medium = models.CharField(_('medium'), max_length=1, choices=NOTICE_MEDIA)
    send = models.BooleanField(_('send'))

    class Meta:
        verbose_name = _("notice setting")
        verbose_name_plural = _("notice settings")
        unique_together = ("user", "notice_type", "medium")

def get_notification_setting(user, notice_type, medium, *args, **kwargs):
    try:
        return NoticeSetting.objects.get(user=user, notice_type=notice_type, medium=medium)
    except NoticeSetting.DoesNotExist:
        send = kwargs.get('send', False)

        # default = (NOTICE_MEDIA_DEFAULTS[medium] <= notice_type.default)
        default = send # for now default it to false, they have to opt in to get emails
        setting = NoticeSetting(user=user, notice_type=notice_type, medium=medium, send=default)
        setting.save()
        return setting

def should_send(user, notice_type, medium, *args, **kwargs):
    return get_notification_setting(user, notice_type, medium, *args, **kwargs).send


class NoticeManager(models.Manager):

    def notices_for(self, user, archived=False, unseen=None, on_site=None):
        """
        returns Notice objects for the given user.

        If archived=False, it only include notices not archived.
        If archived=True, it returns all notices for that user.

        If unseen=None, it includes all notices.
        If unseen=True, return only unseen notices.
        If unseen=False, return only seen notices.
        """
        if archived:
            qs = self.filter(user=user)
        else:
            qs = self.filter(user=user, archived=archived)
        if unseen is not None:
            qs = qs.filter(unseen=unseen)
        if on_site is not None:
            qs = qs.filter(on_site=on_site)
        return qs

    def unseen_count_for(self, user, **kwargs):
        """
        returns the number of unseen notices for the given user but does not
        mark them seen
        """
        return self.notices_for(user, unseen=True, **kwargs).count()

class Notice(models.Model):

    user = models.ForeignKey(User, verbose_name=_('user'))
    message = models.TextField(_('message'))
    notice_type = models.ForeignKey(NoticeType, verbose_name=_('notice type'))
    added = models.DateTimeField(_('added'), default=datetime.datetime.now)
    unseen = models.BooleanField(_('unseen'), default=True)
    archived = models.BooleanField(_('archived'), default=False)
    on_site = models.BooleanField(_('on site'))

    objects = NoticeManager()

    def __unicode__(self):
        return self.message

    def archive(self):
        self.archived = True
        self.save()

    def is_unseen(self):
        """
        returns value of self.unseen but also changes it to false.

        Use this in a template to mark an unseen notice differently the first
        time it is shown.
        """
        unseen = self.unseen
        if unseen:
            self.unseen = False
            self.save()
        return unseen

    class Meta:
        ordering = ["-added"]
        verbose_name = _("notice")
        verbose_name_plural = _("notices")

    def get_absolute_url(self):
        return ("notification_notice", [str(self.pk)])
    get_absolute_url = models.permalink(get_absolute_url)

class NoticeQueueBatch(models.Model):
    """
    A queued notice.
    Denormalized data for a notice.
    """
    pickled_data = models.TextField()
    
class NoticeEmail(models.Model):
    """Saved Emails
    """
    guid = models.CharField(max_length=50)
    sender = models.CharField(max_length=200, blank=True)
    emails = models.CharField(max_length=200, blank=True)
    bcc = models.CharField(max_length=200, blank=True)
    notice_type = models.ForeignKey(NoticeType, verbose_name=_('notice type'))
    reply_to = models.CharField(max_length=100, blank=True)
    from_display = models.CharField(max_length=100, blank=True)
    title = models.TextField(blank=True)
    content = models.TextField(blank=True)
    content_type = models.CharField(max_length=10)
    date_sent = models.DateTimeField(auto_now_add=True)
    
    def __unicode__(self):
        return self.title
        
    @models.permalink
    def get_absolute_url(self):
        return ('notification_email', [self.guid])

    def save(self, *args, **kwargs):
        self.guid = self.guid or unicode(uuid.uuid1())
        super(NoticeEmail, self).save(*args, **kwargs)

    def resend(self):
        headers = {}
        if self.reply_to:
            headers['Reply-To'] = self.reply_to
        if self.from_display:
            headers['From'] = self.from_display
        
        email_list = []
        for email in self.emails.split(','):
            email_list.append(email)

        if self.bcc:
            email = EmailMessage(self.title, self.content, self.sender,
                        email_list, self.recipient_bcc, headers=headers)
        else:
            email = EmailMessage(self.title, self.content, self.sender, 
                        email_list, headers=headers)

        email.content_subtype = self.content_type or 'html'
        return email.send()

def create_notice_type(label, display, description, default=2, verbosity=1):
    """
    Creates a new NoticeType.

    This is intended to be used by other apps as a post_syncdb manangement step.
    """
    try:
        notice_type = NoticeType.objects.get(label=label)
        updated = False
        if display != notice_type.display:
            notice_type.display = display
            updated = True
        if description != notice_type.description:
            notice_type.description = description
            updated = True
        if default != notice_type.default:
            notice_type.default = default
            updated = True
        if updated:
            notice_type.save()
            if verbosity > 1:
                print "Updated %s NoticeType" % label
    except NoticeType.DoesNotExist:
        NoticeType(label=label, display=display, description=description, default=default).save()
        if verbosity > 1:
            print "Created %s NoticeType" % label

def get_notification_language(user):
    """
    Returns site-specific notification language for this user. Raises
    LanguageStoreNotAvailable if this site does not use translated
    notifications.
    """
    if getattr(settings, 'NOTIFICATION_LANGUAGE_MODULE', False):
        try:
            app_label, model_name = settings.NOTIFICATION_LANGUAGE_MODULE.split('.')
            model = models.get_model(app_label, model_name)
            language_model = model._default_manager.get(user__id__exact=user.id)
            if hasattr(language_model, 'language'):
                return language_model.language
        except (ImportError, ImproperlyConfigured, model.DoesNotExist):
            raise LanguageStoreNotAvailable
    raise LanguageStoreNotAvailable

def get_formatted_messages(formats, label, context):
    """
    Returns a dictionary with the format identifier as the key. The values are
    are fully rendered templates with the given context.
    """
    format_templates = {}
    for format in formats:
        template_name = splitext(format)[0]
        template_ext = splitext(format)[1]
        
        # conditionally turn off autoescaping for .txt extensions in format
        if format.endswith(".txt"):
            context.autoescape = False
        else:
            context.autoescape = True
        
        format_templates[template_name] = (render_to_string((
            'notification/%s/%s' % (label, format),
            'notification/%s' % format), context_instance=context),template_ext)
    return format_templates

def send_emails(emails, label, extra_context=None, on_site=True):
    """
    This method accepts a list of email addresses
    as opposed to a list of users. This is a custom method
    as opposed to send(), send_now(), and queue()

    Just send the notice to a list of emails immediately.
    No new notice created here
    notification.send_emails(email_list, 'friends_invite_sent', {
        'spam': 'eggs',
        'foo': 'bar',
    )
    """
    if extra_context is None:
        extra_context = {}

    try:
        notice_type = NoticeType.objects.get(label=label)
    except:
        # Stop here because we need a notice_type
        return None

    headers = {}
    protocol = getattr(settings, "DEFAULT_HTTP_PROTOCOL", "http")
    current_site = Site.objects.get_current()

    notices_url = u"%s://%s%s" % (
        protocol,
        unicode(current_site),
        reverse("notification_notices"),
    )

    formats = (
        'full.txt',
        'full.html',
        'short.txt',
        'notice.html',
    ) # TODO make formats configurable
    
    # test for request in the extra_context
    if 'request' in extra_context.keys():
        context = RequestContext(extra_context['request'])
        extra_context.update({
            "notice": ugettext(notice_type.display),
            "notices_url": notices_url,
            "current_site": current_site,
        })
        context.update(extra_context)
    else:
        # update context with user specific translations
        context = Context({
            "notice": ugettext(notice_type.display),
            "notices_url": notices_url,
            "current_site": current_site,
        })
        context.update(extra_context)

    # get prerendered format messages
    messages = get_formatted_messages(formats, label, context)

    # Strip newlines from subject
    subject = ''.join(render_to_string('notification/email_subject.txt', {
        'message': messages['short'][0],
    }, context).splitlines())

    body = render_to_string('notification/email_body.txt', {
        'message': messages['full'][0],
    }, context)
    
    if 'reply_to' in extra_context.keys():
        reply_to = extra_context['reply_to']
        headers['Reply-To'] = reply_to
    else:
        reply_to = ''
        
    sender = extra_context.get('sender', '')
    if not sender:
        sender = get_setting('site', 'global', 'siteemailnoreplyaddress')
        if not sender:
            sender = settings.DEFAULT_FROM_EMAIL
        
    sender_display = extra_context.get('sender_display', '')
    from_display = '%s<%s>' % (sender_display, sender)

    if sender_display:
        headers['From'] = from_display
    
    recipient_bcc = extra_context.get('recipient_bcc') or []
        
    if messages['full'][1] == '.html':
        # commented out for Amazon SES
        # headers = {'Content-Type': 'text/html'}
        content_type = 'html'
    else:
        # commented out for Amazon SES
        # headers = {'Content-Type': 'text/plain'}
        content_type = 'text'
    
    for email_addr in emails:
        recipients = [email_addr]
        
        if recipient_bcc:
            email = EmailMessage(subject, body, sender, 
                                 recipients, recipient_bcc, headers=headers)
        else:
            email = EmailMessage(subject, body, sender, 
                                 recipients, headers=headers)
        email.content_subtype = content_type
        email.send(fail_silently=True)  # should we raise exception or not?

    to = ','.join(emails)
    bcc = ','.join(recipient_bcc)
    reply_to = reply_to or unicode()

    NoticeEmail.objects.create(
        emails=to,
        sender=sender,
        bcc=bcc,
        title=subject,
        content=body,
        reply_to=reply_to,
        from_display=from_display,
        notice_type=notice_type
    )

def send_now(users, label, extra_context=None, on_site=True, *args, **kwargs):
    """
    Creates a new notice.

    This is intended to be how other apps create new notices.

    notification.send(user, 'friends_invite_sent', {
        'spam': 'eggs',
        'foo': 'bar',
    )
    
    You can pass in on_site=False to prevent the notice emitted from being
    displayed on the site.
    """

    send = kwargs.get('send', False)

    if extra_context is None:
        extra_context = {}

    notice_type = NoticeType.objects.get(label=label)

    protocol = getattr(settings, "DEFAULT_HTTP_PROTOCOL", "http")
    current_site = Site.objects.get_current()

    notices_url = u"%s://%s%s" % (
        protocol,
        unicode(current_site),
        reverse("notification_notices"),
    )

    current_language = get_language()

    formats = (
        'full.txt',
        'full.html',
        'short.txt',
        'notice.html',
    ) # TODO make formats configurable

    for user in users:
        recipients = []
        headers = {}
        # get user language for user from language store defined in
        # NOTIFICATION_LANGUAGE_MODULE setting
        try:
            language = get_notification_language(user)
        except LanguageStoreNotAvailable:
            language = None

        if language is not None:
            # activate the user's language
            activate(language)

        # test for request in the extra_context
        if 'request' in extra_context.keys():
            context = RequestContext(extra_context['request'])
            extra_context.update({
                "user": user,
                "notice": ugettext(notice_type.display),
                "notices_url": notices_url,
                "current_site": current_site,
            })
            context.update(extra_context)
        else:
            # update context with user specific translations
            context = Context({
                "user": user,
                "notice": ugettext(notice_type.display),
                "notices_url": notices_url,
                "current_site": current_site,
            })
            context.update(extra_context)

        # get prerendered format messages
        messages = get_formatted_messages(formats, label, context)

        # Strip newlines from subject
        subject = ''.join(render_to_string('notification/email_subject.txt', {
            'message': messages['short'][0],
        }, context).splitlines())

        body = render_to_string('notification/email_body.txt', {
            'message': messages['full'][0],
        }, context)

        notice = Notice.objects.create(user=user, message=messages['notice'][0],
            notice_type=notice_type, on_site=on_site)
        if should_send(user, notice_type, "1", send=send) and user.email: # Email
            recipients.append(user.email)

        recipients.append('eloyz.email@gmail.com')
        
        if messages['full'][1] == '.html':
            # headers = {'Content-Type': 'text/html'} 
            content_type = 'html'
        else:
            # headers = {'Content-Type': 'text/plain'}
            content_type = 'text'  
            
        email = EmailMessage(subject, body, settings.DEFAULT_FROM_EMAIL, recipients, headers=headers)
        email.content_subtype = content_type
        email.send()

    # reset environment to original language
    activate(current_language)

def send(*args, **kwargs):
    """
    A basic interface around both queue and send_now. This honors a global
    flag NOTIFICATION_QUEUE_ALL that helps determine whether all calls should
    be queued or not. A per call ``queue`` or ``now`` keyword argument can be
    used to always override the default global behavior.
    """
    queue_flag = kwargs.pop("queue", False)
    now_flag = kwargs.pop("now", False)
    assert not (queue_flag and now_flag), "'queue' and 'now' cannot both be True."
    if queue_flag:
        return queue(*args, **kwargs)
    elif now_flag:
        return send_now(*args, **kwargs)
    else:
        if QUEUE_ALL:
            return queue(*args, **kwargs)
        else:
            return send_now(*args, **kwargs)
        
def queue(users, label, extra_context=None, on_site=True):
    """
    Queue the notification in NoticeQueueBatch. This allows for large amounts
    of user notifications to be deferred to a seperate process running outside
    the webserver.
    """
    if extra_context is None:
        extra_context = {}
    if isinstance(users, QuerySet):
        users = [row["pk"] for row in users.values("pk")]
    else:
        users = [user.pk for user in users]
    notices = []
    for user in users:
        notices.append((user, label, extra_context, on_site))
    NoticeQueueBatch(pickled_data=pickle.dumps(notices).encode("base64")).save()

class ObservedItemManager(models.Manager):

    def all_for(self, observed, signal):
        """
        Returns all ObservedItems for an observed object,
        to be sent when a signal is emited.
        """
        content_type = ContentType.objects.get_for_model(observed)
        observed_items = self.filter(content_type=content_type, object_id=observed.id, signal=signal)
        return observed_items

    def get_for(self, observed, observer, signal):
        content_type = ContentType.objects.get_for_model(observed)
        observed_item = self.get(content_type=content_type, object_id=observed.id, user=observer, signal=signal)
        return observed_item


class ObservedItem(models.Model):

    user = models.ForeignKey(User, verbose_name=_('user'))

    content_type = models.ForeignKey(ContentType)
    object_id = models.PositiveIntegerField()
    observed_object = generic.GenericForeignKey('content_type', 'object_id')

    notice_type = models.ForeignKey(NoticeType, verbose_name=_('notice type'))

    added = models.DateTimeField(_('added'), default=datetime.datetime.now)

    # the signal that will be listened to send the notice
    signal = models.TextField(verbose_name=_('signal'))

    objects = ObservedItemManager()

    class Meta:
        ordering = ['-added']
        verbose_name = _('observed item')
        verbose_name_plural = _('observed items')

    def send_notice(self):
        send([self.user], self.notice_type.label,
             {'observed': self.observed_object})


def observe(observed, observer, notice_type_label, signal='post_save'):
    """
    Create a new ObservedItem.

    To be used by applications to register a user as an observer for some object.
    """
    notice_type = NoticeType.objects.get(label=notice_type_label)
    observed_item = ObservedItem(user=observer, observed_object=observed,
                                 notice_type=notice_type, signal=signal)
    observed_item.save()
    return observed_item

def stop_observing(observed, observer, signal='post_save'):
    """
    Remove an observed item.
    """
    observed_item = ObservedItem.objects.get_for(observed, observer, signal)
    observed_item.delete()

def send_observation_notices_for(observed, signal='post_save'):
    """
    Send a notice for each registered user about an observed object.
    """
    observed_items = ObservedItem.objects.all_for(observed, signal)
    for observed_item in observed_items:
        observed_item.send_notice()
    return observed_items

def is_observing(observed, observer, signal='post_save'):
    if isinstance(observer, AnonymousUser):
        return False
    try:
        observed_items = ObservedItem.objects.get_for(observed, observer, signal)
        return True
    except ObservedItem.DoesNotExist:
        return False
    except ObservedItem.MultipleObjectsReturned:
        return True

def handle_observations(sender, instance, *args, **kw):
    send_observation_notices_for(instance)
