from io import StringIO
from pathlib import Path
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_INVALID_NEW_NAME,
    NANO_REQUEST_ERROR_HANDLER_TARGET_EXISTS,
    NANO_REQUEST_ERROR_HANDLER_FAILURE,
    NANO_REQUEST_ERROR_LOCKED_PATH,
)
from clarabot_nano_common.common.protocol.request import NanoRequestContext
from clarabot_nano_common.common.protocol.server import NanoServerResponse
from clarabot_nano_common.server.base.config import ServerConfig
from clarabot_nano_common.server.base.handler.base import AbstractNanoServerHandler, ServerHandleHelper
from clarabot_nano_common.utility.path.chroot import NewNameValidationError
from clarabot_nano_common.utility.path.common import (
    normalize_validate_resolve_absolute_path,
    normalize_validate_absolute_path,
)
from clarabot_nano_common.utility.path.resolve import resolve_path
from clarabot_nano_common.utility.path.validate import validate_name, ValidationError

from clarabot_nano.common.filesystem.operation.mkdir import MkdirOperation
from clarabot_nano.common.filesystem.tx_queue import TokenLockError
from clarabot_nano.common.request.server_fstx import AdminMkdirRequest
from clarabot_nano.server.fstx.handler.base import FilesystemTransactorServerHandleHelper


class AdminCreateDirectoryHandler(AbstractNanoServerHandler):
    """
    The owner may create directories outside of drives when browsing for a drive root.
    """

    REQUIRES_OWNER_IDENTITY = True
    REQUIRES_ADMIN_SESSION = True
    EXECUTION_STATE_CAN_CONFLICT = False  # cacheable and request_key are constant, no conflict is possible

    HANDLER_KEY = AdminMkdirRequest.TYPE_KEY

    helper: FilesystemTransactorServerHandleHelper

    def __init__(self, helper: ServerHandleHelper, extracted_config: ServerConfig, context: dict, payload: dict):
        AbstractNanoServerHandler.__init__(self, helper, extracted_config, context, payload)
        if not isinstance(self.payload.get(AdminMkdirRequest.PATH), str):
            raise NanoHandlerError(NANO_REQUEST_ERROR_HANDLER_BAD_REQUEST, "Missing path", self)
        if not isinstance(self.payload.get(AdminMkdirRequest.NAME), str):
            raise NanoHandlerError(NANO_REQUEST_ERROR_HANDLER_BAD_REQUEST, "Missing name", self)
        self.path = None
        self.name = None

    async def execute(self) -> dict:
        response = await AbstractNanoServerHandler.execute(self)
        response[NanoServerResponse.TOUCH_ADMIN_SESSION] = True
        return response

    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 AbstractNanoServerHandler._execute_initial(self)
        try:
            name = self.extracted_config.normalize_remote_path(self.payload[AdminMkdirRequest.NAME])
            try:
                validate_name(name)
            except ValidationError:
                # Make this validation detectable and separate from other path name validation issues.
                raise NewNameValidationError()
            # NOTE: Sensitive restriction is not relevant for mkdir, because not all sensitive places are recursively
            # protected. This means the admin may create a not sensitive folder at a sensitive place, eg. their home.
            path = Path(normalize_validate_absolute_path(self.payload[AdminMkdirRequest.PATH]))
            try:
                self.extracted_config.drive_restriction_validator(path)
                path = resolve_path(path, strict=True)
                self.extracted_config.drive_restriction_validator(path)
            except ValidationError:
                raise NanoHandlerError(NANO_REQUEST_ERROR_HANDLER_FAILURE, "Outside of restricted paths", self)
            self.path = path / name
            if self.path.exists():
                raise NanoHandlerError(NANO_REQUEST_ERROR_HANDLER_TARGET_EXISTS, "Already exists", self)
            async with self.helper.fstx_queue.wait_ready(
                MkdirOperation(self.context[NanoRequestContext.ACCOUNT_ID], self.path.as_posix())
            ) as submitter:
                # Final check before execution. This can fail if contesting operations have changed the filesystem.
                if self.path != normalize_validate_resolve_absolute_path(self.payload[AdminMkdirRequest.PATH]) / name:
                    raise ValidationError("Depending paths are contested")
                if self.path.exists():
                    raise NanoHandlerError(NANO_REQUEST_ERROR_HANDLER_TARGET_EXISTS, "Already exists", self)
                await submitter()  # TODO: timeout
        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
        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"Admin make directory {log_escape_str(self.path)}"

    def build_error_message(self, buf: StringIO) -> None:
        buf.write("Admin make directory ")
        buf.write(log_escape_str(self.payload.get(AdminMkdirRequest.NAME)))
        buf.write(" in ")
        buf.write(log_escape_str(self.payload.get(AdminMkdirRequest.PATH)))
