""" Django CAS 2.0 authentication models """

from datetime import datetime, timedelta
from django.conf import settings
from django.contrib.auth import BACKEND_SESSION_KEY
from django.contrib.auth.models import User
from django.contrib.auth.signals import user_logged_in, user_logged_out
from django.contrib.sessions.models import Session
from django.core.exceptions import ImproperlyConfigured
from django.db import models
from django.db.models.signals import post_save, post_delete
from django.dispatch.dispatcher import receiver
from django.utils.translation import ugettext_lazy as _
from django_cas.exceptions import CasTicketException
from urllib import urlencode, urlopen
from urlparse import urljoin
from xml.dom import minidom

__all__ = ['Tgt']

class Tgt(models.Model):
    """
    Model representing CAS ticket granting tickets. It can be used
    to retrieve proxy granting tickets for backend web services 
    """
    username = models.CharField(_('username'), max_length = 255, unique = True)
    tgt = models.CharField(_('ticket granting ticket'), max_length = 255)
    
    class Meta:
        db_table = 'django_cas_tgt'
        verbose_name = _('ticket granting ticket')
        verbose_name_plural = _('ticket granting tickets')


    @classmethod
    def get_tgt_for_user(self, user):
        """
        Returns the ticket granting ticket stored for a user in the database.

        The user can be specified as a User object or its Django username.
        Raises Tgt.DoesNotExist if the ticket can't be found.
        """
        if isinstance(user, User):
            return Tgt.objects.get(username = user.username)
        return Tgt.objects.get(username = user)


    def get_proxy_ticket_for_service(self, service):
        """
        Returns a string representing a proxy ticket for the given service as
        given by the CAS server. This ticket can then be used to authenticate
        to the backend service, typically in a 'ticket' parameter, but may
        be in other manners detailed by the service specification.
        """
        if not settings.CAS_PROXY_CALLBACK:
            raise ImproperlyConfigured("No proxy callback set in settings")

        params = {'pgt': self.tgt, 'targetService': service}
        page = urlopen(urljoin(settings.CAS_SERVER_URL, 'proxy') + '?' + urlencode(params))

        try:
            response = minidom.parseString(page.read())
            if response.getElementsByTagName('cas:proxySuccess'):
                return response.getElementsByTagName('cas:proxyTicket')[0].firstChild.nodeValue
            raise CasTicketException("Failed to get proxy ticket")
        finally:
            page.close()


class PgtIOU(models.Model):
    """ Proxy granting ticket and IOU """
    pgtIou = models.CharField(_('proxy ticket IOU'), max_length = 255, unique = True)
    tgt = models.CharField(_('ticket granting ticket'), max_length = 255)
    timestamp = models.DateTimeField(auto_now = True)

    class Meta:
        db_table = 'django_cas_pgtiou'
        verbose_name = _('proxy ticket IOU')
        verbose_name_plural = _('proxy ticket IOUs')


class SessionServiceTicket(models.Model):
    """ Handles a mapping between the CAS Service Ticket and the session key
        as long as user is connected to an application that uses the CASBackend
        for authentication
    """
    service_ticket = models.CharField(_('service ticket'), max_length=255, primary_key=True)
    session_key = models.CharField(_('session key'), max_length=40)


    class Meta:
        db_table = 'django_cas_session_service_ticket'
        verbose_name = _('session service ticket')
        verbose_name_plural = _('session service tickets')


    def get_session(self):
        """ Searches the session in store and returns it """
        session_engine = __import__(name=settings.SESSION_ENGINE, fromlist=['SessionStore'])
        SessionStore = getattr(session_engine, 'SessionStore')
        return SessionStore(session_key=self.session_key)


    def __unicode__(self):
        return self.ticket


def _is_cas_backend(session):
    """ Checks if the auth backend is CASBackend """
    backend = session.get(BACKEND_SESSION_KEY)
    from django_cas.backends import CASBackend
    return backend == '{0.__module__}.{0.__name__}'.format(CASBackend)


@receiver(user_logged_in)
def map_service_ticket(sender, **kwargs):
    """ Creates the mapping between a session key and a service ticket after user
        logged in """
    request = kwargs['request']
    ticket = request.GET.get('ticket')
    if settings.CAS_SINGLE_SIGN_OUT and ticket and _is_cas_backend(request.session):
        session_key = request.session.session_key
        SessionServiceTicket.objects.create(service_ticket=ticket,
                                            session_key=session_key)


@receiver(user_logged_out)
def delete_service_ticket(sender, **kwargs):
    """ Deletes the mapping between session key and service ticket after user
        logged out """
    request = kwargs['request']
    if settings.CAS_SINGLE_SIGN_OUT and _is_cas_backend(request.session):
        session_key = request.session.session_key
        SessionServiceTicket.objects.filter(session_key=session_key).delete()


@receiver(post_delete, sender=Session)
def delete_old_session_service_tickets(sender, instance, **kwargs):
    """ Deletes session service tickets when mapped sessions are deleted 
        from the database. Note that this does not catch the case with
        cached sessions that are not mapped to ordinary Django models.
        
        You have to run the django-admin command purge_session_service_tickets
        if you don't have sessions mapped to the database.
    """
    if settings.CAS_SINGLE_SIGN_OUT:
        SessionServiceTicket.objects.filter(session_key=instance.session_key).delete()


@receiver(post_save, sender=PgtIOU)
def delete_old_tickets(**kwargs):
    """ Delete tickets if they are over 2 days old 
        kwargs = ['raw', 'signal', 'instance', 'sender', 'created']
    """
    sender = kwargs.get('sender')
    now = datetime.now()
    expire = datetime(now.year, now.month, now.day) - timedelta(days=2)
    sender.objects.filter(timestamp__lt=expire).delete()