import os
import stat
from pathlib import Path

from clarabot_nano_common.common.error import log_escape_str
from clarabot_nano_common.common.handler.error import NanoHandlerError
from clarabot_nano_common.common.handler.mixin import AbstractNanoHandlerEvalMixin
from clarabot_nano_common.common.protocol.errors import NANO_REQUEST_ERROR_HANDLER_BAD_REQUEST
from clarabot_nano_common.common.protocol.request import NanoRequestContext
from clarabot_nano_common.utility.file import path_stat_differs
from clarabot_nano_common.utility.path.chroot import ChrootPath
from clarabot_nano_common.utility.path.validate import ValidationError, validate_path, validate_name
from tarka.utility.file.reserve import ReserveFile

from clarabot_nano.common.filesystem.operation.mkdir import MkdirOperation
from clarabot_nano.common.filesystem.operation.replace import ReplaceOperation
from clarabot_nano.common.filesystem.tx_queue import FilesystemTxQueue

AUTO_PATH_FILE = ReserveFile()


class AbstractEnsureDirectoryNanoHandlerMixin(AbstractNanoHandlerEvalMixin):
    chroot: ChrootPath
    fstx_queue: FilesystemTxQueue

    async def ensure_get_drive_directory(self, rel: str) -> Path:
        """
        Create, check and return the resolved absolute  automatic upload directory path by the chroot.
        """
        # First, try and resolve the new path as-is quickly to see if we need to create any directories at all.
        rel_path = Path(rel)
        validate_path(rel_path)
        for part in rel_path.parts:
            validate_name(part)
        if rel_path.is_absolute():
            raise ValidationError("Path must be relative")
        try:
            path, s = self.chroot.path_stat(rel_path)
        except (OSError, ValidationError):
            pass  # Does not exist, a parent is not a directory or there is some broken symlink in the path.
        else:
            if stat.S_ISDIR(s.st_mode):
                return path
        # We need to check and build the auto parent path. Any part of it ~may exist and not be a directory...
        # in this case we will be a little recalcitrant and move it away to make the directory for use. This
        # unfortunately means we need to gradually check and build the path and can't use "mkdir -p" behaviour.
        rel_paths = [rel_path]
        while len(rel_paths[-1].parts) > 1:
            rel_paths.append(rel_paths[-1].parent)
        for i in range(len(rel_paths) - 1, 0, -1):
            await self._check_move_make_directory(rel_paths[i])
        return await self._check_move_make_directory(rel_paths[0])

    async def _check_move_make_directory(self, rel_path: Path) -> Path:
        """
        Create, check and return the resolved absolute path by the chroot.
        """
        path, s = self.chroot.new_path(rel_path)
        if s is None:
            pass  # Target does not exist, we can create it.
        elif stat.S_ISDIR(s.st_mode):
            return path  # Target is either a directory or a symlink inside the drive to a directory.
        else:  # Target exists, but it is not a directory.
            async with self.fstx_queue.wait_ready(paths=(path.as_posix(),)):  # to avoid parent races
                alt_fd, alt_name = AUTO_PATH_FILE.open_reserve(path, try_as_is=False)
            alt_path = path.parent / alt_name
            try:
                async with self.fstx_queue.wait_ready(
                    ReplaceOperation(self.context[NanoRequestContext.ACCOUNT_ID], path.as_posix(), alt_path.as_posix())
                ) as submitter:
                    check_path, check_s = self.chroot.new_path(rel_path)
                    if check_s is None or stat.S_ISDIR(check_s.st_mode):
                        pass  # If it was removed or became a directory, then just skip submitting our move.
                    elif path_stat_differs(path, s, check_path, check_s):
                        raise NanoHandlerError(
                            NANO_REQUEST_ERROR_HANDLER_BAD_REQUEST,
                            f"Inconsistent move path for {log_escape_str(rel_path)}",
                            self,
                        )
                    else:
                        # Close and remove our exclusively created and currently tx-queue locked target to move to.
                        os.close(alt_fd)
                        alt_path.unlink()
                        alt_fd = None  # Must be before submit to ensure that the alt-path is not removed after move.
                        # Move target whatever it may be.
                        await submitter()  # TODO: timeout
            finally:
                if alt_fd is not None:
                    os.close(alt_fd)
                    alt_path.unlink(missing_ok=True)
        # Now create the target directory.
        async with self.fstx_queue.wait_ready(
            MkdirOperation(self.context[NanoRequestContext.ACCOUNT_ID], path.as_posix(), exist_ok=True)
        ) as submitter:
            if path != self.chroot.new_path(rel_path)[0]:
                raise NanoHandlerError(
                    NANO_REQUEST_ERROR_HANDLER_BAD_REQUEST,
                    f"Inconsistent make path for {log_escape_str(rel_path)}",
                    self,
                )
            await submitter()  # TODO: timeout
        return path
