from __future__ import unicode_literals

from .picklemixin import PickleMixIn
from cryptography.fernet import Fernet, InvalidToken
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC

import random
import os
import string
import base64
import pickle
import tempfile


class BaseSaltHandle(object):
    pass


class BaseCryptHandle(object):
    pass


class SaltHandle(BaseSaltHandle, PickleMixIn):
    """
    Class to generate/store salt key & hold it within the class
    """

    __slots__ = 'salt_key'

    @staticmethod
    def __random_text():
        digits = "".join([random.choice(string.digits + string.ascii_letters) for i in range(15)])
        return digits

    def __init__(self, salt=None):
        """
        :param salt: [Optional] Salt key in bytes format
        """

        if salt and isinstance(salt, bytes):
            self.salt_key = salt
        elif salt:
            raise ValueError("'salt' %r is not an instance of bytes" % salt)
        else:
            text = self.__random_text().encode()
            salt = os.urandom(16)

            kdf = PBKDF2HMAC(
                algorithm=hashes.SHA256(),
                length=32,
                salt=salt,
                iterations=100000,
                backend=default_backend()
            )

            self.salt_key = base64.urlsafe_b64encode(kdf.derive(text))

    def __eq__(self, other):
        return getattr(self, self.__slots__) == getattr(other, self.__slots__)

    def __hash__(self):
        return [hash(self.salt_key)]

    def __repr__(self):
        return self.__class__.__name__ + repr(str(self.salt_key))

    def __str__(self):
        return str(self.salt_key)


class CryptHandle(BaseCryptHandle, PickleMixIn):
    """
    Class to encrypt data or objects
    """

    __slots__ = ('alias', 'salt')

    def __init__(self, alias=None, salt=SaltHandle(), enc_obj=None, private=False):
        """
        Create CryptHandle objects the way I like!

        :param alias: [Optional] Alias Name for this class object
        :param salt: [Optional] Existing SaltHandle object
        :param enc_obj: [Optional] Existing Encrypted Objected generated by Crypthandle after encryption
        :param private: [Optional] (True/False) Will make encrypted item private when user uses peak() option
        """

        if not isinstance(salt, SaltHandle):
            raise ValueError("'salt_key' %r must be a SaltHandle instance" % salt)

        if not alias:
            alias = tempfile.NamedTemporaryFile()
            alias = os.path.basename(alias.name)

        self.alias = alias
        self.salt = salt
        self.__enc_obj = enc_obj
        self.__private = private

    @property
    def encrypted_obj(self):
        """
        :return: Returns encrypted object within class
        """

        return self.__enc_obj

    @property
    def private(self):
        """
        :return: Class attribute private
        """

        return self.__private

    @private.setter
    def private(self, private):
        """
        Sets class attribute private when user calls .private = (True/False)
        :param private:
        """

        if not isinstance(private, bool):
            raise ValueError("'private' %r is not an Bool instance" % private)

        self.__private = private

    def encrypt(self, item):
        """
        Encrypts data the way I like it!

        :param item: Data or Object item to be encrypted using SHA256 & Salt
        """

        if item:
            item_bytes = pickle.dumps(item)

            if not isinstance(item_bytes, bytes):
                raise ValueError("'item' %r is unable to serialize into bytes")

            crypt_obj = Fernet(self.salt.salt_key)
            self.__enc_obj = crypt_obj.encrypt(item_bytes)
        else:
            self.__enc_obj = None

    def decrypt(self):
        """
        Decrypt dat data!!!

        :return: Returns decrypted data or object that was previously encrypted by this class
        """

        if self.__enc_obj:
            crypt_obj = Fernet(self.salt.salt_key)

            try:
                return pickle.loads(crypt_obj.decrypt(self.__enc_obj))
            except InvalidToken as e:
                raise Exception('Error: Invalid Salt Token used. %s' % e)
            except Exception as e:
                raise Exception('Error: %s' % e)

    def peak(self):
        """
        Peaking can be dangerous sometimes! In this case, better to peak than to decrypt!

        :return: Returns **** if class is set to private mode or returns decrypted data if private mode is off
        """
        if self.__private:
            if self.decrypt():
                return '*' * len(self.decrypt())
            else:
                return ''
        else:
            return str(self.decrypt())

    def get_attr(self):
        """
        Gimme everything but salt!!!
        Gah! My steak is now bland!

        :return: Returns private mode attribute and encrypted object attribute in a list
        """

        return [self.__private, self.__enc_obj]

    def __eq__(self, other):
        for k in self.__slots__:
            if getattr(self, k) != getattr(other, k):
                return False

        return True

    def __hash__(self):
        return hash(self.alias)

    def __repr__(self):
        return self.__class__.__name__ + repr(str(self.alias))

    def __str__(self):
        return str(self.alias)
