import asyncio
import logging
from typing import Sequence

from clarabot_nano_common.common.error import AbstractAccessLoggableError
from clarabot_nano_common.common.protocol.errors import (
    NANO_REQUEST_ERROR,
    NANO_REQUEST_ERROR_PREPARSE,
    NANO_REQUEST_ERROR_PAYLOAD_TYPE_INVALID,
    NANO_REQUEST_ERROR_INTERNAL_FAILURE,
    NANO_REQUEST_ERROR_TOO_MANY_REQUESTS,
)
from clarabot_nano_common.common.protocol.request import NanoRequestContext, NanoBaseRequest
from clarabot_nano_common.common.protocol.server import NanoServerResponse
from clarabot_nano_common.gateway.utility import GatewayServerError
from clarabot_nano_common.utility.serialize import msgpack_dumps, msgpack_loads

from clarabot_nano.common.gateway.message import InternalGatewayRequest, InternalGatewayResponse
from clarabot_nano.common.request.constants import get_server_prefix_from_request_type
from clarabot_nano.common.spawn.constants import ShortNames
from clarabot_nano.common.zmq.constants import REPLY_MESSAGE, INTERNAL_REQUEST_MESSAGE
from clarabot_nano.common.zmq.inproc_client import AioInprocClient
from clarabot_nano.gateway.errors import SERVER_ERROR_TOO_MANY_REQUESTS
from clarabot_nano.gateway.job.request import GatewayRequestProcessingError
from clarabot_nano.gateway.request import get_room_access_info
from clarabot_nano.gateway.state.gateway import GatewayState

logger = logging.getLogger(__name__)
access_logger = logging.getLogger("nano/access-log")


async def run_internal_gateway_request(gws: GatewayState, internal_request: dict) -> dict:
    """
    Can be used inside the gateway to invoke any internal handler.
    """
    worker_pool = gws.worker_requester
    gateway_handler_registry = gws.handler_registry
    account_id = None
    try:
        if (
            not isinstance(internal_request, dict)
            or not isinstance(account_id := internal_request.get(InternalGatewayRequest.ACCOUNT_ID), int)
            or not isinstance(room_id := internal_request.get(InternalGatewayRequest.ROOM_ID), int)
        ):
            raise GatewayRequestProcessingError(NANO_REQUEST_ERROR_PREPARSE, "Request context args invalid")
        context = {
            NanoRequestContext.PART_SIZE: gws.part_size,
            NanoRequestContext.OWNER_ACCOUNT_ID: gws.account_id,
            NanoRequestContext.ACCOUNT_ID: account_id,
            NanoRequestContext.ROOM: await get_room_access_info(gws, account_id, room_id, None),
        }
        if internal_request.get(InternalGatewayRequest.LOAD_AVATAR):
            try:
                async with asyncio.timeout(4.5):
                    name_key = await gws.get_account_avatar(account_id)
                    if name_key:
                        context[NanoRequestContext.ACCOUNT_AVATAR_NAME] = name_key[0]
                        context[NanoRequestContext.ACCOUNT_AVATAR_KEY] = name_key[1]
            except asyncio.TimeoutError:
                pass
        internal_response = {InternalGatewayResponse.CONTEXT: context}
        if isinstance(request := internal_request.get(InternalGatewayRequest.REQUEST), dict):
            handler_key = request.get(NanoBaseRequest.TYPE)
            if not isinstance(handler_key, str):
                raise GatewayRequestProcessingError(
                    NANO_REQUEST_ERROR_PAYLOAD_TYPE_INVALID, "Type is invalid", account_id=account_id
                )
            server_prefix = get_server_prefix_from_request_type(handler_key)
            if server_prefix == ShortNames.GATEWAY:
                try:
                    response = await gateway_handler_registry.invoke(
                        gws, gws.extracted_config, context, request, internal=True
                    )
                except GatewayServerError as e:
                    if e.error == SERVER_ERROR_TOO_MANY_REQUESTS:
                        raise GatewayRequestProcessingError(
                            NANO_REQUEST_ERROR_TOO_MANY_REQUESTS, "Too many requests (internal)"
                        ) from e
                    raise e
            else:
                response_frames = await worker_pool.request(
                    [msgpack_dumps(context), msgpack_dumps(request)],
                    server_prefix=server_prefix,
                    message_type=INTERNAL_REQUEST_MESSAGE,  # transmit to the internal handler
                )
                if len(response_frames) != 1:
                    raise GatewayRequestProcessingError(
                        NANO_REQUEST_ERROR_INTERNAL_FAILURE, "Server response framing is invalid", account_id=account_id
                    )
                try:
                    response = msgpack_loads(response_frames[0])
                except Exception:
                    raise GatewayRequestProcessingError(
                        NANO_REQUEST_ERROR_INTERNAL_FAILURE, "Server response format is invalid", account_id=account_id
                    )
                if not isinstance(response, dict):
                    raise GatewayRequestProcessingError(
                        NANO_REQUEST_ERROR_INTERNAL_FAILURE, "Server response type is invalid", account_id=account_id
                    )
            if isinstance(error := response.get(NanoServerResponse.ERROR), int):
                internal_response = {NanoServerResponse.ERROR: error}
            else:
                if not isinstance(payload := response.get(NanoServerResponse.INTERNAL_PAYLOAD), dict):
                    raise GatewayRequestProcessingError(
                        NANO_REQUEST_ERROR_INTERNAL_FAILURE, "Server response payload is invalid", account_id=account_id
                    )
                internal_response[InternalGatewayResponse.RESPONSE] = payload
    except AbstractAccessLoggableError as e:
        access_logger.error(e.log + " (internal)")
        internal_response = {NanoServerResponse.ERROR: e.code}
    except Exception:
        log_msg = GatewayRequestProcessingError.format_message(
            NANO_REQUEST_ERROR, "Unexpected error preprocessing request (internal)", account_id=account_id
        )
        access_logger.error(log_msg)
        logger.exception(log_msg)
        internal_response = {NanoServerResponse.ERROR: NANO_REQUEST_ERROR}
    return internal_response


async def state_internal_request_job(
    gws: GatewayState,
    worker_server_client: AioInprocClient,  # to respond
    message: Sequence[bytes],
):
    """
    Internal request processing is necessary to facilitate executing handlers that affect multiple servers.
    There is no cryptography on these messages, they come from secured processes that operate on validated
    requests already.
    These requests are also access logged for clarity, but they are appended an "(internal)" postfix.
    """
    try:
        internal_request = msgpack_loads(message[3])
    except Exception:  # pragma: no cover
        access_logger.error("Request serialization invalid (internal)")
        internal_response = {NanoServerResponse.ERROR: NANO_REQUEST_ERROR_PREPARSE}
    else:
        internal_response = await run_internal_gateway_request(gws, internal_request)
    # Send response.
    response_frames = [message[0], REPLY_MESSAGE, message[2], msgpack_dumps(internal_response)]
    worker_server_client.send_multipart(response_frames)
