import time
from typing import Optional, Tuple

import safe_radix32
import ttldict2
from clarabot_nano_common.common.crypto.keyring import NanoSelfAccountKeyring, NanoPeerAccountKeyring, NanoRoomKeyring
from clarabot_nano_common.gateway.state.keyring import BaseKeyringCache


class KeyringCache(BaseKeyringCache):
    """
    The used keys in an account-keyring are designed for slow-upgrade or change. New key(s) will be introduced and
    only after an extensive time will they be used. This allows the clients to lazily update account keyrings and
    cache them for a relatively long time.

    The self-keyring of an account is special, it is always available even it is considered "expired", which means
    it should be updated.

    NOTE: This is made to be primarily useful for Nano and not the clients.
    """

    __slots__ = ("_self_ttl", "_anon_krs", "_own_room_krs", "_peer_room_krs", "_peer_room_map")

    def __init__(self, self_kr: NanoSelfAccountKeyring):
        BaseKeyringCache.__init__(self, self_kr)
        self._self_ttl = time.perf_counter() + self.CACHE_TTL
        self._own_room_krs = ttldict2.TTLDict(self.CACHE_TTL, max_items=1000)  # used for serving
        self._peer_room_krs = ttldict2.TTLDict(self.CACHE_TTL, max_items=100)  # used for Nano2Nano comm.
        self._peer_room_map = ttldict2.TTLDict(self.CACHE_TTL, max_items=100)  # used for Nano2Nano comm. (anon)

    def is_self_kr_expired(self) -> bool:
        """
        Updating the self keyring will be necessary in the future when new keys will be introduced. For now there
        is literally nothing to be done because new keys cannot be loaded by a client that does not support them
        and all currently supported keys are mandatory for operation.
        """
        return self._self_ttl < time.perf_counter()

    def get_own_room_kr(self, room_id: int) -> Optional[NanoRoomKeyring]:
        return self._own_room_krs.get(room_id)

    def load_own_room_kr(self, room_id: int, pinned_key: bytes):
        room_kr = NanoRoomKeyring.load(self.self_kr, self.self_kr, safe_radix32.encode_fw(room_id), None, pinned_key)
        self._own_room_krs[room_id] = room_kr
        return room_kr

    def get_peer_room_kr(self, room_id: int) -> Optional[Tuple[int, NanoRoomKeyring]]:
        return self._peer_room_krs.get(room_id)

    def load_peer_room_kr(
        self,
        room_id: int,
        peer_kr: NanoPeerAccountKeyring,
        pinned_key: Optional[bytes] = None,
        saved_key: Optional[bytes] = None,
    ):
        peer_account_id = safe_radix32.decode(peer_kr.id.decode())
        if pinned_key or saved_key:
            room_kr = NanoRoomKeyring.load(
                self.self_kr, peer_kr, safe_radix32.encode_fw(room_id), saved_key, pinned_key
            )
        else:
            raise ValueError("Can't load peer room keyring without key")
        self._peer_room_krs[room_id] = (peer_account_id, room_kr)
        return room_kr

    def get_owner_of_peer_room(self, room_id: int) -> Optional[int]:
        tmp = self._peer_room_krs.get(room_id)
        if tmp:
            return tmp[0]
        return self._peer_room_map.get(room_id)

    def load_owner_of_peer_room(self, room_id: int, peer_kr: NanoPeerAccountKeyring):
        peer_account_id = safe_radix32.decode(peer_kr.id.decode())
        self._peer_room_map[room_id] = peer_account_id
