from __future__ import annotations

import stat
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
from typing import Optional, Dict

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

from clarabot_nano.inter.check import InterNanoCheckJob
from clarabot_nano.inter.job import AbstractInterNanoJob, AbstractInterNanoJobExecutor


class InterNanoCopyExecutor(AbstractInterNanoJobExecutor):
    _job: InterNanoCopyJob

    NAME_PREFIX = "CPY-"

    async def _run_custom(self, pool: ThreadPoolExecutor):
        # NOTE: Unless the source room matches the target, the initiator is checked to either be the owner of the
        # current Nano or has provided an adhoc-token, which has been checked to be valid at the job-creation handler.
        # This means that the access of the initiator is verified at this point to have access to the source drive,
        # regardless of what Nano it is served by. If there is a drive config for serving the remote room on this Nano,
        # that chroot is resolved here to test source-target copying containment. TODO and perform optimized copy
        remote_chroot_local = await self.dpl.resolve_chroot_by_room(self._job.remote_room_id_int64)
        new_folder, s = self.chroot.path_stat(self._job.drive_path)
        if not stat.S_ISDIR(s.st_mode):
            raise NotADirectoryError("Local copy target must be a directory")
        if self._job.remote_items is None:  # copy the remote item directly (no sub-selection)
            if remote_chroot_local:
                old_folder = self._get_check_local_path(remote_chroot_local, new_folder, self._job.remote_path)
                if old_folder.parent == new_folder:
                    # NOTE: It is possible that copying to a parent path will eventually replace the source by conflict
                    # fixing, but we can't really check that beside this trivial case. The copy will halt and the user
                    # will have to look into what happened.
                    raise OSError("Cannot copy local directory onto itself")
                # TODO: Implement optimized local-copy instead of performing the inter-nano copy...
            await self._sync_remote_item_recursively(pool, None, None, new_folder, self._job.remote_path, True)
        elif not self._job.remote_items:  # empty list means copy all items from remote directory
            if remote_chroot_local:
                _ = self._get_check_local_path(remote_chroot_local, new_folder, self._job.remote_path)
                # TODO: Implement optimized local-copy instead of performing the inter-nano copy...
            await self._sync_remote_directory_recursively(pool, None, None, new_folder, self._job.remote_path, True)
        else:  # copy specific items from remote directory
            if remote_chroot_local:
                for remote_name in self._job.remote_items:  # check each item in advance
                    _ = self._get_check_local_path(
                        remote_chroot_local,
                        new_folder,
                        f"{self._job.remote_path}/{remote_name}" if self._job.remote_path else remote_name,
                    )
                # TODO: Implement optimized local-copy instead of performing the inter-nano copy...
            for remote_name in self._job.remote_items:
                remote_path = f"{self._job.remote_path}/{remote_name}" if self._job.remote_path else remote_name
                await self._sync_remote_item_recursively(pool, None, None, new_folder, remote_path, True)

    async def _copy_file_old_to_new(self, pool: ThreadPoolExecutor, src: Path, trg: Path) -> bool:  # pragma: no cover
        raise Exception("No old source support")

    def _get_check_local_path(self, remote_chroot_local: ChrootPath, new_folder: Path, remote_path: str) -> Path:
        # NOTE: In case the remote room/drive is served by this Nano, we must check that the target path is not
        # contained by the source path after full resolution to avoid circular recursive copying.
        old_folder = remote_chroot_local.path(remote_path)
        try:
            new_folder.relative_to(old_folder)
        except ValueError:
            pass
        else:
            raise OSError("Cannot copy local directory into itself")
        return old_folder


class InterNanoCopyJob(AbstractInterNanoJob):
    TYPE = "copy"
    EXECUTOR_CLS = InterNanoCopyExecutor

    def __init__(
        self,
        account_id: str,  # account-id of the initiator
        room_id: str,  # room-id if the local nano
        drive_path: str,  # directory in local drive to copy to
        remote_room_id: str,  # room-id of the remote nano
        remote_path: str,  # directory in remote drive to copy from
        # This has to be None for file path, otherwise None means copy the directory, empty list means copy all items
        # in the directory, non-empty list means to copy the specified items from the directory.
        remote_items: Optional[list[str]],
        room_keys: Optional[Dict[str, bytes]] = None,
        adhoc_token: Optional[str] = None,
        adhoc_kr: Optional[AdhocKeyring] = None,
    ):
        AbstractInterNanoJob.__init__(self, account_id, room_id, remote_room_id, room_keys, adhoc_token, adhoc_kr)
        self.drive_path = drive_path
        self.remote_path = remote_path
        self.remote_items = remote_items

    def describe(self) -> dict:
        d = AbstractInterNanoJob.describe(self)
        d.update(
            {
                "drive_path": self.drive_path,
                "remote_path": self.remote_path,
                "remote_items": self.remote_items,
            }
        )
        return d

    def create_check_job(self) -> AbstractInterNanoJob:
        return InterNanoCheckJob(
            self.account_id,
            self.room_id,
            self.drive_path,
            self.remote_room_id,
            self.remote_path,
            self.room_keys,
            self.adhoc_token,
            self.adhoc_kr,
        )
