"""
Helper definitions to create cross-platform error handling when it comes to filesystems.
"""
import asyncio
import sys
from contextlib import contextmanager, nullcontext


class BroadNotFoundError(OSError):
    """
    This not found error should be used when the specific resource is not found, but for a broader reason. It
    indicates that the error why the file is not found is not specific to the file.
    For example: network error, device is busy or overwhelmed.
    """


class SharedLockError(PermissionError):
    """
    The operation is denied by an other process. Either permanently... or often temporarily by a security
    software scanning the file that has just been modified for example.
    """


# Make a platform independent and always-available alias for the builtin WindowsError class. By having this we
# can integrate platform specific (Windows) error handling into all codes nicely. We do not re-use or shadow the
# name of the builtin so everything will be explicit and clear.
NtOSError = __builtins__.get("WindowsError")
if NtOSError is None:

    class NtOSError(OSError):
        winerror: int  # to help code completion


class NtOSErrorCodes(object):
    """
    Most of these error codes have been picked from the builtin ntpath module.
    These are mostly relevant for determining if the path in question could be accessed at all.
    """

    # "Access is denied."
    # This is raised when a directory is attempted to be unlinked, which is confusing. The class of the exception is
    # PermissionError which is fine, but there's no way to tell if it should be an IsADirectoryError without
    # additional IO operation.
    ERROR_ACCESS_DENIED = 5
    # "The device is not ready."
    # This implies the device has no media inserted in it.
    ERROR_NOT_READY = 21
    # "The process cannot access the file because it is being used by another process."
    # Either a system file or some exclusively locked file.
    # This is what happens if a security system locks the file for inspection. We need to treat it with patience.
    ERROR_SHARING_VIOLATION = 32
    # "The request is not supported."
    # In case of symlinks this means the FS does not have support. (FAT for example)
    ERROR_NOT_SUPPORTED = 50
    # "The network name cannot be found."
    # Remote server is not reachable. Name resolution or routing error maybe?
    ERROR_BAD_NET_NAME = 67

    # "The filename, directory name, or volume label syntax is incorrect."
    # This is also raised if the path contains broken symlinks, but it is still valid.
    ERROR_INVALID_NAME = 123

    # "The file cannot be accessed by the system."
    # TODO: is this for some arbitrary policy or technical problem?
    ERROR_CANT_ACCESS_FILE = 1920
    # "The name of the file cannot be resolved by the system."
    # Broken symlink most likely.
    ERROR_CANT_RESOLVE_FILENAME = 1921

    # # "The file or directory is not a reparse point."
    # #
    # ERROR_NOT_A_REPARSE_POINT = 4390
    # "The data present in the reparse point buffer is invalid."
    # Invalid symlink.
    ERROR_INVALID_REPARSE_DATA = 4392
    # "The tag present in the reparse point buffer is invalid."
    # Invalid symlink.
    ERROR_REPARSE_TAG_INVALID = 4393

    # Many FileNotFoundErrors could be NotADirectoryError alternatively, but there's no way to discern the correct
    # one. For now we raise the more generic one.
    # This map is created with the indexer's needs in mind:
    #   - FileNotFoundError, PermissionError -> consider path inaccessible explicitly and immediately
    #   - BroadNotFoundError -> consider path inaccessible implicitly (its lost, use delayed removal)
    COERCE_MAP = {
        ERROR_NOT_READY: FileNotFoundError,  # media is not inserted or removed
        ERROR_SHARING_VIOLATION: SharedLockError,  # unknown if permanent or temporary
        ERROR_NOT_SUPPORTED: FileNotFoundError,  # treat unsupported FS item-type or operation as non-existent
        ERROR_BAD_NET_NAME: BroadNotFoundError,
        ERROR_INVALID_NAME: FileNotFoundError,
        ERROR_CANT_ACCESS_FILE: FileNotFoundError,
        ERROR_CANT_RESOLVE_FILENAME: FileNotFoundError,
        ERROR_INVALID_REPARSE_DATA: FileNotFoundError,
        ERROR_REPARSE_TAG_INVALID: FileNotFoundError,
    }


if sys.platform.startswith("win"):

    @contextmanager
    def coerce_platform_os_error():
        try:
            yield
        except WindowsError as e:
            e_cls = NtOSErrorCodes.COERCE_MAP.get(e.winerror)
            if e_cls:  # FIXME: improve instance creation call with arguments
                raise e_cls() from e
            raise e

    async def attempt_share_locked_operation(max_retries: int, delay: float, coroutine, *args, **kwargs):
        # Some operations are very prone to be blocked by security systems temporarily. We expect the
        # specific errors to resolve by themselves eventually, so we'll keep trying a little.
        retry = 0
        while True:
            try:
                return await coroutine(*args, **kwargs)
            except NtOSError as e:
                if e.winerror == NtOSErrorCodes.ERROR_SHARING_VIOLATION:
                    if retry >= max_retries:
                        raise SharedLockError() from e  # raise the coerced error for more control
                    retry += 1
                    await asyncio.sleep(delay)
                else:
                    raise e


else:  # TODO: some other platforms may require specific coercion

    coerce_platform_os_error = nullcontext

    async def attempt_share_locked_operation(max_retries: int, delay: float, coroutine, *args, **kwargs):
        return await coroutine(*args, **kwargs)
