import os
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes

BLOCK_LENGTH = 16


class SatelEncryption:
    """Encryptor and decryptor for Satel integration protocol.

    :param integration_key: Satel integration key to be used for encrypting and decrypting data.

    """

    def __init__(self, integration_key: str):
        encryption_key = self.integration_key_to_encryption_key(integration_key)
        self.cipher = Cipher(algorithms.AES(encryption_key), modes.ECB())

    @classmethod
    def integration_key_to_encryption_key(cls, integration_key: str) -> bytes:
        """Convert Satel integration key into encryption key.

        :param integration_key: Satel integration key

        :returns: encryption key

        """
        integration_key_bytes = bytes(integration_key, "ascii")
        key = [0] * 24
        for i in range(12):
            key[i] = key[i + 12] = (
                integration_key_bytes[i] if len(integration_key_bytes) > i else 0x20
            )
        return bytes(key)

    @classmethod
    def _bytes_to_blocks(cls, message: bytes, block_len: int) -> list[bytes]:
        """Split message into list of blocks of equal length."""
        return [message[i : i + block_len] for i in range(0, len(message), block_len)]

    def encrypt(self, data: bytes) -> bytes:
        """Encrypt protocol data unit.

        :param data: data to be encrypted

        :returns: encrypted data

        """
        if len(data) < BLOCK_LENGTH:
            data += b"\x00" * (BLOCK_LENGTH - len(data))
        encrypted_data = []
        encryptor = self.cipher.encryptor()
        cv = [0] * BLOCK_LENGTH
        cv = list(encryptor.update(bytes(cv)))
        for block in self._bytes_to_blocks(data, BLOCK_LENGTH):
            p = list(block)
            if len(block) == BLOCK_LENGTH:
                p = [a ^ b for a, b in zip(p, cv)]
                p = list(encryptor.update(bytes(p)))
                cv = list(p)
            else:
                cv = list(encryptor.update(bytes(cv)))
                p = [a ^ b for a, b in zip(p, cv)]
            encrypted_data += p
        return bytes(encrypted_data)

    def decrypt(self, data: bytes) -> bytes:
        """Decrypt message.

        :param data: data to be decrypted

        :returns: decrypted data

        """
        decrypted_data = []
        cv = [0] * BLOCK_LENGTH
        decryptor = self.cipher.decryptor()
        encryptor = self.cipher.encryptor()
        cv = list(encryptor.update(bytes(cv)))
        for block in self._bytes_to_blocks(data, BLOCK_LENGTH):
            temp = list(block)
            c = list(block)
            if len(block) == BLOCK_LENGTH:
                c = list(decryptor.update(bytes(c)))
                c = [a ^ b for a, b in zip(c, cv)]
                cv = list(temp)
            else:
                cv = list(encryptor.update(bytes(cv)))
                c = [a ^ b for a, b in zip(c, cv)]
            decrypted_data += c
        return bytes(decrypted_data)


class EncryptedCommunicationHandler:
    """Handler for Satel encrypted communications.

    :param integration_key: Satel integration key to be used for encrypting and decrypting data.

    """

    next_id_s: int = 0

    def __init__(self, integration_key: str):
        self._rolling_counter: int = 0
        # There will be a new value of id_s for each instance . As there will be rather one client this doesn't
        # have much use. However id_s value may show how many reconnections there where.
        self._id_s: int = EncryptedCommunicationHandler.next_id_s
        EncryptedCommunicationHandler.next_id_s += 1
        self._id_r: int = 0
        self._satel_encryption = SatelEncryption(integration_key)

    def _prepare_header(self) -> bytes:
        header = (
            os.urandom(2)
            + self._rolling_counter.to_bytes(2, "big")
            + self._id_s.to_bytes(1, "big")
            + self._id_r.to_bytes(1, "big")
        )
        self._rolling_counter += 1
        self._rolling_counter &= 0xFFFF
        self._id_s = header[4]
        return header

    def prepare_pdu(self, message: bytes) -> bytes:
        """Prepare protocol data unit.

        :param message: message to be included in PDU

        :returns: encrypted PDU with given message

        """
        pdu = self._prepare_header() + message
        encrypted_pdu = self._satel_encryption.encrypt(pdu)
        return encrypted_pdu

    def extract_data_from_pdu(self, pdu: bytes) -> bytes:
        """Extract data from protocol data unit.

        :param pdu: PDU from which a message to be extracted

        :returns: extracted message

        """
        decrypted_pdu = self._satel_encryption.decrypt(pdu)
        header = decrypted_pdu[:6]
        data = decrypted_pdu[6:]
        self._id_r = header[4]
        if (self._id_s & 0xFF) != decrypted_pdu[5]:
            raise RuntimeError(
                f"Incorrect value of ID_S, received \\x{decrypted_pdu[5]:x} but expected \\x{self._id_s:x}\n"
                "Decrypted data: %s" % "".join(f"\\x{x:02x}" for x in decrypted_pdu)
            )
        return bytes(data)
