from .. import query
from ....cio.core.commands import Open, OpenMany, Upload, Download, EnsureDirectory, \
    DownloadMany, UnShare, CreateDirectory, GetMetadata, GetProperties, ListVersions, RecursiveIterator, \
    Delete, Recover, Rename, GetShareMetadata, Link, Copy, Move, ResourceIterator, GetPermalink
from ..base_command import BaseCommand
from ....lib.storage import commonfs
from . import io


class FileBrowser(BaseCommand):
    """Async File Browser API."""

    async def handle(self, path):
        """
        Get a file handle.

        :param str path: Path to a file.
        :returns: File handle.
        :rtype: object
        :raises cterasdk.exceptions.io.core.OpenError: Raised on error to obtain file handle.
        """
        return await Open(io.handle, self._core, path).a_execute()

    async def handle_many(self, directory, *objects):
        """
        Get a ZIP archive file handle.

        :param str directory: Path to a folder.
        :param args objects: List of files and folders to include.
        :returns: File handle.
        :rtype: object
        :raises cterasdk.exceptions.io.core.GetMetadataError: If directory not found.
        :raises cterasdk.exceptions.io.core.NotADirectoryException: If target path is not a directory.
        """
        async with EnsureDirectory(io.listdir, self._core, directory) as (_, resource):
            return await OpenMany(io.handle_many, self._core, resource, directory, *objects).a_execute()

    async def download(self, path, destination=None):
        """
        Download a file.

        :param str path: Path.
        :param str, optional destination: File destination. If directory, original filename preserved. Defaults to default directory.
        :returns: Path to local file.
        :rtype: str
        :raises cterasdk.exceptions.io.core.OpenError: Raised on error to obtain file handle.
        """
        return await Download(io.handle, self._core, path, destination).a_execute()

    async def download_many(self, directory, objects, destination=None):
        """
        Download selected files and/or directories as a ZIP archive.

        .. warning::
            Only existing files and directories will be included in the resulting ZIP file.

        :param str directory: Path to a folder.
        :param list[str] objects: List of files and / or directory names to download.
        :param str destination: Optional path to destination file or directory. Defaults to default download directory.
        :returns: Path to local file.
        :rtype: str
        :raises cterasdk.exceptions.io.core.GetMetadataError: If directory not found.
        :raises cterasdk.exceptions.io.core.NotADirectoryException: If target path is not a directory.
        """
        async with EnsureDirectory(io.listdir, self._core, directory) as (_, resource):
            return await DownloadMany(io.handle_many, self._core, resource, directory, objects, destination).a_execute()

    async def listdir(self, path=None, include_deleted=False):
        """
        List directory contents.

        :param str, optional path: Path, defaults to Cloud Drive root.
        :param bool, optional include_deleted: Include deleted files. Defaults to False.
        :returns: Directory contents.
        :rtype: AsyncIterator[cterasdk.cio.core.types.PortalResource]
        :raises cterasdk.exceptions.io.core.GetMetadataError: If directory not found.
        :raises cterasdk.exceptions.io.core.NotADirectoryException: If target path is not a directory.
        :raises cterasdk.exceptions.io.core.ListDirectoryError: Raised on error fetching directory contents.
        """
        async with EnsureDirectory(io.listdir, self._core, path):
            async for o in ResourceIterator(query.iterator, self._core, path, None, include_deleted, None, None).a_execute():
                yield o

    async def properties(self, path):
        """
        Get object properties.

        :param str path: Path.
        :returns: Object properties.
        :rtype: cterasdk.cio.core.types.PortalResource
        :raises cterasdk.exceptions.io.core.GetMetadataError: Raised on error retrieving object metadata.
        """
        return await GetProperties(io.listdir, self._core, path, False).a_execute()

    async def exists(self, path):
        """
        Check if item exists.

        :param str path: Path.
        :returns: True if exists, False otherwise.
        :rtype: bool
        """
        async with GetMetadata(io.listdir, self._core, path, True) as (exists, *_):
            return exists

    async def versions(self, path):
        """
        List snapshots of a file or directory.

        :param str path: Path.
        :returns: List of versions.
        :rtype: list[cterasdk.cio.core.types.PreviousVersion]
        :raises cterasdk.exceptions.io.core.GetVersionsError: Raised on error retrieving versions.
        """
        return await ListVersions(io.versions, self._core, path).a_execute()

    async def walk(self, path=None, include_deleted=False):
        """
        Walk directory contents.

        :param str, optional path: Path to walk, defaults to root directory.
        :param bool, optional include_deleted: Include deleted files. Defaults to False.
        :returns: Async generator of file-system objects.
        :rtype: AsyncIterator[cterasdk.cio.edge.types.PortalResource]
        :raises cterasdk.exceptions.io.core.GetMetadataError: If directory not found.
        :raises cterasdk.exceptions.io.core.NotADirectoryException: If target path is not a directory.
        """
        async with EnsureDirectory(io.listdir, self._core, path):
            async for o in RecursiveIterator(query.iterator, self._core, path, include_deleted).a_generate():
                yield o

    async def public_link(self, path, access='RO', expire_in=30):
        """
        Create a public link to a file or folder.

        :param str path: Path of file/folder.
        :param str, optional access: Access policy. Defaults 'RO'.
        :param int, optional expire_in: Days until link expires. Defaults 30.
        :returns: Public link.
        :rtype: str
        :raises cterasdk.exceptions.io.core.CreateLinkError: Raised on failure generating public link.
        """
        return await Link(io.public_link, self._core, path, access, expire_in).a_execute()

    async def copy(self, *paths, destination=None, resolver=None, cursor=None, wait=False):
        """
        Copy one or more files or folders.

        :param list[str] paths: Paths to copy.
        :param str destination: Destination path.
        :param cterasdk.core.types.ConflictResolver, optional resolver: Conflict resolver. Defaults None.
        :param cterasdk.common.object.Object, optional cursor: Resume copy from cursor.
        :param bool, optional wait: Wait for task completion. Defaults False.
        :returns: Task status object or awaitable task.
        :rtype: cterasdk.common.object.Object or :class:`cterasdk.lib.tasks.AwaitablePortalTask`
        :raises cterasdk.exceptions.io.core.CopyError: Raised on failure copying resources.
        """
        try:
            return await Copy(io.copy, self._core, wait, *paths, destination=destination, resolver=resolver, cursor=cursor).a_execute()
        except ValueError:
            raise ValueError('Copy destination was not specified.')

    async def permalink(self, path):
        """
        Get permalink for a path.

        :param str path: Path.
        :returns: Permalink.
        :rtype: str
        :raises cterasdk.exceptions.io.core.GetMetadataError: Raised on error retrieving object metadata.
        """
        return await GetPermalink(io.listdir, self._core, path).a_execute()


class CloudDrive(FileBrowser):
    """Async CloudDrive API with upload and sharing functionality."""

    async def upload(self, destination, handle, name=None, size=None):
        """
        Upload from file handle.

        :param str destination: Remote path.
        :param object handle: File-like handle.
        :param str, optional size: File size. Defaults to content length.
        :param str, optional name: Filename to use if it cannot be derived from ``destination``
        :returns: Remote file path.
        :rtype: str
        :raises cterasdk.exceptions.io.core.UploadError: Raised on upload failure.
        """
        return await Upload(io.upload, self._core, io.listdir, destination, handle, name, size).a_execute()

    async def upload_file(self, path, destination):
        """
        Upload a file.

        :param str path: Local path.
        :param str destination: Remote path.
        :returns: Remote file path.
        :rtype: str
        :raises cterasdk.exceptions.io.core.UploadError: Raised on upload failure.
        """
        _, name = commonfs.split_file_directory(path)
        with open(path, 'rb') as handle:
            return await self.upload(destination, handle, name, commonfs.properties(path)['size'])

    async def mkdir(self, path):
        """
        Create a directory.

        :param str path: Directory path.
        :returns: Remote directory path.
        :rtype: str
        :raises cterasdk.exceptions.io.core.CreateDirectoryError: Raised on error creating directory.
        """
        return await CreateDirectory(io.mkdir, self._core, path).a_execute()

    async def makedirs(self, path):
        """
        Recursively create a directory.

        :param str path: Directory path.
        :returns: Remote directory path.
        :rtype: str
        :raises cterasdk.exceptions.io.core.CreateDirectoryError: Raised on error creating directory.
        """
        return await CreateDirectory(io.mkdir, self._core, path, True).a_execute()

    async def rename(self, path, name, *, resolver=None, wait=False):
        """
        Rename a file or folder.

        :param str path: Path to rename.
        :param str name: New name.
        :param cterasdk.core.types.ConflictResolver, optional resolver: Conflict resolver. Defaults None.
        :param bool, optional wait: Wait for task completion. Defaults False.
        :returns: Task status object or awaitable task.
        :rtype: cterasdk.common.object.Object or :class:`cterasdk.lib.tasks.AwaitablePortalTask`
        :raises cterasdk.exceptions.io.core.RenameError: Raised on error renaming object.
        """
        return await Rename(io.move, self._core, wait, path, name, resolver).a_execute()

    async def delete(self, *paths, wait=False):
        """
        Delete one or more files or folders.

        :param list[str] paths: Paths to delete.
        :param bool, optional wait: Wait for task completion. Defaults False.
        :returns: Task status object or awaitable task.
        :rtype: cterasdk.common.object.Object or :class:`cterasdk.lib.tasks.AwaitablePortalTask`
        :raises cterasdk.exceptions.io.core.DeleteError: Raised on error deleting resources.
        """
        return await Delete(io.delete, self._core, wait, *paths).a_execute()

    async def undelete(self, *paths, wait=False):
        """
        Recover one or more files or folders.

        :param list[str] paths: Paths to recover.
        :param bool, optional wait: Wait for task completion. Defaults False.
        :returns: Task status object or awaitable task.
        :rtype: cterasdk.common.object.Object or :class:`cterasdk.lib.tasks.AwaitablePortalTask`
        :raises cterasdk.exceptions.io.core.RecoverError: Raised on error recovering resources.
        """
        return await Recover(io.undelete, self._core, wait, *paths).a_execute()

    async def move(self, *paths, destination=None, resolver=None, cursor=None, wait=False):
        """
        Move one or more files or folders.

        :param list[str] paths: Paths to move.
        :param str destination: Destination path.
        :param cterasdk.core.types.ConflictResolver, optional resolver: Conflict resolver. Defaults None.
        :param cterasdk.common.object.Object, optional cursor: Resume move from cursor.
        :param bool, optional wait: Wait for task completion. Defaults False.
        :returns: Task status object or awaitable task.
        :rtype: cterasdk.common.object.Object or :class:`cterasdk.lib.tasks.AwaitablePortalTask`
        :raises cterasdk.exceptions.io.core.MoveError: Raised on error moving resources.
        """
        try:
            return await Move(io.move, self._core, wait, *paths, destination=destination, resolver=resolver, cursor=cursor).a_execute()
        except ValueError:
            raise ValueError('Move destination was not specified.')

    async def get_share_info(self, path):
        """
        Get share settings and recipients.

        :param str path: Path.
        :returns: Share metadata.
        :rtype: object
        :raises cterasdk.exceptions.io.core.GetShareMetadataError: Raised on error obtaining share metadata.
        """
        return await GetShareMetadata(io.list_shares, self._core, path).a_execute()

    async def share(self, path, recipients, as_project=True, allow_reshare=True, allow_sync=True):
        """
        Share a file or folder.

        :param str path: Path to file/folder.
        :param list[cterasdk.core.types.Collaborator] recipients: Recipients to share with.
        :param bool, optional as_project: Share as team project. Defaults True if cloud folder.
        :param bool, optional allow_reshare: Allow re-share. Defaults True.
        :param bool, optional allow_sync: Allow sync. Defaults True if cloud folder.
        :returns: Current list of share members.
        :rtype: list[cterasdk.core.types.Collaborator]
        """
        return await io.share(self._core, path, recipients, as_project, allow_reshare, allow_sync)

    async def add_share_recipients(self, path, recipients):
        """
        Add share recipients.

        :param str path: Path of file/folder.
        :param list[cterasdk.core.types.Collaborator] recipients: Recipients to add.
        :returns: Current list of share members.
        :rtype: list[cterasdk.core.types.Collaborator]
        """
        return await io.add_share_recipients(self._core, path, recipients)

    async def remove_share_recipients(self, path, accounts):
        """
        Remove share recipients.

        :param str path: Path of file/folder.
        :param list[cterasdk.core.types.PortalAccount] accounts: Accounts to remove.
        :returns: Current list of share members.
        :rtype: list[cterasdk.core.types.Collaborator]
        """
        return await io.remove_share_recipients(self._core, path, accounts)

    async def unshare(self, path):
        """
        Unshare a file or folder.

        :param str path: Path of file/folder.
        """
        return await UnShare(io.update_share, self._core, path).a_execute()
