import json

from django.core.signing import TimestampSigner, BadSignature
from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode
from django.utils.encoding import DjangoUnicodeDecodeError

from .models import Contrib


class URLTokenManager(TimestampSigner):
    """ Handle signed json data as URL-safe strings

    This class has two responsibilities:
    - sign/unsign
    - pack/unpack JSON to base64
    """

    def sign(self, payload, *args, **kwargs):
        """
        :param payload: the data to be embeded into the token
        :type data: dict
        """
        return urlsafe_base64_encode(
            super().sign(
                json.dumps(payload), *args, **kwargs
            ).encode('utf-8')).decode('ascii')

    def unsign(self, encoded_payload, *args, **kwargs):
        decoded_payload = urlsafe_base64_decode(encoded_payload)
        unsigned = super().unsign(decoded_payload)
        return json.loads(unsigned)


class TokenError(Exception):
    pass


class ContribTokenManager:
    """Produce and use signed URL tokens for account-less contrib management
    """
    SCOPE = 'contrib/manage'

    def __init__(self):
        self.manager = URLTokenManager()

    def mk_token(self, contrib):
        """ Generate a signed contrib management token

        Valid for a given contrib, and for a limited time.

        :type contrib: Contrib
        :rtype str:
        """
        return self.manager.sign({'scope': self.SCOPE, 'pk': contrib.pk})

    def get_instance_if_allowed(self, encoded_token, pk=None):
        """Return a contrib if the token grants authz for that Contrib pk

        Raise a TokenError if not authorized.

        :param pk: the contrib pk (optional, if you want to check that the
          instance is the right one)
        :param encoded_token: the encoded token, from ``mk_token()``:
        :return: a Contrib instance or None, if the object does not exist
        :rtype Contrib:
        """
        try:
            data = self.manager.unsign(encoded_token)
        except BadSignature:
            raise TokenError('Invalid token signature')

        except (DjangoUnicodeDecodeError, UnicodeDecodeError):
            raise TokenError('This is not a well-formed token')

        if (pk is not None) and (data['pk'] != pk):
            raise TokenError('Token is not valid for id {}'.format(pk))
        else:
            try:
                return Contrib.objects.get(pk=data['pk'])
            except Contrib.DoesNotExist:
                return None