import asyncio
import logging

from clarabot_nano.common.error import maybe_rad32
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
from clarabot_nano.gateway.tofudb import FirstUseIdentityDatabase
from clarabot_nano.gateway.utility import check_single_response, GatewayServerError

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"])
        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:
        async with gws.server_request({"type": "account-keys", "account_id": account_id}) as future:
            resp = check_single_response(await future, dict, "query account keys")
        tofus = await tofu_db.get_tofu(account_id)
        kr = gws.keyring_cache.load_peer_kr(
            account_id,
            resp["encryptingPublicEc"],
            resp["encryptingPublicEcSignature"],
            resp["signingPublicEc"],
            resp["trust"],
            tofus,
        )
        if kr.tofu_fingerprints:
            new_fingerprints = dict()
            for key_version, fingerprint in kr.tofu_fingerprints.items():
                if key_version not in tofus:
                    new_fingerprints[key_version] = fingerprint
            if new_fingerprints:
                await tofu_db.insert_tofu(account_id, new_fingerprints)
        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)
