"""Proposal System Models
"""
from django.db import models
from django.conf import settings
from django.contrib.auth.models import User, AnonymousUser, Group
from django.core.validators import ValidationError, lazy_inter
from django.utils.translation import ugettext_lazy as _
from django.db.models import Q
from permissions import UserPermissions
from templatetags.propmeth import register
from pycon.tags.fields import MultiWordTagField
from pycon.core import *
from datetime import datetime
from django.core.cache import cache
from pycon.restructuredtext.validators import IsValidReST
import types, re

############################################################################
##
## Constants
##
AUTHORS_GROUP = None
SUBMITTERS_GROUP = None

REVIEW_SCORE_CHOICES = Choices([
    ('-1', '-1'),
    ('-0', '-0'),
    ('+0', '+0'),
    ('+1', '+1'),
])

PROPOSAL_STATUS_CHOICES = Choices([
    ('R', 'Under Review'),
    ('A', 'Accepted'),
    ('D', 'Declined'),
    ('C', 'Canceled'),
    ('W', 'Withdrawn'),
])

PROPOSAL_LEVEL_CHOICES = Choices([
    ('B', 'Beginner'),
    ('I', 'Intermediate'),
    ('A', 'Advanced'),
])

############################################################################
##
## Models
##
class Proposal(models.Model):
    """
    """
    title       = models.CharField(maxlength=180, unique=True,
                                   null=False, blank=False)
    summary     = models.TextField(blank=False)
    description = models.TextField(null=False, blank=False,
                    validator_list=[IsValidReST(
                        "Invalid ReST Markup (see error above): ", True)])
    duration    = models.PositiveIntegerField(blank=False, default=30)
    published   = models.BooleanField(help_text="Can authors view the reviews?",
                                      null=False, blank=False, default=False)
    published_description = models.BooleanField(default=False,
                        help_text=("Include the ReStructuredText Description "
                                   "in the published materials for this talk."))
    submitter   = models.ForeignKey(User,
                                    related_name='proposals_submitted_set')
    coauthors   = models.ManyToManyField(User, null=True, blank=True,
                                    filter_interface = models.HORIZONTAL,
                                    related_name='proposals_coauthored_set')
    reviewers   = models.ManyToManyField(User,
                                         limit_choices_to={
                                             'groups__name':'Reviewers'},
                                         filter_interface = models.HORIZONTAL,
                                         related_name='proposals_reviewing_set')
    status      = models.CharField(maxlength=1, null=False, blank=False,
                                   choices=PROPOSAL_STATUS_CHOICES,
                                   default=PROPOSAL_STATUS_CHOICES.default)
    level       = models.CharField("Difficulty Level", maxlength=1,
                                   null=False, blank=False,
                                   choices=PROPOSAL_LEVEL_CHOICES,
                                   default=PROPOSAL_LEVEL_CHOICES.default)
    categories  = MultiWordTagField(max_length=256, blank=False, null=False)
    submitted   = models.DateTimeField("submitted on", auto_now_add=True)
    updated     = models.DateTimeField("last updated on", auto_now=True)

    class Meta:
        ordering = ('id',)
        permissions = (("can_view_all_proposals",    "Can view all proposals"),
                       ("can_view_proposal_listing",
                                "Can view summary listing"),
                       ("can_view_proposal_stats",   "Can view stats"),
                       ("change_proposal_status", "Can change proposal status"),
                       ("can_view_all_authors_override",
                                "Can view all proposal authors (absolute)"),)

    class Admin:
        list_display  = ('id', 'title', 'submitter', 'status',
                         'submitted', 'updated')
        list_display_links = ('id', 'title')
        date_hierarchy = 'submitted'
        list_filter   = ('status', 'submitted', 'updated')
        search_fields = ('title', 'summary', 'description')

    def get_absolute_url(self):
        return settings.ROOT_URL + "proposals/%i/" % self.id

    def get_all_updated_sense(self, dtobj):
        return self.objects.filter(updated__gt=dtobj)

    def __unicode__(self):
        return self.title

    ## RED_FLAG: need to move to a 'objects' implementation
    @classmethod
    def needs_champion_set(cls):
        ids = [ p['id'] for p in cls.objects.filter(Q(review__score='-1')|
                        Q(review__score='+1')).distinct().values('id') ]
        return cls.objects.exclude(id__in=ids)

    @property
    def total_review_score(self):
        def s2v(score):
            scm = [-1.0, -0.5, 0.5, 1]
            return scm[REVIEW_SCORE_CHOICES.index(score)]
        res = sum(s2v(s['score']) for s in self.review_set.values('score'))
        if res>0: return "+%s" % (abs(int(res)) > 0 and abs(res) or 0)
        if res<0: return "-%s" % (abs(int(res)) > 0 and abs(res) or 0)
        return str(res)

    def get_review_rank(self):
        cache_name='prop_rank_' + repr(self.id)
        rank = cache.get(cache_name)
        if rank is not None: return rank
        reviews = self.review_set.all()
        if len(reviews) < 3: #rids - rrids:
            rank= 'missing_votes'
        else:
            scores = [ REVIEW_SCORE_CHOICES.index(review.score) - 2
                       for review in reviews ]
            if reduce(lambda x, y: x and y<0, scores, True):
                rank = 'all_negative'
            elif reduce(lambda x, y: x and y>=0, scores, True):
                rank = 'all_positive'
            else:
                rank = 'mixed'
        cache.set(cache_name, rank, 60*60*60)
        return rank

    def get_categories(self):
        return [ cat.strip() for cat in self.categories.split(',') ]

    @register.filter
    def get_review_score_for_user(self, user):
        try:
            return self.review_set.get(reviewer=user).get_score_display()
        except Review.DoesNotExist:
            return ""

    @register.filter('mark_proposal_as_seen_by') ## MEGA HACK!
    def mark_viewed(self, user):
        if not user.is_authenticated(): return ''
        vh, new = ViewHistory.objects.get_or_create(proposal=self, viewer=user)
        if new or vh.viewed_on < self.updated:
            vh.save()
        return '' ## Massive Hack because I am too lazy to write a tag

    @staticmethod
    @bind_prop_to_class(User, 'proposals_unassociated')
    def query_for_unassociated(user):
        return Proposal.objects.exclude(submitter=user).extra(where=[
            ("%d NOT IN (select propmgr_proposal_reviewers.user_id "
             "from propmgr_proposal_reviewers where "
             "propmgr_proposal_reviewers.proposal_id = propmgr_proposal.id)")
            %user.id,
            ("%d NOT IN (select propmgr_proposal_coauthors.user_id "
             "from propmgr_proposal_coauthors where "
             "propmgr_proposal_coauthors.proposal_id = propmgr_proposal.id)")
            %user.id,],
            select={
                'seen' : ('(select propmgr_proposal.updated <= '
                               'propmgr_viewhistory.viewed_on '
                           'from propmgr_viewhistory '
                           'where propmgr_viewhistory.viewer_id = %d and '
                                'propmgr_viewhistory.proposal_id = '
                                'propmgr_proposal.id)') % user.id  }
            ).select_related()

    @staticmethod
    @bind_prop_to_class(User, 'proposals_reviewing')
    def query_for_reviewer(user):
        '''Can be accessed as a static method on Proposal or as a special
        QuerySet property on User objects:
            user.proposals_reviewing.filter(...)
        The objects retuned will have an extra boolean attribute 'seen'
        which denotes if the user has seen this proposal sence its last
        modification.
        '''
        return user.proposals_reviewing_set.extra(select={
            'seen' : ('(select propmgr_proposal.updated <= '
                            'propmgr_viewhistory.viewed_on '
                       'from propmgr_viewhistory '
                       'where propmgr_viewhistory.viewer_id = %d and '
                            'propmgr_viewhistory.proposal_id = '
                            'propmgr_proposal.id)') % user.id  }
            ).select_related()

    @staticmethod
    @bind_prop_to_class(User, 'proposals_submitted')
    def query_for_submitter(user):
        '''Can be accessed as a static method on Proposal or as a special
        QuerySet property on User objects:
            user.proposals_submitted.filter(...)
        The objects retuned will have an extra boolean attribute 'seen'
        which denotes if the user has seen this proposal sence its last
        modification.
        '''
        return user.proposals_submitted_set.extra(select={
            'seen' : ('(select propmgr_proposal.updated <= '
                            'propmgr_viewhistory.viewed_on '
                       'from propmgr_viewhistory '
                       'where propmgr_viewhistory.viewer_id = %d and '
                            'propmgr_viewhistory.proposal_id = '
                                'propmgr_proposal.id)') % user.id  })

    @staticmethod
    @bind_prop_to_class(User, 'proposals_coauthored')
    def query_for_coauthor(user):
        '''Can be accessed as a static method on Proposal or as a special
        QuerySet property on User objects:
            user.proposals_coauthored.filter(...)
        The objects retuned will have an extra boolean attribute 'seen'
        which denotes if the user has seen this proposal sence its last
        modification.
        '''
        return user.proposals_coauthored_set.extra(select={
            'seen' : ('(select propmgr_proposal.updated <= '
                            'propmgr_viewhistory.viewed_on '
                       'from propmgr_viewhistory '
                       'where propmgr_viewhistory.viewer_id = %d and '
                            'propmgr_viewhistory.proposal_id = '
                                'propmgr_proposal.id)') % user.id  })
    @staticmethod
    @bind_prop_to_class(User, 'proposals_commentonly')
    def query_for_commentonly(user):
        return Proposal.objects.filter(comment__commenter=user).exclude(
            submitter=user).extra(where=[
            ("%d NOT IN (select propmgr_proposal_reviewers.user_id "
             "from propmgr_proposal_reviewers "
             "where propmgr_proposal_reviewers.proposal_id = "
                "propmgr_proposal.id)") %user.id,
            ("%d NOT IN (select propmgr_proposal_coauthors.user_id "
             "from propmgr_proposal_coauthors "
             "where propmgr_proposal_coauthors.proposal_id = "
                 "propmgr_proposal.id)") %user.id,],
            select={
                'seen' : ('(select propmgr_proposal.updated <= '
                                'propmgr_viewhistory.viewed_on '
                           'from propmgr_viewhistory '
                           'where propmgr_viewhistory.viewer_id = %d and '
                                'propmgr_viewhistory.proposal_id = '
                                    'propmgr_proposal.id)') % user.id  }
            ).distinct().select_related()


    def save(self):
        global AUTHORS_GROUP, SUBMITTERS_GROUP, INSTRUCTORS_GROUP
        isnew = self.id == None
        res = super(Proposal, self).save()
        if AUTHORS_GROUP is None:
            AUTHORS_GROUP = Group.objects.get(name='Authors')
        if SUBMITTERS_GROUP is None:
            SUBMITTERS_GROUP = Group.objects.get(name='Submitters')
        if isnew:
            self.submitter.groups.add(SUBMITTERS_GROUP)
            self.submitter.groups.add(AUTHORS_GROUP)
            self.submitter.save()
        for user in self.coauthors.all():
            if AUTHORS_GROUP not in user.groups.all():
                user.groups.add(AUTHORS_GROUP)
                user.save()
        return res

class AttachedFile(models.Model):
    """
    """
    proposal  = models.ForeignKey(Proposal,
                                  #edit_inline=models.TABULAR,
                                  num_in_admin=3)
    submitter = models.ForeignKey(User, core=True)
    submitted = models.DateTimeField("uploaded on", auto_now_add=True)
    file      = models.FileField(upload_to=settings.PROPOSAL_UPLOAD_TO)
    comment   = models.CharField(maxlength=100)
    published = models.BooleanField(default=False,
                help_text='Document can be included in conference materials')

    class Admin:
        list_display = ('get_file_short_name', '_get_proposal_id', 'proposal',
                        'submitter', 'submitted')
        list_filter = ('submitter', 'proposal', 'submitted')
        date_hierarchy = 'submitted'
        search_fields = ('proposal', 'comment')

    class Meta:
        ordering = ('submitted',)

    def save(self):
        ### HACK Fix for change manipulator inline bug!
        if self.id and self.submitted is None:
            self.submitted = AttachedFile.objects.get(pk=self.id).submitted
        super(AttachedFile, self).save()
        self.proposal.save()

    @short_description("id")
    def _get_proposal_id(self):
        return self.proposal_id

    @short_description("name")
    def get_file_short_name(self):
        return self.file.replace("\\", "/").split("/")[-1]

    def __unicode__(self):
        return unicode(self.file)

class Review(models.Model):
    """
    """
    proposal = models.ForeignKey(Proposal,
                                 #edit_inline=models.TABULAR,
                                 num_in_admin=3)
    reviewer = models.ForeignKey(User, core=True,
                                 limit_choices_to={'groups__name':'Reviewers'})
    score    = models.CharField(core=True, maxlength=2, null=False, blank=False,
                                choices=REVIEW_SCORE_CHOICES)
    comment  = models.TextField(core=True, null=False, blank=False,
                validator_list=[IsValidReST(
                    "Invalid ReST Markup (see inline error above): ", True)])
    updated  = models.DateTimeField("last updated on", auto_now=True)

    class Admin:
        list_display = ('_get_proposal_id', 'proposal', 'reviewer',
                        'score', 'updated')
        list_display_links = ('proposal',)
        list_filter = ('reviewer', 'score', 'proposal', 'updated')
        date_hierarchy = 'updated'
        search_fields = ('proposal', 'comment')
    class Meta:
        permissions = (("can_view_all_reviews",    "Can view all reviews"),)
        ordering = ('id',)

    @short_description("id")
    def _get_proposal_id(self):
        return self.proposal_id

    def save(self):
        super(Review, self).save()
        if self.reviewer not in self.proposal.reviewers.all():
            self.proposal.reviewers.add(self.reviewer)
        self.proposal.save()
        cache.delete('prop_rank_' + repr(self.proposal.id))

    def __unicode__(self):
        return self.score

class Comment(models.Model):
    """
    """
    proposal  = models.ForeignKey(Proposal,
                                  #edit_inline=models.TABULAR,
                                  num_in_admin=3)
    submitted = models.DateTimeField("submitted on", auto_now_add=True)
    commenter = models.ForeignKey(User, core=True)
    comment   = models.TextField(validator_list=[IsValidReST(
                    "Invalid ReST Markup (see inline error above): ", True)])

    class Admin:
        list_display = ('_get_proposal_id', 'proposal',
                        'commenter', 'submitted')
        list_display_links = ('proposal',)
        list_filter = ('commenter', 'proposal', 'submitted')
        date_hierarchy = 'submitted'
        search_fields = ('proposal', 'comment')
    class Meta:
        permissions = (("can_view_all_comments",    "Can view all comments"),)
        ordering = ('submitted',)

    def save(self):
        ### HACK Fix for change manipulator inline bug!
        if self.id and self.submitted is None:
            self.submitted = Comment.objects.get(pk=self.id).submitted
        super(Comment, self).save()
        self.proposal.save()

    @short_description("id")
    def _get_proposal_id(self):
        return self.proposal_id

    def __unicode__(self):
        return u"Comment by: " + unicode(self.commenter)

class ViewHistory(models.Model):
    """
    """
    proposal  = models.ForeignKey(Proposal)
    viewer    = models.ForeignKey(User)
    viewed_on = models.DateTimeField("viewed on", auto_now=True)


    @staticmethod
    @bind_meth_to_class(User, 'last_viewed_proposal_on')
    @bind_meth_to_class(AnonymousUser, 'last_viewed_proposal_on')
    @register.filter('last_viewed_proposal_on')
    def last_viewed(user, proposal):
        if user.is_anonymous(): return None
        try:
            vh = ViewHistory.objects.get(viewer=user, proposal=proposal)
            return vh.viewed_on
        except ViewHistory.DoesNotExist:
            return None
        return None

    @staticmethod
    def create_if_not_present(proposal, user):
        try:
            ViewHistory.objects.get(viewer=user, proposal=proposal)
        except ViewHistory.DoesNotExist:
            ViewHistory(viewer=user, proposal=proposal).save()
            return True
        return False

    @staticmethod
    @bind_meth_to_class(User)
    @bind_meth_to_class(AnonymousUser)
    @register.filter
    def has_seen_proposal(user, proposal):
        if user.is_anonymous():
            return True
        vh = None
        try:
            vh = ViewHistory.objects.get(viewer=user, proposal=proposal)
        except ViewHistory.DoesNotExist:
            #return proposal.updated <= user.last_login
            return False
        return proposal.updated <= vh.viewed_on

    @staticmethod
    @bind_meth_to_class(User)
    @bind_meth_to_class(AnonymousUser)
    @register.filter
    def has_not_seen_proposal(user, proposal):
        return not ViewHistory.have_seen_proposal(user, proposal)


def generate_proposal_stats():
    from django.contrib.contenttypes.models import ContentType
    from django.contrib.admin.models import LogEntry, CHANGE
    ct_proposal = ContentType.objects.get_for_model(Proposal)
    ct_attachment = ContentType.objects.get_for_model(AttachedFile)
    ct_comment    = ContentType.objects.get_for_model(Comment)
    ct_review   = ContentType.objects.get_for_model(Review)

    stats = {
        'prop_count': Proposal.objects.count(),
        'org_count': Group.objects.get(name='Organizers').user_set.count(),
        'sub_count': Group.objects.get(name='Submitters').user_set.count(),
        'auth_count': Group.objects.get(name='Authors').user_set.count(),
        'rev_count': Group.objects.get(name='Reviewers').user_set.count(),
        'vote_count': Review.objects.count(),
        'user_count': User.objects.filter(is_active=True).count(),
        'staff_count': User.objects.filter(is_active=True,
                                           is_staff=True).count(),
        'comment_count': Comment.objects.count(),
        'attachment_count': AttachedFile.objects.count(),
        'dur_t_count': Proposal.objects.filter(duration=30).count(),
        'dur_f_count': Proposal.objects.filter(duration=45).count(),
        'dur_o_count': Proposal.objects.exclude(duration=45,
                                                duration=30).count(),
        'history_count': LogEntry.objects.filter(content_type__in=[
                    ct_proposal,ct_attachment,ct_comment,ct_review]).count(),
        'prop_edit_count': LogEntry.objects.filter(content_type=ct_proposal,
                                                   action_flag=CHANGE).count(),
        'vote_edit_count': LogEntry.objects.filter(content_type=ct_review,
                                                   action_flag=CHANGE).count(),
        'mixed': 0,
        'missing_votes': 0,
        'all_negative': 0,
        'all_positive': 0,
    }
    for key, name in PROPOSAL_LEVEL_CHOICES:
        stats[name] = Proposal.objects.filter(level=key).count()

    proposals = Proposal.objects.all()
    for p in proposals:
        name = p.get_review_rank()
        data = stats.setdefault(name, 0)
        stats[name] = data + 1
    stats['needs_champion'] = Proposal.needs_champion_set().count()
    return stats
