import asyncio
import logging

from clarabot_nano_common.common.error import maybe_rad32
from clarabot_nano_common.gateway.job.keyring import gateway_load_peer_kr_impl
from clarabot_nano_common.gateway.tofudb import FirstUseIdentityDatabase
from clarabot_nano_common.gateway.utility import check_single_response, GatewayServerError

from clarabot_nano.gateway.errors import (
    SERVER_ERROR_NO_SESSION_BIND,
    SERVER_ERROR_ROOM_IS_NOT_SERVED,
    SERVER_ERROR_ROOM_IS_INVALID,
    SERVER_ERROR_TOO_MANY_REQUESTS,
)
from clarabot_nano.gateway.state.gateway import GatewayState

logger = logging.getLogger(__name__)


async def state_keyring_job(gws: GatewayState, job_queue: asyncio.Queue, tofu_db: FirstUseIdentityDatabase):
    """
    Serial processing of keyring loading requests and update events.
    """
    while True:
        job = await job_queue.get()
        t = job["type"]
        if t == "load-peer":
            await _load_peer_kr(gws, tofu_db, job["id"], job["future"])
        elif t == "trust-peer":
            await _update_peer_kr_trust(gws, job["id"], job["trust"])
        elif t == "load-room":
            await _load_room_kr(gws, job["id"], job["future"])
        elif t == "load-owner-room":
            await _load_owner_room_kr(gws, tofu_db, job["id"], job["future"])
        else:
            raise Exception(f"Unknown keyring job: {t}")


async def _load_peer_kr(
    gws: GatewayState, tofu_db: FirstUseIdentityDatabase, account_id: int, result_future: asyncio.Future
):
    """
    Request keyring data from the server for a peer account keyring.
    All errors are caught and propagated to the requesting future. Failure to load a single keyring shall
    not terminate the job-processing task.
    """
    try:
        kr = await gateway_load_peer_kr_impl(gws, tofu_db, account_id)
        result_future.set_result(kr)
    except GatewayServerError as e:
        if e.error in (SERVER_ERROR_NO_SESSION_BIND, SERVER_ERROR_TOO_MANY_REQUESTS):
            pass
        else:
            logger.exception(f"Error while loading peer-account keyring ({maybe_rad32(account_id)})")
        result_future.set_exception(e)
    except Exception as e:
        logger.exception(f"Error while loading peer-account keyring ({maybe_rad32(account_id)})")
        result_future.set_exception(e)


async def _update_peer_kr_trust(gws: GatewayState, account_id: int, trust: bytes):
    """
    Update the trust-state of a peer keyring if it is loaded.
    No errors shall happen here, the self keyring is always available.
    """
    kr = gws.keyring_cache.get_peer_kr(account_id)
    if not kr:
        return
    kr.update(trust, gws.keyring_cache.self_kr)


async def _load_room_kr(gws: GatewayState, room_id: int, result_future: asyncio.Future):
    """
    Request keyring data from the server for a room keyring.
    All errors are caught and propagated to the requesting future. Failure to load a single keyring shall
    not terminate the job-processing task.
    """
    try:
        async with gws.server_request({"type": "room-keys", "room_id": room_id}) as future:
            resp = check_single_response(await future, dict, "query room keys")
        kr = gws.keyring_cache.load_own_room_kr(room_id, resp["pinnedKey"])
        result_future.set_result(kr)
    except GatewayServerError as e:
        if e.error in (SERVER_ERROR_NO_SESSION_BIND, SERVER_ERROR_TOO_MANY_REQUESTS):
            pass
        elif e.error in (SERVER_ERROR_ROOM_IS_NOT_SERVED, SERVER_ERROR_ROOM_IS_INVALID):
            logger.warning(f"Error while loading room keyring: {e.error} ({maybe_rad32(room_id)})")
        else:
            logger.exception(f"Error while loading room keyring ({maybe_rad32(room_id)})")
        result_future.set_exception(e)
    except Exception as e:
        logger.exception(f"Error while loading room keyring ({maybe_rad32(room_id)})")
        result_future.set_exception(e)


async def _load_owner_room_kr(
    gws: GatewayState, tofu_db: FirstUseIdentityDatabase, room_id: int, result_future: asyncio.Future
):
    """
    Request keyring data from the server for a room keyring that may or may not be owned by current account.
    All errors are caught and propagated to the requesting future. Failure to load a single keyring shall
    not terminate the job-processing task.
    """
    try:
        async with gws.server_request({"type": "room-owner-and-keys", "room_id": room_id}) as future:
            resp = check_single_response(await future, dict, "query room owner-keys")
        if resp["ownerAccountId"] == gws.account_id:
            pkr = gws.keyring_cache.self_kr
            kr = gws.keyring_cache.load_own_room_kr(room_id, resp["pinnedKey"])
        else:
            pkr = await gateway_load_peer_kr_impl(gws, tofu_db, resp["ownerAccountId"])
            if resp.get("pinnedKey") or resp.get("savedKey"):
                kr = gws.keyring_cache.load_peer_room_kr(room_id, pkr, resp.get("pinnedKey"), resp.get("savedKey"))
            else:
                gws.keyring_cache.load_owner_of_peer_room(room_id, pkr)  # save the room-owner mapping for anon
                kr = None  # returning without room-keyring allows the use of adhoc-keyring for inter-nano processes
        result_future.set_result((pkr, kr))
    except GatewayServerError as e:
        if e.error in (SERVER_ERROR_NO_SESSION_BIND, SERVER_ERROR_TOO_MANY_REQUESTS):
            pass
        elif e.error in (SERVER_ERROR_ROOM_IS_NOT_SERVED, SERVER_ERROR_ROOM_IS_INVALID):
            logger.warning(f"Error while loading room keyring: {e.error} ({maybe_rad32(room_id)})")
        else:
            logger.exception(f"Error while loading room keyring ({maybe_rad32(room_id)})")
        result_future.set_exception(e)
    except Exception as e:
        logger.exception(f"Error while loading room keyring ({maybe_rad32(room_id)})")
        result_future.set_exception(e)
