import hashlib
import json
import random
from base64 import decodebytes, encodebytes

from Cryptodome.Cipher import AES
from Cryptodome.Util.Padding import pad, unpad

from pubnub.crypto_core import PubNubCrypto

Initial16bytes = "0123456789012345"


class PubNubCryptodome(PubNubCrypto):
    mode = AES.MODE_CBC
    fallback_mode = None

    def __init__(self, pubnub_config):
        self.pubnub_configuration = pubnub_config
        self.mode = pubnub_config.cipher_mode
        self.fallback_mode = pubnub_config.fallback_cipher_mode

    def encrypt(self, key, msg, use_random_iv=False):
        secret = self.get_secret(key)
        initialization_vector = self.get_initialization_vector(use_random_iv)

        cipher = AES.new(
            bytes(secret[0:32], "utf-8"),
            self.mode,
            bytes(initialization_vector, "utf-8"),
        )
        encrypted_message = cipher.encrypt(self.pad(msg.encode("utf-8")))
        msg_with_iv = self.append_random_iv(
            encrypted_message, use_random_iv, bytes(initialization_vector, "utf-8")
        )

        return encodebytes(msg_with_iv).decode("utf-8").replace("\n", "")

    def decrypt(self, key, msg, use_random_iv=False):
        secret = self.get_secret(key)

        decoded_message = decodebytes(msg.encode("utf-8"))
        initialization_vector, extracted_message = self.extract_random_iv(
            decoded_message, use_random_iv
        )
        cipher = AES.new(bytes(secret[0:32], "utf-8"), self.mode, initialization_vector)
        try:
            plain = self.depad((cipher.decrypt(extracted_message)).decode("utf-8"))
        except UnicodeDecodeError as e:
            if not self.fallback_mode:
                raise e

            cipher = AES.new(
                bytes(secret[0:32], "utf-8"), self.fallback_mode, initialization_vector
            )
            plain = self.depad((cipher.decrypt(extracted_message)).decode("utf-8"))

        try:
            return json.loads(plain)
        except Exception:
            return plain

    def append_random_iv(self, message, use_random_iv, initialization_vector):
        if self.pubnub_configuration.use_random_initialization_vector or use_random_iv:
            return initialization_vector + message
        else:
            return message

    def extract_random_iv(self, message, use_random_iv):
        if self.pubnub_configuration.use_random_initialization_vector or use_random_iv:
            return message[0:16], message[16:]
        else:
            return bytes(Initial16bytes, "utf-8"), message

    def get_initialization_vector(self, use_random_iv):
        if self.pubnub_configuration.use_random_initialization_vector or use_random_iv:
            return f"{random.randint(0, 9999999999999999):016}"
        else:
            return Initial16bytes

    def pad(self, msg, block_size=16):
        padding = block_size - (len(msg) % block_size)
        return msg + (chr(padding) * padding).encode("utf-8")

    def depad(self, msg):
        return msg[0 : -ord(msg[-1])]

    def get_secret(self, key):
        return hashlib.sha256(key.encode("utf-8")).hexdigest()


class PubNubFileCrypto(PubNubCryptodome):
    def encrypt(self, key, file):
        secret = self.get_secret(key)
        initialization_vector = self.get_initialization_vector(use_random_iv=True)
        cipher = AES.new(
            bytes(secret[0:32], "utf-8"),
            self.mode,
            bytes(initialization_vector, "utf-8"),
        )
        initialization_vector = bytes(initialization_vector, "utf-8")

        return self.append_random_iv(
            cipher.encrypt(pad(file, 16)),
            use_random_iv=True,
            initialization_vector=initialization_vector,
        )

    def decrypt(self, key, file):
        secret = self.get_secret(key)
        initialization_vector, extracted_file = self.extract_random_iv(
            file, use_random_iv=True
        )
        try:
            cipher = AES.new(
                bytes(secret[0:32], "utf-8"), self.mode, initialization_vector
            )
            result = unpad(cipher.decrypt(extracted_file), 16)
        except ValueError:
            cipher = AES.new(
                bytes(secret[0:32], "utf-8"), self.fallback_mode, initialization_vector
            )
            result = unpad(cipher.decrypt(extracted_file), 16)

        return result
