import asyncio
import random
import time
from concurrent.futures.thread import ThreadPoolExecutor
from contextlib import asynccontextmanager
from pathlib import Path
from typing import Sequence, Type, AsyncContextManager, List

from clarabot_nano.common.zmq.inproc_client import AioInprocClient
from clarabot_nano.server.base.handler.base import AbstractNanoServerHandler
from clarabot_nano.server.base.process import ServerProcess
from clarabot_nano.server.fstx.filesystem.operation.delete import DeleteOperation
from clarabot_nano.server.fstx.filesystem.tx_queue import FilesystemTxQueue
from clarabot_nano.server.fstx.handler.admin.mkdir import AdminCreateDirectoryHandler
from clarabot_nano.server.fstx.handler.base import FilesystemTransactorServerHandleHelper
from clarabot_nano.server.fstx.handler.filesystem.delete import DeleteSystemHandler
from clarabot_nano.server.fstx.handler.filesystem.mkdir import MakeDirectorySystemHandler
from clarabot_nano.server.fstx.handler.filesystem.move import MoveSystemHandler
from clarabot_nano.server.fstx.handler.filesystem.put import PutSystemHandler
from clarabot_nano.server.fstx.tmpfiledb import TemporaryFileDatabase


class FilesystemTransactorServerProcess(ServerProcess):
    TEMP_FILE_CLEANUP_AFTER = 60 * 20  # 20 minutes
    TEMP_FILE_ABANDON_AFTER = 60 * 60 * 14  # 14 days

    def __init__(self, pool: ThreadPoolExecutor, server_prefix: str):
        self._temp_file_db = None
        self._fstx_queue = FilesystemTxQueue(pool)
        ServerProcess.__init__(self, pool, server_prefix, log_with_pid=True)

    @asynccontextmanager
    async def _create_handle_helper(
        self, extracted_config
    ) -> AsyncContextManager[FilesystemTransactorServerHandleHelper]:
        with TemporaryFileDatabase.create(
            asyncio.get_event_loop(), str(extracted_config.data_dir / "fstx.db"), sqlite_timeout=10.0
        ) as temp_file_db:
            try:
                yield FilesystemTransactorServerHandleHelper(self._pool, temp_file_db, self._fstx_queue)
            finally:
                await self._fstx_queue.close()  # TODO: timeout

    def _init_get_handler_classes(self) -> Sequence[Type[AbstractNanoServerHandler]]:
        return [
            AdminCreateDirectoryHandler,
            MakeDirectorySystemHandler,
            DeleteSystemHandler,
            MoveSystemHandler,
            PutSystemHandler,
        ]

    def _create_work_tasks(
        self, gateway_client: AioInprocClient, helper: FilesystemTransactorServerHandleHelper
    ) -> List[asyncio.Future]:
        return [asyncio.create_task(self._cleanup_old_temp_files(helper))]

    def _worker_start_delay(self) -> float:
        return 20.0 + 40.0 * random.random()

    async def _cleanup_old_temp_files(self, helper: FilesystemTransactorServerHandleHelper):
        """
        The used temp files are tracked using absolute system paths, they will be cleaned up properly even if a
        drive has been deleted usually. Until cleanup takes place an upload can be continued for the same path by
        the same user.
        """
        await asyncio.sleep(
            self._worker_start_delay()
        )  # wait a little at startup to stagger the initial load generated by all the workers
        while True:
            old_rows = await helper.temp_file_db.get_old_tmp_files(self.TEMP_FILE_CLEANUP_AFTER, 100)  # TODO: limit
            for tmp_id, _, target_path, tmp_name, _, last_used in old_rows:
                try:
                    # NOTE: account_id for operation would allow tracking access to this operation
                    async with helper.fstx_queue.wait_ready(
                        DeleteOperation(0, (Path(target_path).parent / tmp_name).as_posix())
                    ) as submitter:
                        await submitter()  # TODO: timeout
                except Exception:
                    # Several issues could exist to prevent removing the temporary file, and we can't discern if
                    # they are temporary or permanent generally. For example, after a long shutdown a drive is
                    # missing, where a file shall be cleaned up. In this case abandoning it may be okay if the
                    # drive/path never comes online again, but we can't know that.
                    if last_used + self.TEMP_FILE_ABANDON_AFTER > time.time():
                        # Do not abandon cleaning up this temp file yet.
                        continue
                await helper.temp_file_db.drop_tmp_file(tmp_id)
            await asyncio.sleep(self.TEMP_FILE_CLEANUP_AFTER >> 2)
