from io import StringIO
from typing import Tuple, Optional

from clarabot_nano_common.common.error import log_escape_str
from clarabot_nano_common.common.handler.error import NanoHandlerError
from clarabot_nano_common.common.protocol.errors import (
    NANO_REQUEST_ERROR_HANDLER_BAD_REQUEST,
    NANO_REQUEST_ERROR_HANDLER_DENIED,
    NANO_REQUEST_ERROR_INVALID_NEW_NAME,
    NANO_REQUEST_ERROR_HANDLER_TARGET_EXISTS,
    NANO_REQUEST_ERROR_HANDLER_TARGET_NOT_FOUND,
    NANO_REQUEST_ERROR_LOCKED_PATH,
)
from clarabot_nano_common.common.protocol.request import NanoRequestContext
from clarabot_nano_common.common.protocol.response import NanoBaseResponse
from clarabot_nano_common.server.base.config import ServerConfig
from clarabot_nano_common.server.base.handler.base import ServerHandleHelper
from clarabot_nano_common.utility.file import path_stat_differs
from clarabot_nano_common.utility.path.chroot import NewNameValidationError
from clarabot_nano_common.utility.path.validate import ValidationError

from clarabot_nano.common.filesystem.operation.rename import RenameOperation
from clarabot_nano.common.filesystem.tx_queue import TokenLockError
from clarabot_nano.common.request.server_fstx import NanoFilesystemMoveRequest
from clarabot_nano.server.fstx.handler.base import AbstractFilesystemTransactorNanoHandler


class NanoFilesystemMoveResponse(NanoBaseResponse):
    NORMALIZED_NAME = "norm_name"  # may be different from the input name due to normalization


class MoveSystemHandler(AbstractFilesystemTransactorNanoHandler):
    EXECUTION_STATE_CAN_CONFLICT = False  # cacheable and request_key are constant, no conflict is possible

    HANDLER_KEY = NanoFilesystemMoveRequest.TYPE_KEY

    def __init__(self, helper: ServerHandleHelper, extracted_config: ServerConfig, context: dict, payload: dict):
        AbstractFilesystemTransactorNanoHandler.__init__(self, helper, extracted_config, context, payload)
        if not isinstance(self.payload.get(NanoFilesystemMoveRequest.SRC_PATH), str):
            raise NanoHandlerError(NANO_REQUEST_ERROR_HANDLER_BAD_REQUEST, "Missing source path", self)
        if not isinstance(self.payload.get(NanoFilesystemMoveRequest.NEW_PATH), str):
            raise NanoHandlerError(NANO_REQUEST_ERROR_HANDLER_BAD_REQUEST, "Missing target path", self)
        if not self.can_write_drive():  # anonymous is denied by mutation-id requirement
            raise NanoHandlerError(NANO_REQUEST_ERROR_HANDLER_DENIED, "No permission", self)
        self.src_path = None
        self.new_path = None

    async def _create_request_key(self, secure: bool = False) -> Tuple[bool, Optional[list], int]:
        return self._secure_request_key(False, None, 1) if secure else False, None, 1

    async def _execute_initial(self) -> dict:
        d = await AbstractFilesystemTransactorNanoHandler._execute_initial(self)
        try:
            try:
                self.src_path, src_s = self.chroot.new_path(self.payload[NanoFilesystemMoveRequest.SRC_PATH])
            except NewNameValidationError:  # Src should be known to exist, so "new" name error would be confusing.
                raise NanoHandlerError(NANO_REQUEST_ERROR_HANDLER_BAD_REQUEST, "Invalid path", self)
            except OSError:
                raise NanoHandlerError(NANO_REQUEST_ERROR_HANDLER_TARGET_NOT_FOUND, "Not found", self)
            if src_s is None:
                raise NanoHandlerError(NANO_REQUEST_ERROR_HANDLER_TARGET_NOT_FOUND, "Not found", self)
            self.new_path, new_s = self.chroot.new_path(
                self.payload[NanoFilesystemMoveRequest.NEW_PATH],
                name_preprocess_fn=self.extracted_config.normalize_remote_path,
            )
            if new_s is not None and (src_s.st_dev != new_s.st_dev or src_s.st_ino != new_s.st_ino):
                raise NanoHandlerError(NANO_REQUEST_ERROR_HANDLER_TARGET_EXISTS, "Already exists", self)
            async with self.helper.fstx_queue.wait_ready(
                RenameOperation(
                    self.context[NanoRequestContext.ACCOUNT_ID], self.src_path.as_posix(), self.new_path.as_posix()
                )
            ) as submitter:
                # Final check before execution. This can fail if contesting operations have changed the filesystem.
                try:
                    src_path2, src_s2 = self.chroot.new_path(self.payload[NanoFilesystemMoveRequest.SRC_PATH])
                except OSError:
                    raise NanoHandlerError(NANO_REQUEST_ERROR_HANDLER_TARGET_NOT_FOUND, "Not found", self)
                if src_s2 is None:
                    raise NanoHandlerError(NANO_REQUEST_ERROR_HANDLER_TARGET_NOT_FOUND, "Not found", self)
                new_path2, new_s2 = self.chroot.new_path(
                    self.payload[NanoFilesystemMoveRequest.NEW_PATH],
                    name_preprocess_fn=self.extracted_config.normalize_remote_path,
                )
                if new_s2 is not None and (src_s.st_dev != new_s2.st_dev or src_s.st_ino != new_s2.st_ino):
                    raise NanoHandlerError(NANO_REQUEST_ERROR_HANDLER_TARGET_EXISTS, "Already exists", self)
                if path_stat_differs(self.src_path, src_s, src_path2, src_s2) or path_stat_differs(
                    self.new_path, new_s, new_path2, new_s2
                ):
                    raise ValidationError("Depending paths are contested")
                await submitter()  # TODO: timeout
                # NOTE: Moving a file can still facilitate a diversion-attack. Imagine renaming an "rm" executable
                # to "cat" for example.
                self._set_remote_file_security(self.new_path)
        except NewNameValidationError as e:
            raise NanoHandlerError(NANO_REQUEST_ERROR_INVALID_NEW_NAME, "Invalid name", self) from e
        except ValidationError as e:
            raise NanoHandlerError(
                NANO_REQUEST_ERROR_HANDLER_BAD_REQUEST, "Invalid path", self, maybe_conflict=True
            ) from e
        except OSError as e:
            raise NanoHandlerError(
                NANO_REQUEST_ERROR_HANDLER_BAD_REQUEST, "Unresolved path", self, maybe_conflict=True
            ) from e
        except TokenLockError as e:
            raise NanoHandlerError(NANO_REQUEST_ERROR_LOCKED_PATH, "Locked path", self) from e
        d[NanoFilesystemMoveResponse.NORMALIZED_NAME] = self.new_path.name
        return d

    async def _execute_part(self, part: int, s_hmac_start_part: Optional[int]) -> dict:  # pragma: no cover
        raise NanoHandlerError(NANO_REQUEST_ERROR_HANDLER_BAD_REQUEST, "Part request is invalid", self)

    def _log_message(self) -> str:
        return f"Move {log_escape_str(self.src_path)} to {log_escape_str(self.new_path)}"

    def build_error_message(self, buf: StringIO) -> None:
        buf.write("Move ")
        buf.write(log_escape_str(self.payload.get(NanoFilesystemMoveRequest.SRC_PATH)))
        buf.write(" to ")
        buf.write(log_escape_str(self.payload.get(NanoFilesystemMoveRequest.NEW_PATH)))
        buf.write(" in ")
        buf.write(log_escape_str(self.get_drive_root_for_error_message()))
