import asyncio
import logging
from collections import deque

import safe_radix32

from clarabot_nano.gateway.state.gateway import GatewayState
from clarabot_nano.gateway.state.inter import GatewayInterNanoRequester, GatewayDrivePathLocking
from clarabot_nano.inter.backup import InterNanoBackupExecutor
from clarabot_nano.inter.tmpfiledb import InterTemporaryFileDatabase

logger = logging.getLogger(__name__)


async def state_backup_job(gws: GatewayState, job_queue: asyncio.Queue, temp_file_db: InterTemporaryFileDatabase):
    """
    This job task is responsible for performing periodic full backup copies of remote drives.
    TODO: This could be put into a separate process if we create a utility for easy server request submission from
          outside the gateway process.
    """
    do_polling = False
    current_targeted = None
    targeted_queue = deque()
    tasks = [asyncio.create_task(job_queue.get())]
    delay = 90.0  # first wait before backup processing (after start) shall be a little shorter
    try:
        while True:
            # handle requests and job state changes
            done, _ = await asyncio.wait(tasks, timeout=delay, return_when=asyncio.FIRST_COMPLETED)
            if not done:
                do_polling = True
            elif tasks[0] in done:
                job = tasks[0].result()
                tasks[0] = asyncio.create_task(job_queue.get())
                if job["type"] == "config-change":
                    do_polling = True
                elif job["type"] == "backup-room" and isinstance(job.get("target_room_id"), str):
                    if job["target_room_id"] == current_targeted and not tasks[-1].done() and not tasks[-1].cancelled():
                        # NOTE: By making a request restart the backup, a full scan and up-to-date state is synced.
                        tasks[-1].cancel()
                        if job["target_room_id"] in targeted_queue:  # requeue to be next in line
                            targeted_queue.remove(job["target_room_id"])
                        targeted_queue.appendleft(job["target_room_id"])
                    elif job["target_room_id"] not in targeted_queue:
                        targeted_queue.append(job["target_room_id"])
                else:
                    logger.warning("Unexpected job for backup queue %s", job)
            else:
                try:
                    tasks.pop(-1).result()
                except asyncio.CancelledError:
                    pass
                except Exception:
                    logger.exception("Unexpected exception during backup config processing")
                finally:
                    current_targeted = None
            # manage current backup task
            if len(tasks) == 2:
                if (
                    len(targeted_queue) > 0
                    and current_targeted is None
                    and not tasks[-1].done()
                    and not tasks[-1].cancelled()
                ):
                    tasks[-1].cancel()
            elif len(tasks) == 1:
                if len(targeted_queue) > 0:
                    current_targeted = targeted_queue.popleft()
                    tasks.append(asyncio.create_task(_check_backups(gws, temp_file_db, current_targeted)))
                elif do_polling:
                    do_polling = False
                    tasks.append(asyncio.create_task(_check_backups(gws, temp_file_db)))
            delay = 300.0
    finally:
        for task in tasks:
            if not task.done():
                task.cancel()
        await asyncio.gather(*tasks, return_exceptions=True)


async def _check_backups(gws: GatewayState, temp_file_db: InterTemporaryFileDatabase, target_backup_id: str = None):
    for drive_path, (room_id, backups) in gws.extracted_config.get_drive_backups().items():
        for packed_backup_id in backups:
            if target_backup_id and packed_backup_id != target_backup_id:
                continue
            try:
                await _check_backup(
                    gws, temp_file_db, drive_path, room_id, packed_backup_id, force_run=bool(target_backup_id)
                )
            except Exception:
                logger.exception(
                    "Unexpected exception during backup-process for %s in drive %s", packed_backup_id, drive_path
                )


async def _check_backup(
    gws: GatewayState,
    temp_file_db: InterTemporaryFileDatabase,
    drive_path: str,
    drive_room_id: int,
    packed_room_id: str,
    force_run: bool = False,
):
    try:
        backup_room_id = safe_radix32.decode(packed_room_id)
    except (UnicodeEncodeError, OverflowError):
        logger.warning("Ignoring invalid room-id for backup: %s", packed_room_id)
        return
    try:
        peer_kr, room_kr = await gws.get_room_owner_keyring(backup_room_id)
        if peer_kr.id == gws.keyring_cache.self_kr.id:
            room_kr = None  # remote is owned room/nano
    except Exception:
        logger.warning("Failed to get peer and room keyring for backup: %s", packed_room_id)
        return
    bup_exec = InterNanoBackupExecutor(
        GatewayInterNanoRequester(gws, gws.keyring_cache.self_kr, peer_kr, backup_room_id, room_kr),
        GatewayDrivePathLocking(gws),
        gws.account_id,
        gws.part_size,
        drive_room_id,
        gws.extracted_config.resolve_chroot(drive_path),
        temp_file_db,
        gws.extracted_config.backup_interval,
        gws.extracted_config.name,
        force_run,
    )
    await bup_exec.run()
