"""
Utilities for working with nexus tokens.

Helps with generating tokens, validating tokens.
"""
import binascii
from collections import Mapping
from datetime import datetime
import hashlib
import json
import logging
import os
import re
import sys
import time
import urllib

import requests
import rsa

log = logging.getLogger()

class DictObj(Mapping):
    """
    Simple dictionary wrapper
    """
    def __init__(self, delegate):
        self.delegate = delegate

    def __len__(self):
        return len(self.delegate)

    def __iter__(self):
        return self.delegate.__iter__()

    def __getitem__(self, item):
        return self.delegate[item]

    def __getattr__(self, attrname):
        try:
            return self.delegate[attrname]
        except KeyError:
            raise AttributeError()

class InMemoryCache(object):
    """
    Simple cache implementation for signing certificates.
    """

    def __init__(self):
        self.cache_map = {}

    def save_public_key(self, key_id, key):
        self.cache_map[key_id] = key

    def has_public_key(self, key_id):
        return key_id in self.cache_map

    def get_public_key(self, key_id):
        return rsa.PublicKey.load_pkcs1(self.cache_map[key_id])

class FileSystemCache(object):
    """
    Cache signing certificates to the filesystem.
    """

    def __init__(self, cache_path):
        self.cache_path = cache_path
        if not os.path.exists(self.cache_path):
            os.makedirs(self.cache_path)

    def save_public_key(self, key_id, key):
        cached_cert_path = os.path.join(self.cache_path,
            "{0}.pem".format(key_id))
        with open(cached_cert_path, 'w') as cert:
            cert.write(str(key))

    def has_public_key(self, key_id):
        cached_cert_path = os.path.join(self.cache_path,
            "{0}.pem".format(key_id))
        return os.path.exists(cached_cert_path)

    def get_public_key(self, key_id):
        cached_cert_path = os.path.join(self.cache_path,
            "{0}.pem".format(key_id))
        with open(cached_cert_path, 'r') as cert:
            return rsa.PublicKey.load_pkcs1(cert.read())

class LoggingCacheWrapper(object):

    def __init__(self, cache):
        self.cache = cache

    def save_public_key(self, key_id, key):
        message = "{0}: Saving public key {1}:{2}"
        log.debug(message.format(self.cache.__class__.__name__,
                    key_id, key))
        self.cache.save_public_key(key_id, key)

    def has_public_key(self, key_id):
        return self.cache.has_public_key(key_id)

    def get_public_key(self, key_id):
        message = "{0}: Getting public key {1}"
        log.debug(message.format(self.cache.__class__.__name__, key_id))
        return self.cache.get_public_key(key_id)

def validate_token(token, cache=InMemoryCache(), verify=True):
    """
    Given a token validate it.

    Keyword arguments:
    :param tokens: A signed authentication token which was provided by Nexus

    :raises ValueError: If the signature is invalid
    """
    unencoded_token = urllib.unquote(token)
    token_map = {}
    for entry in unencoded_token.split('|'):
        key, value = entry.split('=')
        token_map[key] = value
    subject_hash = hashlib.md5(token_map['SigningSubject']).hexdigest()
    if not cache.has_public_key(subject_hash):
        key_struct = requests.get(token_map['SigningSubject'], verify=verify).content
        public_key = json.loads(key_struct)['pubkey']
        cache.save_public_key(subject_hash, public_key)

    public_key = cache.get_public_key(subject_hash)
    sig = token_map.pop('sig')
    match = re.match('^(.+)\|sig=.*', unencoded_token)
    signed_data = match.group(1)
    try:
        sig = binascii.a2b_hex(sig)
        rsa.verify(signed_data, sig, public_key)
    except rsa.VerificationError:
        exc_value, exc_traceback = sys.exc_info()[1:]
        log.debug('RSA Verification error')
        log.debug(exc_value)
        log.debug(exc_traceback)
        raise ValueError('Invalid Signature')
    now = time.mktime(datetime.utcnow().timetuple())
    if token_map['expiry'] < now:
        raise ValueError('TokenExpired')
    return token_map['un']

def request_access_token(client_id, client_secret,
        auth_code, auth_uri="https://graph.api.globusonline.org/token",
        verify=True):
    """
    Given an authorization code, request an access token.

    :param client_id: The client's api id
    :param client_secret: The client's api secret
    :param auth_code: The authorization code given to the resource owner by nexus
    :param auth_uri: The url of the authentication endpoint

    :returns: A dictionary of the access code response.  This will include the
    fields: access_token, refresh_token and expires_in
    :raises AccessTokenRequestError: If the request for an access token fails
    """
    payload = {
            'grant_type': 'authorization_code',
            'code': auth_code,
            }
    response = requests.post(auth_uri,
            auth=(client_id, client_secret),