from io import StringIO
from typing import Tuple, Optional

import safe_radix32
from clarabot_crypto.group_config.block import GroupConfigBlockMemberFlags
from clarabot_nano_common.common.error import maybe_rad32
from clarabot_nano_common.common.handler.error import NanoHandlerError
from clarabot_nano_common.common.protocol.errors import (
    NANO_REQUEST_ERROR_HANDLER_BAD_REQUEST,
    NANO_REQUEST_ERROR_HANDLER_DENIED,
    NANO_REQUEST_ERROR_HANDLER_TEMPORARY_FAILURE,
)
from clarabot_nano_common.common.protocol.request import NanoRequestContext
from clarabot_nano_common.common.protocol.response import NanoBaseResponse
from clarabot_nano_common.gateway.utility import check_single_response
from clarabot_nano_common.utility.algorithm import dict_nested_get

from clarabot_nano.common.request.gateway import NanoCreateRoomConfigBlockRequest
from clarabot_nano.gateway.config import GatewayConfig
from clarabot_nano.gateway.handler.base import AbstractNanoGatewayHandler
from clarabot_nano.gateway.state.gateway import GatewayState


class CreateRoomConfigBlockResponse(NanoBaseResponse):
    BLOCK_ID = "block_id"


class CreateRoomConfigBlockHandler(AbstractNanoGatewayHandler):
    EXECUTION_STATE_CAN_CONFLICT = False  # cacheable and request_key are constant, no conflict is possible

    HANDLER_KEY = NanoCreateRoomConfigBlockRequest.TYPE_KEY

    def __init__(self, helper: GatewayState, extracted_config: GatewayConfig, context: dict, payload: dict):
        AbstractNanoGatewayHandler.__init__(self, helper, extracted_config, context, payload)
        if not self.context.get(NanoRequestContext.ROOM):
            raise NanoHandlerError(NANO_REQUEST_ERROR_HANDLER_BAD_REQUEST, "Room is missing", self)
        if self.context[NanoRequestContext.OWNER_ACCOUNT_ID] == self.context[NanoRequestContext.ACCOUNT_ID]:
            pass  # owner may always use the Nano to create room config blocks
        else:
            # check if member is active and administrator
            config_member_flags = self.context[NanoRequestContext.ROOM][NanoRequestContext.ROOM_CONFIG_MEMBER]
            if (
                not config_member_flags
                or not (config_member_flags & GroupConfigBlockMemberFlags.ACTIVE)
                or not (config_member_flags & GroupConfigBlockMemberFlags.ADMINISTRATOR)
            ):
                raise NanoHandlerError(NANO_REQUEST_ERROR_HANDLER_DENIED, "No permission", self)
        # prepare arguments
        member_flags = self.payload.get(NanoCreateRoomConfigBlockRequest.MEMBER_FLAGS)
        if member_flags is not None and not isinstance(member_flags, dict):
            raise NanoHandlerError(NANO_REQUEST_ERROR_HANDLER_BAD_REQUEST, "Invalid member-flags", self)
        self.member_flags = dict()
        if member_flags:
            for member_id_rad32, flags in member_flags.items():
                self.member_flags[safe_radix32.decode(member_id_rad32)] = flags
        self.group_flags = self.payload.get(NanoCreateRoomConfigBlockRequest.GROUP_FLAGS)
        if self.group_flags is not None and not isinstance(self.group_flags, int):
            raise NanoHandlerError(NANO_REQUEST_ERROR_HANDLER_BAD_REQUEST, "Invalid group-flags", self)
        # these will be saved for logging
        self.block_id: str = None
        self.room_id: str = None

    async def _create_request_key(self, secure: bool = False) -> Tuple[bool, Optional[list], int]:
        return self._secure_request_key(False, None, 1) if secure else False, None, 1

    async def _execute_initial(self) -> dict:
        # Preload member keyrings that are bing given membership.
        prepared_peer_krs = dict()
        for member_id, flags in self.member_flags.items():
            if bool(flags & GroupConfigBlockMemberFlags.ACTIVE):
                prepared_peer_krs[member_id] = await self.helper.get_peer_account_keyring(member_id)
        # Prepare room keyring and config state.
        room_id: int = self.context[NanoRequestContext.ROOM][NanoRequestContext.ROOM_ID]
        self.room_id = safe_radix32.encode_fw(room_id)
        room_kr = await self.helper.get_own_room_keyring(room_id)
        room_config = self.helper.room_config_store.get(room_id)
        if not room_config:
            raise NanoHandlerError(
                NANO_REQUEST_ERROR_HANDLER_TEMPORARY_FAILURE, "Room is not served (inconsistent)", self
            )
        await room_config.wait_ready()
        # Acquire new id for the block.
        async with self.helper.server_request({"type": "create-new-id"}) as future:
            resp = check_single_response(await future, dict, "create new room block id")
        block_id = safe_radix32.decode(resp["data"]["createNewId"])
        self.block_id = safe_radix32.encode_fw(block_id)
        # Create new block and deltas for transaction. (re-checks ready-state)
        raw_block, signature, member_deltas, group_deltas = await room_config.new_block(
            self.helper.keyring_cache.self_kr, block_id, self.member_flags, self.group_flags
        )
        # Create delta-keys map for the transaction.
        delta_keys = {}
        for member_id, flags in member_deltas.items():
            if bool(flags & GroupConfigBlockMemberFlags.ACTIVE):
                peer_kr = prepared_peer_krs.get(member_id)
                if not peer_kr:
                    peer_kr = await self.helper.get_peer_account_keyring(member_id)
                delta_keys[safe_radix32.encode_fw(member_id)] = room_kr.dump(
                    self.helper.keyring_cache.self_kr, peer_kr
                )[room_kr.PINNED]
        # Submit create-block transaction request.
        async with self.helper.server_request(
            {
                "type": "add-room-config",
                "variables": {
                    "roomId": self.room_id,
                    "newBlock": raw_block,
                    "newBlockSignature": signature,
                    "deltaKeys": delta_keys,
                },
            }
        ) as future:
            resp = check_single_response(await future, dict, "create new room block")
        if resp["data"]["createRoomConfigBlock"] is not True:
            # This should be always a temporary error, maybe by the forking of the blockchain.
            raise NanoHandlerError(NANO_REQUEST_ERROR_HANDLER_TEMPORARY_FAILURE, "Server request failed", self)
        # Reply to the requesting user with the new block's id.
        d = await AbstractNanoGatewayHandler._execute_initial(self)
        d[CreateRoomConfigBlockResponse.BLOCK_ID] = self.block_id
        return d

    async def _execute_part(self, part: int, s_hmac_start_part: Optional[int]) -> dict:  # pragma: no cover
        raise NanoHandlerError(NANO_REQUEST_ERROR_HANDLER_BAD_REQUEST, "Part request is invalid", self)

    def _log_message(self) -> str:
        return f"Create config block {self.block_id} for room {self.room_id}"

    def build_error_message(self, buf: StringIO) -> None:
        buf.write("Create config block for room ")
        try:
            room_id = dict_nested_get(self.context, NanoRequestContext.ROOM, NanoRequestContext.ROOM_ID)
        except KeyError:
            pass
        else:
            buf.write(maybe_rad32(room_id))
