import asyncio
from typing import Set, List

import safe_radix32

from clarabot_nano.common.core.message import MSG_TYPE_REQUEST_CONFIG_UPDATE
from clarabot_nano.gateway.errors import SERVER_ERROR_NO_SESSION_BIND, SERVER_ERROR_TOO_MANY_REQUESTS
from clarabot_nano.gateway.state.config import RoomGroupConfigState
from clarabot_nano.gateway.state.gateway import GatewayState
from clarabot_nano.gateway.utility import check_single_response, GatewayServerError
from clarabot_nano.utility.serialize import msgpack_dumps, msgpack_loads


async def state_config_job(gws: GatewayState, job_queue: asyncio.Queue):
    """
    The job of this work task is to load and keep the group-config blockchain up to date. Processing of
    incoming relayed request from users may be stalled or rejected if the room's blockchain is not ready.
    TODO: Optimally the room-state objects in ERROR state could be periodically retried by loading, but that
      should be such a rare occurrence that we consider it optional. Most of the time it will not even be
      recoverable.
    """
    while True:
        job = await job_queue.get()
        t = job["type"]
        if t == "serves-change":
            await _serves_change(gws, job["room_ids"])
        elif t == "load-chain":
            await _load_chain(gws, job["room_id"])
        elif t == "add-block":
            await _add_block(gws, job["room_id"], job["block"], job["signature"])
        elif t == "verified":
            await _verified(gws, job["room_id_rad32"])
        else:
            raise Exception(f"Unknown room config job: {t}")


async def _serves_change(gws: GatewayState, room_ids: Set[int]):
    """
    This is called to setup the served rooms each time the Nano session binding changes or the remote config gets
    updated for resource and room binding.
    """
    gws.set_served_rooms(room_ids)


async def _load_chain(gws: GatewayState, room_id: int):
    """
    Load the blockchain of the room being served.
    Since the whole loading is serialized, new block events are buffered and will be processed afterwards.
    """
    room_config_state = gws.room_config_store.get(room_id)
    if not room_config_state or not room_config_state.needs_to_be_loaded():
        return
    try:
        count = 300
        pivot = None
        while True:
            try:
                async with gws.server_request(
                    {"type": "room-config", "room_id": room_id, "pivot": pivot, "count": count}
                ) as future:
                    blocks = check_single_response(await future, list, "query room config")
            except GatewayServerError as e:
                # Rate-limiting shall only delay loading the config of a served room.
                if e.error == SERVER_ERROR_TOO_MANY_REQUESTS:
                    await asyncio.sleep(5.0)
                    continue
                raise e
            for block in blocks:
                room_config_state.add_block(block["block"], block["signature"], gws.keyring_cache.self_kr)
            if len(blocks) < count:
                break
            pivot = blocks[-1]["id"]
    except GatewayServerError as e:
        if e.error == SERVER_ERROR_NO_SESSION_BIND:
            return  # NOTE: Realistically no other errors should arrive here.
        raise e
    if gws.room_config_store.get(room_id) is not room_config_state:
        return  # The state object was replaced by revoke-grant while loading the data for example.
    if not room_config_state.needs_to_be_loaded():
        return  # The room may have been revoked while loading the data for example.
    verified_room_blocks = gws.get_verified_room_blocks(room_id)
    room_config_state.load_init(gws.keyring_cache.self_kr, verified_room_blocks)
    if room_config_state.state_is_final():
        # If final state is REVOKED it is no longer in the store and will be garbage collected soon. (unlikely)
        # If final state is ERROR then this room blockchain may be irrecoverably broken, which should not happen
        # under normal circumstances. There are a few fringe cases when this may be possible, when considering
        # asynchronous pairwise DC replication and Nano config or machine migration from one locality to another.
        # Aside from those, it could be that the Nano is obsolete and cannot parse something new in the blockchain.
        # An exception about the error is already logged so there's nothing to do here.
        return
    await _check_store_top_room_blocks(gws, room_id, room_config_state, verified_room_blocks)


async def _add_block(gws: GatewayState, room_id: int, block: bytes, block_signature: bytes):
    """
    This is for the processing of a new block that is received from the server in an event.
    This is the final acknowledgement of the blockchain mutation if the Nano requests creating a new block.
    NOTE: The following is not implemented as the forking is expected to happen and is handled well, thus there's
    no great incentive to avoid it.
    It is possible to wait for these acknowledgements at least for the blocks that are created by this instance,
    to avoid unnecessarily forking the blockchain DAG. If well coordinated the following blockchain mutations can
    block until this arrives. NOTE: If the mutation was acknowledged by the server then it is guaranteed to be
    eventually processed and added, so abandoning-timeout is not necessary.
    """
    room_config_state = gws.room_config_store.get(room_id)
    if not room_config_state or room_config_state.state_is_final():
        return  # no longer served or is in unrecoverable error
    room_config_state.add_block(block, block_signature, gws.keyring_cache.self_kr)
    if room_config_state.needs_to_be_loaded() or room_config_state.state_is_final():
        return  # only buffering for loading, or the chain has just broken
    verified_room_blocks = gws.get_verified_room_blocks(room_id)
    if not room_config_state.ready_for_use():
        # State is not final, nor loading. (it is unverified by process of elimination)
        room_config_state.update_verification(verified_room_blocks)
    await _check_store_top_room_blocks(gws, room_id, room_config_state, verified_room_blocks)


async def _verified(gws: GatewayState, room_id_rad32: str):
    """
    Config changes to the room block verification will feedback to here from the other job handlers ans from itself
    as well. It will not create an endless update loop or a broadcast storm either because it is a no-op if the
    verified matches the top blocks, which it will converge to eventually.
    This is necessary for when there is a served room with an unverified config blockchain. When the owner will
    verify the chain with a room-attach admin request, the blockchain will not be re-loaded so without this it
    would be stuck as UNVERIFIED.
    It would also be stuck as verified if the user purposefully sets a block-id-hash that is not contained in the
    chain. (this will be also handled, although it is not a recommended action)
    """
    try:
        room_id = safe_radix32.decode(room_id_rad32)
    except Exception:
        return  # invalid room-id changed
    room_config_state = gws.room_config_store.get(room_id)
    if not room_config_state or room_config_state.state_is_final() or room_config_state.needs_to_be_loaded():
        return  # not loaded, no longer served or is in unrecoverable error
    # State is not final, nor loading. (it is ready or unverified by process of elimination)
    verified_room_blocks = gws.get_verified_room_blocks(room_id)
    room_config_state.update_verification(verified_room_blocks)
    await _check_store_top_room_blocks(gws, room_id, room_config_state, verified_room_blocks)


async def _check_store_top_room_blocks(
    gws: GatewayState, room_id: int, room_config_state: RoomGroupConfigState, verified_room_blocks: List[str]
):
    """
    If the chain is ready for use that means the last recorded block is present in it. All newer are signed by the
    owner so they are valid. Advance the verified block references in the config to the new head if any.
    """
    top_room_blocks = room_config_state.get_top_verified_blocks()
    if top_room_blocks and top_room_blocks != verified_room_blocks:
        reply = await gws.core_requester.request(
            [
                MSG_TYPE_REQUEST_CONFIG_UPDATE,
                msgpack_dumps(
                    {
                        "_handler_key": "verify-room-blocks",
                        "room_id": safe_radix32.encode_fw(room_id),
                        "room_blocks": top_room_blocks,
                    }
                ),
            ],
            timeout=20.0,
        )
        if len(reply) != 1 or not bool(msgpack_loads(reply[0]).get("ok")):
            raise Exception("Failed to update verified room blocks")
