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,
)
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.filesystem.tx_queue import TokenLockError
from clarabot_nano.common.handler.filesystem.fstx import AbstractEnsureDirectoryNanoHandlerMixin
from clarabot_nano.common.request.server_fstx import NanoFilesystemLockRequest
from clarabot_nano.server.fstx.handler.base import AbstractFilesystemTransactorNanoHandler


class NanoFilesystemLockResponse(NanoBaseResponse):
    CONFLICT = "c"


class LockSystemHandler(AbstractFilesystemTransactorNanoHandler, AbstractEnsureDirectoryNanoHandlerMixin):
    EXECUTION_STATE_CAN_CONFLICT = False  # cacheable and request_key are constant, no conflict is possible
    ALLOW_INTERNAL_REQUEST_EXECUTION = True

    HANDLER_KEY = NanoFilesystemLockRequest.TYPE_KEY

    def __init__(self, helper: ServerHandleHelper, extracted_config: ServerConfig, context: dict, payload: dict):
        AbstractFilesystemTransactorNanoHandler.__init__(self, helper, extracted_config, context, payload)
        self.fstx_queue = self.helper.fstx_queue
        if not isinstance(self.payload.get(NanoFilesystemLockRequest.PATH), str):
            raise NanoHandlerError(NANO_REQUEST_ERROR_HANDLER_BAD_REQUEST, "Missing path", self)
        token = self.payload.get(NanoFilesystemLockRequest.TOKEN)
        if not isinstance(token, str) or not token or len(token) >= 256:
            raise NanoHandlerError(NANO_REQUEST_ERROR_HANDLER_BAD_REQUEST, "Invalid token", self)
        self.token = token
        try:
            self.ensure_dir = bool(int(self.payload.get(NanoFilesystemLockRequest.EDD, 0)))
        except Exception:
            raise NanoHandlerError(NANO_REQUEST_ERROR_HANDLER_BAD_REQUEST, "Invalid edd", self)
        try:
            self.no_resolve = bool(int(self.payload.get(NanoFilesystemLockRequest.NOR, 0)))
        except Exception:
            raise NanoHandlerError(NANO_REQUEST_ERROR_HANDLER_BAD_REQUEST, "Invalid nor", self)
        op = self.payload.get(NanoFilesystemLockRequest.OP)
        if not isinstance(op, str) or op not in ("create", "delete", "refresh", "replace"):
            raise NanoHandlerError(NANO_REQUEST_ERROR_HANDLER_BAD_REQUEST, "Invalid token", self)
        self.op = op
        if self.op == "replace":
            old_token = self.payload.get(NanoFilesystemLockRequest.OLD)
            if not isinstance(old_token, str) or not old_token or len(old_token) >= 256:
                raise NanoHandlerError(NANO_REQUEST_ERROR_HANDLER_BAD_REQUEST, "Invalid old token", self)
            self.old_token = old_token
        elif self.payload.get(NanoFilesystemLockRequest.OLD) is not None:
            raise NanoHandlerError(NANO_REQUEST_ERROR_HANDLER_BAD_REQUEST, "Invalid old token", self)
        else:
            self.old_token = None
        if not self.can_write_drive():
            raise NanoHandlerError(NANO_REQUEST_ERROR_HANDLER_DENIED, "No permission", self)
        self.path = None

    async def _execute_internal(self, internal_request: bool = True) -> dict:
        if internal_request and self.no_resolve:  # NOTE: only for internal requests
            self.path = self.no_resolve_chroot_path(self.chroot, self.payload[NanoFilesystemLockRequest.PATH])
        else:
            if internal_request and self.ensure_dir:
                await self.ensure_get_drive_directory(self.payload[NanoFilesystemLockRequest.PATH])
            paths, s = self.resolve_chroot_paths_stat(self.chroot, self.payload[NanoFilesystemLockRequest.PATH])
            self.path = paths[-1][0]
        try:
            if self.op == "create":
                self.helper.fstx_queue.ex_lock(self.path.as_posix(), self.token)
            elif self.op == "delete":
                self.helper.fstx_queue.ex_unlock(self.path.as_posix(), self.token)
            elif self.op == "refresh":
                self.helper.fstx_queue.ex_refresh_lock(self.path.as_posix(), self.token)
            elif self.op == "replace":
                self.helper.fstx_queue.ex_replace_lock(self.path.as_posix(), self.token, self.old_token)
            else:  # pragma: no cover
                raise NotImplementedError()
        except TokenLockError as e:
            return {NanoFilesystemLockResponse.CONFLICT: e.token}
        return {}

    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)
        d.update(await self._execute_internal(internal_request=False))
        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:
        msg = f"{self.op.title()} lock {log_escape_str(self.path)} with {log_escape_str(self.token)}"
        if self.op == "replace":
            return msg + f" and {log_escape_str(self.old_token)}"
        return msg

    def build_error_message(self, buf: StringIO) -> None:
        buf.write(str(self.payload.get(NanoFilesystemLockRequest.OP)).title())
        buf.write(" lock ")
        buf.write(log_escape_str(self.payload.get(NanoFilesystemLockRequest.PATH)))
        buf.write(" in ")
        buf.write(log_escape_str(self.get_drive_root_for_error_message()))
        buf.write(" with ")
        buf.write(log_escape_str(self.payload.get(NanoFilesystemLockRequest.TOKEN)))
        if self.op == "replace":
            buf.write(" and ")
            buf.write(log_escape_str(self.payload.get(NanoFilesystemLockRequest.OLD)))
