from __future__ import annotations

import asyncio
import logging
import time
from collections import deque
from typing import Optional, Dict, Type, List

import safe_radix32
from clarabot_crypto.keyring.adhoc import AdhocKeyring
from clarabot_nano_common.utility.path.chroot import ChrootPath
from ttldict2 import TTLDict

from clarabot_nano.inter.base import AbstractInterNanoExecutor, AbstractInterNanoRequester, AbstractDrivePathLocking
from clarabot_nano.inter.tmpfiledb import InterTemporaryFileDatabase

logger = logging.getLogger(__name__)


class AbstractInterNanoJob:
    TYPE: str = None
    EXECUTOR_CLS: Type[AbstractInterNanoJobExecutor]

    STATE_WAITING = "waiting"
    STATE_RUNNING = "running"
    STATE_CANCELLED = "cancelled"
    STATE_FAILED = "failed"
    STATE_COMPLETED = "completed"

    id: int = None
    state: str = STATE_WAITING
    finish_time: Optional[float] = None
    task: Optional[asyncio.Task] = None  # for cancelling?

    def __init__(
        self,
        account_id: str,  # account-id of the initiator
        room_id: str,  # room-id if the local nano
        remote_room_id: str,  # room-id of the remote nano
        room_keys: Optional[Dict[str, bytes]] = None,
        adhoc_token: Optional[str] = None,
        adhoc_kr: Optional[AdhocKeyring] = None,
    ):
        self.account_id = account_id
        self.room_id = room_id
        self.remote_room_id = remote_room_id
        # These are sensitive information, as they can provide access to a remote nano to other people if revealed.
        # They are only used if the remote Nano's owner does not match self.
        self.room_keys = room_keys
        self.adhoc_token = adhoc_token
        self.adhoc_kr = adhoc_kr
        if (self.adhoc_token is None) is not (self.adhoc_kr is None):
            raise Exception("Both adhoc token and secret must be provided or not")
        # computed
        self.account_id_int64 = safe_radix32.decode(self.account_id)
        self.room_id_int64 = safe_radix32.decode(self.room_id)
        self.remote_room_id_int64 = safe_radix32.decode(self.remote_room_id)

    def describe(self) -> dict:
        """
        Data to be returned to clients.
        """
        d = {
            "id": self.id,
            "type": self.TYPE,
            "state": self.state,
            "account_id": self.account_id,
            "room_id": self.room_id,
            "remote_room_id": self.remote_room_id,
        }
        if self.state in (self.STATE_CANCELLED, self.STATE_FAILED, self.STATE_COMPLETED):
            d["finish_time"] = self.finish_time
        return d

    def create_check_job(self) -> AbstractInterNanoJob:
        raise NotImplementedError()


class AbstractInterNanoJobExecutor(AbstractInterNanoExecutor):
    _job: AbstractInterNanoJob

    @classmethod
    def create(
        cls,
        job: AbstractInterNanoJob,
        inr: AbstractInterNanoRequester,
        dpl: AbstractDrivePathLocking,
        owner_account_id: int,
        part_size: int,
        drive_room_id: int,
        chroot: ChrootPath,
        temp_db: InterTemporaryFileDatabase,
    ):
        # NOTE: Cryptography setup cases relevant here:
        #   - remote's owner is self: Whom ever initiates the copy, there is no need to supply room keyring info.
        #   - remote's owner is other:
        #       - when self is member of remote: no additional keyring info needed
        #       - remote is accessed with anonymous URL: room-keyring from URL must be provided
        #       - remote is accessed with adhoc URL: room-keyring and adhoc-token must be provided
        # The benefit of utilizing the "self" and member access options (without adhoc-token) is that the copy will
        # be functional for Nanos that have `deny_anonymous` config set to True. The copy initiator client needs to
        # know about the membership status of the copying Nano's owner in the remote Nano's room to set up the optimal
        # keyring info.
        self = cls(
            inr,
            dpl,
            owner_account_id,
            part_size,
            drive_room_id,
            chroot,
            temp_db,
        )
        self._job = job
        return self


class AbstractInterNanoJobQueue:
    FINISH_KEEP_TIME = 12 * 60 * 60.0

    def __init__(self):
        self._loop = asyncio.get_running_loop()
        self._q_lock = asyncio.Lock()
        self._e = asyncio.Event()
        self._q = deque()
        self._i = 1
        self._history = TTLDict(self.FINISH_KEEP_TIME)

    async def add(self, job: AbstractInterNanoJob):
        async with self._q_lock:
            if not job.TYPE:
                raise Exception("inter nano job class missing type")
            if job.id is not None:
                raise Exception("inter nano job already initialized")
            while True:
                i = self._i
                self._i = 1 if i >= 1073741824 else i + 1
                if i in self._history:
                    continue
                if any(j.id == i for j in self._q):
                    continue
                job.id = i
                break
            self._q.append(job)
            self._e.set()

    async def get(self, id_: int) -> Optional[AbstractInterNanoJob]:
        async with self._q_lock:
            job = self._history.get(id_)
            if job:
                return job
            for job in self._q:
                if job.id == id_:
                    return job

    async def describe(self, job: AbstractInterNanoJob) -> dict:
        d = job.describe()
        async with self._q_lock:
            if not self._history.get(job.id):
                for i, qj in enumerate(self._q):
                    if job.id == qj.id:
                        d["queue_pos"] = i
        return d

    async def list(self, only_room: Optional[int] = None) -> List[dict]:
        jobs = []
        async with self._q_lock:
            for i, job in enumerate(self._q):
                if only_room is None or job.room_id_int64 == only_room:
                    d = job.describe()
                    d["queue_pos"] = i
                    jobs.append(d)
        return jobs

    async def delete(self, job: AbstractInterNanoJob):
        async with self._q_lock:
            try:
                self._q.remove(job)
            except ValueError:
                pass  # already finished
            else:
                if job.task:  # already started
                    if not job.task.done():
                        job.task.cancel()
                else:  # not started
                    job.state = job.STATE_CANCELLED
                    self._history[job.id] = job

    async def run(self):
        while True:
            await self._e.wait()
            async with self._q_lock:
                self._e.clear()
            while True:
                async with self._q_lock:
                    if len(self._q) == 0:
                        break
                    job = self._q[0]
                    job.state = job.STATE_RUNNING
                    job.task = self._loop.create_task(self._run_job(job))
                try:
                    await job.task
                except asyncio.CancelledError:
                    job.state = job.STATE_CANCELLED
                except Exception:
                    logger.exception("Failure during inter-nano job of %s:%s", job.TYPE, job.id)
                    job.state = job.STATE_FAILED
                else:
                    job.state = job.STATE_COMPLETED
                finally:
                    job.finish_time = time.time()
                    async with self._q_lock:
                        try:
                            self._q.remove(job)
                        except ValueError:
                            pass  # already deleted
                        self._history[job.id] = job

    async def _run_job(self, job: AbstractInterNanoJob):
        raise NotImplementedError()
