import base64
import os
import time

import ttldict2

from clarabot_nano.utility.serialize import ulong2bytes


class GatewayMutationTracking(object):
    """
    To prevent the server from performing request replay attacks, all mutating requests are required to be
    supplied with a session identifier for mutation and the correct sequence number that is not repeating.
    The server may maliciously repeat the session-creating request to knock out all the client's tokens. It
    will only result in DoS attack. The malicious server won't gain advantage from it, because the same
    mutation-session creation request will always create new and unique tokens.
    """

    def __init__(self):
        self._token_stores = ttldict2.TTLDict(60.0 * 60)

    def create(self, account_id: int) -> str:
        """
        Create a new token for the authenticated account to perform mutating requests with.
        """
        token_store = self._token_stores.get(account_id, touch=True)
        if not token_store:
            self._token_stores[account_id] = token_store = ttldict2.TTLDict(60.0 * 60, max_items=32)
        # NOTE: The token is a plain timestamp and random value. The initial sequence value is constant. It needs no
        # protection against guessing or forging, because only the related account is able to submit values
        # that will be checked at all. This is due to the fact that the token is transmitted in an authenticated
        # end-to-end encrypted payload.
        # The single goal of this is to prevent replay-attacks by a malicious server, which means the most valuable
        # property it can possess is to be nonce-like. The timestamp and the size of the random provides sufficient
        # certainty of that.
        while True:
            # Normally it is impossible for a collision to happen with the timestamp+random construct.
            # So the loop is mostly for show.
            token = base64.b64encode(ulong2bytes(int(time.time() * 1000000)) + os.urandom(10)).decode("ascii")
            if token not in token_store:
                break
        token_store[token] = 1
        return token

    def check(self, account_id: int, token: str, sequence: int) -> bool:
        """
        The checking of the token here is not sensitive like a cryptographical comparison, because all values
        are from already well authenticated ciphertexts that prevent timing attacks and alike.
        The goal is to absolutely prevent replay-attacks, so the sequence component of the mutation-id is incremented
        if it is found to be valid here. This means that even if there is a processing error later, the client will
        need to increment the id as well regardless of the error.
        """
        token_store = self._token_stores.get(account_id, touch=True)
        if token_store:
            seq = token_store.get(token)
            if seq and seq <= sequence:  # allow skipping so error handling is much simpler
                token_store[token] = seq + 1
                return True
        return False
