from django.db import models
from django.utils.translation import ugettext_lazy as _
from asena.utils import random_chars, get_default_setting
import string, random
from datetime import timedelta, datetime

import logging, pprint
logger = logging.getLogger('to_terminal')

"""
" Token Exceptions
"""

class TokenException(Exception):
    """ A general abstract exception related to tokens.
    "
    """
    def __init__(self, token=None):
        """
        " :param token: the invalid token passed in
        " :type token: object (typically a Token or string)
        """
        self.token = token
        
    def __repr__(self):
        return "%s: %s"%(self.__class__.__name__, self.token)
    
class InvalidTokenException(TokenException):
    """ Thrown when a token is invalid (not in the database)
    """
    def __init__(self, *args, **kwargs):
        super(AutorizationException, self).__init__()
    
class DisabledTokenException(TokenException):
    """ Thrown when a token is valid but has been disabled.
    """
    def __init__(self, *args, **kwargs):
        super(AutorizationException, self).__init__()
    
class AuthorizationException(TokenException):
    """ Thrown when a user has tried to access a page for which they need a 
        token.
    """
    def __init__(self, *args, **kwargs):
        super(AuthorizationException, self).__init__()

class TokenSet(models.Model):
    generated = models.DateTimeField(auto_now=True)
    name = models.CharField(blank=True, null=True, max_length=200)
    disabled = models.BooleanField(default=False)
    expiration = models.DateTimeField(blank=True, null=True)
    session_timeout = models.CharField(blank=True, null=True, max_length=20,
        help_text=_("Time Delta format (e.g. 'HH,MM,SS')"))
    
    @classmethod
    def generate_set(Klass, count, length=10, name=None):
        """ Convenience method to generate a set of tokens.
        
         :param count: Number of tokens to generate.
         :type count: int
         
         :param name: An optional name to use for the token set.
         :type name: str
         
         :return: A new TokenSet with the newly-generated tokens.
        """
        token_set = Klass.objects.create(name=name)
        for i in range(0, count):
            Token.generate(length=length, token_set=token_set, comment=name)
        return token_set
    
    def has_expired(self):
        return ( self.expiration ) and (datetime.now() > self.expiration)
    
    def get_tokens(self):
        return Token.objects.filter(token_set__pk=self.pk)
            
    def __unicode__(self):
        ts = self.get_tokens().count()
        if self.name:
            return "%s (%d tokens)"%(self.name, ts)
        return "%d tokens generated %s"%(ts, self.generated)

    class Meta:
        abstract = False
        app_label = "asena"
        verbose_name = "Token Set"
        verbose_name_plural = "Token Sets"


class Token(models.Model):
    """ A randomly-generated string.
    
    :param value: The value of the string (generated by machine).
    :type value: str
    
    :param comment: A comment for the token (optional).
    :type comment: str
    
    :type token_set: A foreign key to TokenSet.
    :type token_set: models.ForeignKey
    
    """
    value = models.CharField(max_length=200)
    comment = models.TextField(max_length=1000, null=True, blank=True)
    token_set = models.ForeignKey("TokenSet", related_name="tokens",
                                  related_query_name="tokens")
    disabled = models.BooleanField(default=False)
    expiration = models.DateTimeField(blank=True, null=True)
    session_timeout = models.CharField(blank=True, null=True, max_length=20,
        help_text=_("Note that the token will expire when session" +
            " cookies are cleared."))
    
    _REQUEST_KEY='token'
    
    @classmethod
    def generate(Klass, length, token_set, comment=None,
                 valid_characters=str(string.letters + string.digits)):
        """ Generate a random token.
        
        :param length: The length of the token.
        :type length: int
        
        :param token_set: The assoicated token_set.
        :type token_set: TokenSet
        
        :param comment: A comment for the token (optional).
        :type comment: str
        
        :param valid_characters: A string of valid characters to use for the
            token. Optional. Default is string.letters + string.digits.
        :type valid_characters: str
        
        :return: The token just generated.
        """
        global random_chars
        val = random_chars(valid_characters, length)
        token = Klass.objects.create(token_set=token_set, comment=comment,
                                    value=val)
        return token
    
    def is_disabled(self):
        return (self.token_set.has_expired() or 
            self.token_set.disabled)
    
    def has_expired(self):
        return self.get_expiration() and (self.get_expiration() > 
            datetime.now())
            
    """
    " Methods for the session
    """
    
    def get_session(self):
        """ Get the dict values required for a session variable.
        """
        session_name_key = get_default_setting('ASENA_SESSION_NAME')
        session_time_key = get_default_setting('ASENA_SESSION_TIMEOUT_NAME')
        datetime_format = get_default_setting('ASENA_DATETIME_FORMAT')

        exp = self.get_session_expiration().strftime(datetime_format)
        
        return {
            session_name_key : self.value,
            session_time_key : exp,
        }
    
    def get_session_timeout(self, as_timedelta=False):
        """ Get the current session timeout. If it's null (or "0,0"), then
        return the TokenSet's session timeout.
        
        :param as_timedelta: True if we should conver to timedelta.
        :type as_timedelta: bool
        
        :return: The session timeout value.
        """
        val = self.token_set.session_timeout
        if (self.session_timeout) or (self.session_timeout != '0,0'):
            val = self.token_set.session_timeout
        if as_timedelta and val:
            (hours, minutes) = val.split(',')
            return timedelta(hours=int(hours), minutes=int(minutes))
        return val
    
    def get_session_expiration(self, init=datetime.now()):
        """ Get the timeout for the token. This will be calculated from the
        initial time ``init``
        
        :param init: The initial datetime. Default is datetime.datetime.now()
        :type init: datetime.
        """
        timeout = self.get_session_timeout()
        if timeout:
            return init + self.get_session_timeout(True)
        return None
        
    
    def get_expiration(self):
        """ Get either the token's expiration or (if the token set has an
            expiration) the token set's expiration.
            
            :return: The token expiration.
        """
        if self.token_set.expiration:
            return self.token_set.expiration
        return self.expiration
    
    @classmethod
    def exists(self, value):
        logger.debug("Checking if token value '%s' exists..."%value)
        return Token.objects.filter(value=value).exists()
    
    @classmethod
    def is_valid(self, value):
        if Token.exists(value):
            t = Token.objects.get(value=value)
            return not t.is_disabled
        return False

    def has_session_expired(self, expiration_time):
        """
        " True if the session has expired.
        """

    @classmethod
    def request_is_valid(Klass, request):
        """ Given a request, check that the payload contains a token and that
        the token is valid. If it's invalid, throw an exception.
        
        :return: True if a token is valid for a given request. Throws an 
                exception otherwise.
        """
        if request.method == 'POST':
            arr = request.POST
        elif request.method == 'GET':
            arr = request.GET
        else:
            raise AuthorizationException()
        
        if Klass._REQUEST_KEY not in arr:
            raise AuthorizationException()
        
        tok = arr[Klass._REQUEST_KEY]
        qs = Klass.objects.filter(value=tok)
        
        if not qs.exists():
            raise InvalidTokenException(tok)
        
        if qs[0].is_disabled():
            raise DisabledTokenException(qs[0])
        
        return True
    
    def __unicode__(self):
        if self.comment:
            return "%s - %s"%(self.comment, self.value)
        return "%s - %s's token set"%(self.value, self.token_set.name)

    class Meta:
        abstract = False
        app_label = "asena"
        verbose_name = "Token"
        verbose_name_plural = "Tokens"
