# This file is part of aiootp:
# a high-level async cryptographic anonymity library to scale, simplify,
# & automate privacy best practices for secure data & identity processing,
# communication, & storage.
#
# Licensed under the AGPLv3: https://www.gnu.org/licenses/agpl-3.0.html
# Copyright © 2019-2021 Gonzo Investigative Journalism Agency, LLC
#            <gonzo.development@protonmail.ch>
#           © 2019-2024 Ricchi (Richard) Machado <rmlibre@riseup.net>
# All rights reserved.
#


"""
A definition of shared properties between cipher streaming types.
"""

__all__ = ["AuthFail", "CipherStreamProperties"]


from aiootp._typing import Typing as t
from aiootp._exceptions import TimestampExpired, IncoherentConcurrencyState
from aiootp._exceptions import InvalidBlockID, InvalidSHMAC
from aiootp.asynchs import asleep
from aiootp.commons import OpenFrozenSlots
from aiootp.generics import abytes_are_equal, bytes_are_equal


class AuthFail(OpenFrozenSlots):
    """
    Creates efficient containers for data lost in a buffer during
    authentication failure of a block ID in (Async)DecipherStream
    objects.
    """

    __slots__ = ("block_id", "block", "buffer")

    def __init__(
        self, block_id: bytes, block: bytes, buffer: t.Callable
    ) -> None:
        self.block_id = block_id
        self.block = block
        self.buffer = buffer


class CipherStreamProperties:
    """
    A definition of shared properties between cipher streaming types.
    """

    IncoherentConcurrencyState: type = IncoherentConcurrencyState
    InvalidBlockID: type = InvalidBlockID
    InvalidSHMAC: type = InvalidSHMAC
    TimestampExpired: type = TimestampExpired

    @property
    def PACKETSIZE(self) -> int:
        """
        Returns the number of combined bytes each iteration of the
        stream will produce. Equal to the header bytes + the blocksize.
        """
        return self._config.PACKETSIZE

    @property
    def aad(self) -> bytes:
        """
        An arbitrary bytes value that a user decides to categorize
        keystreams. It's authenticated as associated data & safely
        differentiates keystreams as a tweak when it's unique for
        each permutation of `key`, `salt`, & `iv`.
        """
        return self._key_bundle.aad

    @property
    def salt(self) -> bytes:
        """
        A [pseudo]random salt that may be supplied by the user. By
        default it's sent in the clear attached to the ciphertext.
        Thus it may simplify implementing efficient features, such
        as search or routing, though care must still be taken when
        considering how leaking such metadata may be harmful.

        Keeping this value constant is strongly discouraged. Though,
        the cipher's salt misuse-reuse resistance is ruled by the
        combination of the automatically incorporated `timestamp`,
        `iv`, & `siv_key`. The risk calculation starts with setting
        r = len(iv + siv_key) / 3. Then, all else staying constant,
        once 256**r messages are encrypted within a second, each
        additional encrypted message within that same second begins
        to have more than a 256**(-r) chance of generating a repeat
        context.

        See: https://github.com/rmlibre/aiootp/issues/16
        """
        return self._key_bundle.salt

    @property
    def iv(self) -> bytes:
        """
        An ephemeral, uniform, random value that's generated by
        the encryption algorithm.
        """
        return self._key_bundle.iv

    async def _aconstant_time_final_context_is_done(self) -> bool:
        """
        Tests in constant time whether the queue token set during the
        (a)finalize call has already been popped from the digesting queue.
        If so, the stream has been closed so returns `True`. Otherwise
        returns `False`.
        """
        await asleep()
        if not self._finalizing_now:
            return False

        final_token = self._finalizing_now[0]
        membership_tests = [
            await abytes_are_equal(queued_token, final_token)
            for queued_token in self._digesting_now
        ]
        return not sum(membership_tests)

    def _constant_time_final_context_is_done(self) -> bool:
        """
        Tests in constant time whether the queue token set during the
        (a)finalize call has already been popped from the digesting queue.
        If so, the stream has been closed so returns `True`. Otherwise
        returns `False`.
        """
        if not self._finalizing_now:
            return False

        final_token = self._finalizing_now[0]
        membership_tests = (
            bytes_are_equal(queued_token, final_token)
            for queued_token in self._digesting_now
        )
        return not sum(membership_tests)


module_api = dict(
    AuthFail=t.add_type(AuthFail),
    CipherStreamProperties=t.add_type(CipherStreamProperties),
    __all__=__all__,
    __doc__=__doc__,
    __file__=__file__,
    __name__=__name__,
    __spec__=__spec__,
    __loader__=__loader__,
    __package__=__package__,
)
