import asyncio
import stat
from io import StringIO
from pathlib import Path
from typing import Tuple, Optional, List, Union

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_FAILURE,
)
from clarabot_nano_common.common.protocol.request import NanoRequestContext
from clarabot_nano_common.common.protocol.response import NanoBaseResponse
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 ServerHandleHelper
from clarabot_nano_common.server.general.handler.base import AbstractGeneralNanoServerHandler
from clarabot_nano_common.server.general.handler.filesystem.utility import file_stat, add_disk_usage
from clarabot_nano_common.utility.path.common import normalize_validate_absolute_path
from clarabot_nano_common.utility.path.resolve import resolve_path
from clarabot_nano_common.utility.path.root import get_root_paths
from clarabot_nano_common.utility.path.validate import ValidationError

from clarabot_nano.common.request.server_general import AdminGetListingRequest
from clarabot_nano.server.general.handler.admin.utility import (
    list_admin_directory,
    ls_admin_entry_size,
    list_admin_roots,
)


class AdminGetListingResponse(NanoBaseResponse):
    LISTING = "ls"
    TOTAL = "total"
    FREE = "free"


class GetListingHandler(AbstractGeneralNanoServerHandler):
    REQUIRES_OWNER_IDENTITY = True
    REQUIRES_ADMIN_SESSION = True

    HANDLER_KEY = AdminGetListingRequest.TYPE_KEY

    def __init__(self, helper: ServerHandleHelper, extracted_config: ServerConfig, context: dict, payload: dict):
        AbstractGeneralNanoServerHandler.__init__(self, helper, extracted_config, context, payload)
        if not isinstance(self.payload.get(AdminGetListingRequest.PATH), str):
            raise NanoHandlerError(NANO_REQUEST_ERROR_HANDLER_BAD_REQUEST, "Missing path", self)
        self.path: Union[str, Path] = None
        self.ls: List[List[dict]] = None

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

    async def _create_request_key(self, secure: bool = False) -> Tuple[bool, Optional[list], int]:
        if not self.payload[AdminGetListingRequest.PATH]:
            # Empty path means to return the valid path-roots and locations to start from.
            if self.path is None:
                self.path = "roots"  # used at logging
                if self.extracted_config.drive_restriction_validator.paths:
                    root_paths = [p.as_posix() for p in self.extracted_config.drive_restriction_validator.paths]
                else:
                    # NOTE: Not using threads for the root listing. These are either a constant value (for posix) or
                    # they require COM from pywin32 which would need to be initialised for the calling thread. This
                    # is a very rarely used API call and branch, so fixing the thread-execution is not worth it.
                    root_paths = get_root_paths()
                    # root_paths = await asyncio.get_event_loop().run_in_executor(self.helper.pool, get_root_paths)
                try:
                    ls = await asyncio.get_event_loop().run_in_executor(self.helper.pool, list_admin_roots, root_paths)
                except OSError as e:  # pragma: no cover; cannot simulate this
                    raise NanoHandlerError(
                        NANO_REQUEST_ERROR_HANDLER_BAD_REQUEST, "Listing failed", self, maybe_conflict=True
                    ) from e
                self.ls = self._approximate_part_slicing(ls, ls_admin_entry_size, 16)
            req = False, [self.HANDLER_KEY, 0, "", ""], len(self.ls)
            return self._secure_request_key(*req) if secure else req
        # The path is specified, try to list it.
        try:
            # NOTE: Sensitive restriction is not relevant for listing paths, since we need to traverse through
            # sensitive places to get to desired ones, e.g. folder under the user's home.
            path = Path(normalize_validate_absolute_path(self.payload[AdminGetListingRequest.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)
            s = await asyncio.get_event_loop().run_in_executor(self.helper.pool, file_stat, path)
        except (OSError, ValidationError) as e:
            raise NanoHandlerError(
                NANO_REQUEST_ERROR_HANDLER_BAD_REQUEST,
                "Unresolved path" if isinstance(e, OSError) else "Invalid path",
                self,
                maybe_conflict=True,
            )
        if self.path is None:  # pre-execution
            self.path = path
            if stat.S_ISDIR(s.st_mode):
                try:
                    ls = await asyncio.get_event_loop().run_in_executor(
                        self.helper.pool, list_admin_directory, self.path
                    )
                except OSError as e:  # pragma: no cover; cannot simulate this
                    raise NanoHandlerError(
                        NANO_REQUEST_ERROR_HANDLER_BAD_REQUEST, "Listing failed", self, maybe_conflict=True
                    ) from e
                self.ls = self._approximate_part_slicing(ls, ls_admin_entry_size, 16)
            else:
                raise NanoHandlerError(NANO_REQUEST_ERROR_HANDLER_BAD_REQUEST, "Not a directory", self)
        path_parent, _, path_name = path.as_posix().rpartition("/")
        req = False, [self.HANDLER_KEY, s.st_ino, path_name, path_parent], len(self.ls)
        return self._secure_request_key(*req) if secure else req

    async def _execute_initial(self) -> dict:
        d = await AbstractGeneralNanoServerHandler._execute_initial(self)
        d[AdminGetListingResponse.LISTING] = self.ls[0]
        if self.payload[AdminGetListingRequest.PATH]:  # not roots
            add_disk_usage(self.path, d)  # adds "total" and "free" on success
        return d

    async def _execute_part(self, part: int, s_hmac_start_part: Optional[int]) -> dict:
        d = await AbstractGeneralNanoServerHandler._execute_part(self, part, s_hmac_start_part)
        # NOTE: we cannot s_hmac the listing because it is volatile... no real benefit for p2p encryption anyways
        d[AdminGetListingResponse.LISTING] = self.ls[part]
        return d

    def _log_message(self) -> str:
        if part := self.context.get(NanoRequestContext.PART):
            return f"Admin list {log_escape_str(self.path)} part {part}"
        return f"Admin list {log_escape_str(self.path)}"

    def build_error_message(self, buf: StringIO) -> None:
        buf.write("Admin list ")
        buf.write(log_escape_str(self.payload.get(AdminGetListingRequest.PATH)))
