import asyncio
import logging
from concurrent.futures import ThreadPoolExecutor
from typing import Optional

from clarabot_crypto.keyring.account_base import AbstractSelfAccountKeyring, AbstractPeerAccountKeyring
from clarabot_crypto.keyring.account_self import SelfAccountKeyring
from clarabot_crypto.keyring.adhoc import AdhocKeyring
from clarabot_crypto.keyring.room import RoomKeyring
from clarabot_crypto.keyring.room_base import AbstractRoomKeyring
from clarabot_nano_common.common.crypto.keyring import NanoRoomKeyring
from clarabot_nano_common.common.protocol.nano_request import NanoResponseStreamProcessor
from clarabot_nano_common.common.protocol.server import NanoServerResponse
from clarabot_nano_common.gateway.state.gateway import GatewayServerResponseStreamer
from clarabot_nano_common.utility.path.chroot import ChrootPath

from clarabot_nano.common.gateway.message import InternalGatewayRequest, InternalGatewayResponse
from clarabot_nano.gateway.job.internal import run_internal_gateway_request
from clarabot_nano.gateway.state.gateway import GatewayState
from clarabot_nano.inter.base import AbstractInterNanoRequester, AbstractDrivePathLocking
from clarabot_nano.inter.job import AbstractInterNanoJob

logger = logging.getLogger(__name__)


class GatewayInterNanoRequester(AbstractInterNanoRequester):
    def __init__(
        self,
        gws: GatewayState,
        self_kr: AbstractSelfAccountKeyring,
        remote_kr: AbstractPeerAccountKeyring,
        remote_room_id: int,
        # not necessary when remote's owner is self as well
        room_kr: Optional[AbstractRoomKeyring] = None,
        adhoc_token: Optional[str] = None,
        adhoc_kr: Optional[AdhocKeyring] = None,
    ):
        AbstractInterNanoRequester.__init__(self, self_kr, remote_kr, remote_room_id, room_kr, adhoc_token, adhoc_kr)
        self.gws = gws

    async def perform(self, pool: ThreadPoolExecutor, stream_processor: NanoResponseStreamProcessor):
        q = asyncio.Queue()

        async def _drain_queue():
            while True:
                x = await q.get()
                if x is None:
                    break
                if "message" not in x:
                    y = x.get("data")
                    if isinstance(y, dict):
                        y = y.get("nanoRequest")
                        if isinstance(y, dict):
                            x = y
                await self._loop.run_in_executor(pool, stream_processor.feed, x)

        req = {
            "type": "nano-room-sub",
            "variables": {
                "roomId": self.remote_room_id,
                "request": stream_processor.raw_req,
                "cacheKey": stream_processor.cache_key,
                "transferKey": stream_processor.transfer_key,
                "part": stream_processor.request_part,
                "ephPubKey": (
                    self.self_kr.get_public_key(AbstractSelfAccountKeyring.CURVE25519)
                    if self.self_kr.id == AbstractSelfAccountKeyring.ANONYMOUS_ID.encode("utf-8")
                    else None
                ),
                "adhocToken": self.adhoc_token,
            },
        }
        async with self.gws.server_request(req, GatewayServerResponseStreamer(q.put_nowait)) as future:
            drain_task = asyncio.create_task(_drain_queue())
            try:
                await asyncio.wait([future, drain_task], return_when=asyncio.FIRST_COMPLETED)
                if drain_task.done():
                    drain_task.result()  # raise on error
                if future.done():
                    future.result()  # raise on error
                q.put_nowait(None)  # signal finish
                await drain_task  # wait for processing of remaining responses
            finally:
                # silence not used result and exception in case of we're already handling an exception
                if not drain_task.done():
                    drain_task.cancel()
                await asyncio.gather(drain_task, return_exceptions=True)


class GatewayDrivePathLocking(AbstractDrivePathLocking):
    def __init__(self, gws: GatewayState):
        self.gws = gws

    async def call_ex_lock(self, account_id: int, room_id: int, path: str, token: str, ensure_dir: bool = False):
        await self._call_lock(account_id, room_id, path, token, "create", ensure_dir)

    async def call_ex_refresh_lock(self, account_id: int, room_id: int, path: str, token: str):
        await self._call_lock(account_id, room_id, path, token, "refresh")

    async def call_ex_unlock(self, account_id: int, room_id: int, path: str, token: str):
        await self._call_lock(account_id, room_id, path, token, "delete")

    async def _call_lock(self, account_id: int, room_id: int, path: str, token: str, op: str, ensure_dir: bool = False):
        gw_req = {
            InternalGatewayRequest.ACCOUNT_ID: account_id,
            InternalGatewayRequest.ROOM_ID: room_id,
            InternalGatewayRequest.REQUEST: {
                "type": "FS-L-X",
                "path": path,
                "op": op,
                "token": token,
                "nor": int(not ensure_dir),
                "edd": int(ensure_dir),
            },
        }
        result = await run_internal_gateway_request(self.gws, gw_req)
        response = result.get(InternalGatewayResponse.RESPONSE)
        if e := result.get(NanoServerResponse.ERROR) or not isinstance(response, dict):
            raise Exception(f"Gateway DPL internal lock error {e}")
        conflict = response.get("c")
        if conflict is None:
            pass
        elif not isinstance(conflict, str):
            raise Exception("Gateway DPL internal lock invalid")
        else:
            raise Exception("Gateway DPL internal lock conflict")

    async def resolve_chroot_by_room(self, room_id: int) -> Optional[ChrootPath]:
        return self.gws.extracted_config.resolve_chroot_of_room(room_id)


async def gateway_run_inter_nano_job(gws: GatewayState, job: AbstractInterNanoJob):
    try:
        if job.room_keys:
            # NOTE: When copying is to be done reading from a room with anonymous access, the Nano must perform the
            # requests as anonymous actually, to avoid accidentally revealing data by private access permissions from
            # the owner's identity.
            req_self_kr = SelfAccountKeyring.generate_anonymous()
            room_kr = NanoRoomKeyring(
                job.remote_room_id, {NanoRoomKeyring.PINNED: job.room_keys.get(RoomKeyring.PINNED)}
            )
            peer_kr = await gws.get_owner_keyring_of_room(job.remote_room_id_int64)
        else:
            # NOTE: It would be possible to hide the requesting account when using adhoc-token, but the deny_anonymous
            # policy could interfere with the copy in that case.
            # TODO: investigate deny_anonymous policy behaviour change for adhoc token processing
            req_self_kr = gws.keyring_cache.self_kr
            peer_kr, room_kr = await gws.get_room_owner_keyring(job.remote_room_id_int64)
        # This check prevents further processing when the E2E crypto setup would not work.
        if peer_kr.id == req_self_kr.id:
            room_kr = None  # remote is owned room/nano
        elif not room_kr and not job.adhoc_kr:
            logger.error("Missing room-like keyring for inter-nano job: %s", job.remote_room_id)
            return
    except Exception:
        logger.exception("Failed to setup peer and room keyring for inter-nano job: %s", job.remote_room_id)
        return
    chroot = gws.extracted_config.resolve_chroot_of_room(job.room_id_int64)
    if not chroot:
        logger.error("Failed to get drive-path of local-room for inter-nano job: %s", job.remote_room_id)
        return
    executor = job.EXECUTOR_CLS.create(
        job,
        GatewayInterNanoRequester(
            gws, req_self_kr, peer_kr, job.remote_room_id_int64, room_kr, job.adhoc_token, job.adhoc_kr
        ),
        GatewayDrivePathLocking(gws),
        gws.account_id,
        gws.part_size,
        job.room_id_int64,
        chroot,
        gws.inter_tmp_db,
    )
    return await executor.run()
