from girder.api import access
from girder.constants import AccessType, TokenScope
from girder.exceptions import RestException
from girder.utility import parseTimestamp
from girder.utility import path as path_util
from girder.utility import ziputil
from girder.utility.model_importer import ModelImporter
from girder.utility.progress import ProgressContext
from girder.utility.search import getSearchModeHandler

from ..describe import Description, autoDescribeRoute
from ..rest import Resource as BaseResource
from ..rest import setContentDisposition, setResponseHeader

# Plugins can modify this set to allow other types to be searched
allowedSearchTypes = {'collection', 'file', 'folder', 'group', 'item', 'user'}
allowedDeleteTypes = {'collection', 'file', 'folder', 'group', 'item', 'user'}


class Resource(BaseResource):
    """
    API Endpoints that deal with operations across multiple resource types.
    """

    def __init__(self):
        super().__init__()
        self.resourceName = 'resource'
        self.route('GET', ('search',), self.search)
        self.route('GET', ('lookup',), self.lookup)
        self.route('GET', (':id',), self.getResource)
        self.route('GET', (':id', 'path'), self.path)
        self.route('PUT', (':id', 'timestamp'), self.setTimestamp)
        self.route('GET', ('download',), self.download)
        self.route('POST', ('download',), self.download)
        self.route('PUT', ('move',), self.moveResources)
        self.route('POST', ('copy',), self.copyResources)
        self.route('DELETE', (), self.delete)

    @access.public
    @autoDescribeRoute(
        Description('Search for resources in the system.')
        .param('q', 'The search query.')
        .param('mode', 'The search mode. Can always use either a text search or a '
               'prefix-based search.', required=False, default='text')
        .jsonParam('types', 'A JSON list of resource types to search for, e.g. '
                   '["user", "folder", "item"].', requireArray=True)
        .param('level', 'Minimum required access level.', required=False,
               dataType='integer', default=AccessType.READ)
        .pagingParams(defaultSort=None, defaultLimit=10)
        .errorResponse('Invalid type list format.')
    )
    def search(self, q, mode, types, level, limit, offset):
        """
        Perform a search using one of the registered search modes.
        """
        level = AccessType.validate(level)
        user = self.getCurrentUser()
        handler = getSearchModeHandler(mode)
        if handler is None:
            raise RestException('Search mode handler %r not found.' % mode)
        results = handler(
            query=q,
            types=types,
            user=user,
            limit=limit,
            offset=offset,
            level=level
        )
        return results

    def _validateResourceSet(self, resources, allowedModels=None):
        """
        Validate a set of resources against a set of allowed models.
        Also ensures the requested resource set is not empty.
        # TODO jsonschema could replace this probably

        :param resources: The set of resources requested.
        :param allowedModels: if present, an iterable of models that may be
            included in the resources.
        """
        if allowedModels:
            invalid = set(resources.keys()) - set(allowedModels)
            if invalid:
                raise RestException('Invalid resource types requested: ' + ', '.join(invalid))
        count = sum([len(v) for v in resources.values()])
        if not count:
            raise RestException('No resources specified.')

    def _getResourceModel(self, kind, funcName=None):
        """
        Load and return a model with a specific function or throw an exception.

        :param kind: the name of the model to load
        :param funcName: a function name to ensure that each model contains.
        :returns: the loaded model.
        """
        try:
            model = ModelImporter.model(kind)
        except Exception:
            model = None
        if not model or (funcName and not hasattr(model, funcName)):
            raise RestException('Invalid resources format.')
        return model

    @access.public(scope=TokenScope.DATA_READ)
    @autoDescribeRoute(
        Description('Look up a resource in the data hierarchy by path.')
        .param('path',
               'The path of the resource.  The path must be an absolute Unix '
               'path starting with either "/user/[user name]", for a user\'s '
               'resources or "/collection/[collection name]", for resources '
               'under a collection.')
        .errorResponse('Path is invalid.')
        .errorResponse('Path refers to a resource that does not exist.')
        .errorResponse('Read access was denied for the resource.', 403)
    )
    def lookup(self, path):
        return path_util.lookUpPath(path, self.getCurrentUser())['document']

    @access.public(scope=TokenScope.DATA_READ)
    @autoDescribeRoute(
        Description('Get path of a resource.')
        .param('id', 'The ID of the resource.', paramType='path')
        .param('type', 'The type of the resource (item, file, etc.).')
        .errorResponse('ID was invalid.')
        .errorResponse('Invalid resource type.')
        .errorResponse('Read access was denied for the resource.', 403)
    )
    def path(self, id, type):
        user = self.getCurrentUser()
        doc = self._getResource(id, type)
        if doc is None:
            raise RestException('Invalid resource id.')
        return path_util.getResourcePath(type, doc, user=user)

    @access.public(scope=TokenScope.DATA_READ, cookie=True)
    @autoDescribeRoute(
        Description('Download a set of items, folders, collections, and users '
                    'as a zip archive.')
        .notes('This route is also exposed via the POST method because the '
               'request parameters can be quite long, and encoding them in the '
               'URL (as is standard when using the GET method) can cause the '
               'URL to become too long, which causes errors.')
        .jsonParam('resources', 'A JSON-encoded set of resources to download. Each type is '
                   'a list of ids. For example: {"item": [(item id 1), (item id 2)], '
                   '"folder": [(folder id 1)]}.', requireObject=True)
        .param('includeMetadata', 'Include any metadata in JSON files in the '
               'archive.', required=False, dataType='boolean', default=False)
        .produces('application/zip')
        .errorResponse('Unsupported or unknown resource type.')
        .errorResponse('Invalid resources format.')
        .errorResponse('No resources specified.')
        .errorResponse('Resource not found.')
        .errorResponse('Read access was denied for a resource.', 403)
    )
    def download(self, resources, includeMetadata):
        """
        Returns a generator function that will be used to stream out a zip
        file containing the listed resource's contents, filtered by
        permissions.
        """
        user = self.getCurrentUser()
        self._validateResourceSet(resources)
        # Check that all the resources are valid, so we don't download the zip
        # file if it would throw an error.
        for kind in resources:
            model = self._getResourceModel(kind, 'fileList')
            for id in resources[kind]:
                if not model.load(id=id, user=user, level=AccessType.READ):
                    raise RestException('Resource %s %s not found.' % (kind, id))
        setResponseHeader('Content-Type', 'application/zip')
        setContentDisposition('Resources.zip')

        def stream():
            zip = ziputil.ZipGenerator()
            for kind in resources:
                model = ModelImporter.model(kind)
                for id in resources[kind]:
                    doc = model.load(id=id, user=user, level=AccessType.READ)
                    for (path, file) in model.fileList(
                            doc=doc, user=user, includeMetadata=includeMetadata, subpath=True):
                        yield from zip.addFile(file, path)
            yield zip.footer()
        return stream

    @access.user(scope=TokenScope.DATA_OWN)
    @autoDescribeRoute(
        Description('Delete a set of items, folders, or other resources.')
        .jsonParam('resources', 'A JSON-encoded set of resources to delete. Each '
                   'type is a list of ids.  For example: {"item": [(item id 1), '
                   '(item id2)], "folder": [(folder id 1)]}.', requireObject=True)
        .param('progress', 'Whether to record progress on this task.',
               default=False, required=False, dataType='boolean')
        .errorResponse('Unsupported or unknown resource type.')
        .errorResponse('Invalid resources format.')
        .errorResponse('No resources specified.')
        .errorResponse('Resource not found.')
        .errorResponse('Admin access was denied for a resource.', 403)
    )
    def delete(self, resources, progress):
        user = self.getCurrentUser()
        self._validateResourceSet(resources, allowedDeleteTypes)
        total = sum([len(resources[key]) for key in resources])
        with ProgressContext(
                progress, user=user, title='Deleting resources',
                message='Calculating size...') as ctx:
            ctx.update(total=total)
            current = 0
            for kind in resources:
                model = self._getResourceModel(kind, 'remove')
                for id in resources[kind]:
                    doc = model.load(id=id, user=user, level=AccessType.ADMIN, exc=True)

                    # Don't do a subtree count if we weren't asked for progress
                    if progress:
                        subtotal = model.subtreeCount(doc)
                        if subtotal != 1:
                            total += subtotal - 1
                            ctx.update(total=total)
                    model.remove(doc, progress=ctx)
                    if progress:
                        current += subtotal
                        if ctx.progress._payload['data']['current'] != current:
                            ctx.update(current=current, message='Deleted ' + kind)

    def _getResource(self, id, type):
        model = self._getResourceModel(type)
        return model.load(id=id, user=self.getCurrentUser(), level=AccessType.READ)

    @access.admin
    @autoDescribeRoute(
        Description('Get any resource by ID.')
        .param('id', 'The ID of the resource.', paramType='path')
        .param('type', 'The type of the resource (item, file, etc.).')
        .errorResponse('ID was invalid.')
        .errorResponse('Read access was denied for the resource.', 403)
    )
    def getResource(self, id, type):
        return self._getResource(id, type)

    @access.admin
    @autoDescribeRoute(
        Description('Set the created or updated timestamp for a resource.')
        .param('id', 'The ID of the resource.', paramType='path')
        .param('type', 'The type of the resource (item, file, etc.).')
        .param('created', 'The new created timestamp.', required=False)
        .param('updated', 'The new updated timestamp.', required=False)
        .errorResponse('ID was invalid.')
        .errorResponse('Access was denied for the resource.', 403)
    )
    def setTimestamp(self, id, type, created, updated):
        user = self.getCurrentUser()
        model = self._getResourceModel(type)
        doc = model.load(id=id, user=user, level=AccessType.WRITE, exc=True)

        if created is not None:
            if 'created' not in doc:
                raise RestException('Resource has no "created" field.')
            doc['created'] = parseTimestamp(created)
        if updated is not None:
            if 'updated' not in doc:
                raise RestException('Resource has no "updated" field.')
            doc['updated'] = parseTimestamp(updated)
        return model.filter(model.save(doc), user=user)

    def _prepareMoveOrCopy(self, resources, parentType, parentId):
        user = self.getCurrentUser()
        self._validateResourceSet(resources, ('folder', 'item'))

        if resources.get('item') and parentType != 'folder':
            raise RestException('Invalid parentType.')
        return ModelImporter.model(parentType).load(
            parentId, level=AccessType.WRITE, user=user, exc=True)

    @access.user(scope=TokenScope.DATA_WRITE)
    @autoDescribeRoute(
        Description('Move a set of items and folders.')
        .jsonParam('resources', 'A JSON-encoded set of resources to move. Each type '
                   'is a list of ids.  Only folders and items may be specified.  '
                   'For example: {"item": [(item id 1), (item id2)], "folder": '
                   '[(folder id 1)]}.', requireObject=True)
        .param('parentType', 'Parent type for the new parent of these resources.',
               enum=('user', 'collection', 'folder'))
        .param('parentId', 'Parent ID for the new parent of these resources.')
        .param('progress', 'Whether to record progress on this task.',
               required=False, default=False, dataType='boolean')
        .errorResponse('Unsupported or unknown resource type.')
        .errorResponse('Invalid resources format.')
        .errorResponse('Resource type not supported.')
        .errorResponse('No resources specified.')
        .errorResponse('Resource not found.')
        .errorResponse('ID was invalid.')
    )
    def moveResources(self, resources, parentType, parentId, progress):
        user = self.getCurrentUser()
        parent = self._prepareMoveOrCopy(resources, parentType, parentId)
        total = sum([len(resources[key]) for key in resources])
        with ProgressContext(
                progress, user=user, title='Moving resources',
                message='Calculating requirements...', total=total) as ctx:
            for kind in resources:
                model = self._getResourceModel(kind, 'move')
                for id in resources[kind]:
                    doc = model.load(id=id, user=user, level=AccessType.WRITE, exc=True)
                    ctx.update(message='Moving %s %s' % (kind, doc.get('name', '')))
                    if kind == 'item':
                        if parent['_id'] != doc['folderId']:
                            model.move(doc, parent)
                    elif kind == 'folder':
                        if ((parentType, parent['_id'])
                                != (doc['parentCollection'], doc['parentId'])):
                            model.move(doc, parent, parentType)
                    ctx.update(increment=1)

    @access.user(scope=TokenScope.DATA_WRITE)
    @autoDescribeRoute(
        Description('Copy a set of items and folders.')
        .jsonParam('resources', 'A JSON-encoded set of resources to copy. Each type '
                   'is a list of ids.  Only folders and items may be specified.  '
                   'For example: {"item": [(item id 1), (item id2)], "folder": '
                   '[(folder id 1)]}.', requireObject=True)
        .param('parentType', 'Parent type for the new parent of these '
               'resources.')
        .param('parentId', 'Parent ID for the new parent of these resources.')
        .param('progress', 'Whether to record progress on this task.',
               required=False, default=False, dataType='boolean')
        .errorResponse('Unsupported or unknown resource type.')
        .errorResponse('Invalid resources format.')
        .errorResponse('Resource type not supported.')
        .errorResponse('No resources specified.')
        .errorResponse('Resource not found.')
        .errorResponse('ID was invalid.')
    )
    def copyResources(self, resources, parentType, parentId, progress):
        user = self.getCurrentUser()
        parent = self._prepareMoveOrCopy(resources, parentType, parentId)
        total = len(resources.get('item', []))
        if 'folder' in resources:
            model = self._getResourceModel('folder')
            for id in resources['folder']:
                folder = model.load(id=id, user=user, level=AccessType.READ, exc=True)
                total += model.subtreeCount(folder)
        with ProgressContext(
                progress, user=user, title='Copying resources',
                message='Calculating requirements...', total=total) as ctx:
            for kind in resources:
                model = self._getResourceModel(kind)
                for id in resources[kind]:
                    doc = model.load(id=id, user=user, level=AccessType.READ, exc=True)
                    ctx.update(message='Copying %s %s' % (kind, doc.get('name', '')))
                    if kind == 'item':
                        model.copyItem(doc, folder=parent, creator=user)
                        ctx.update(increment=1)
                    elif kind == 'folder':
                        model.copyFolder(
                            doc, parent=parent, parentType=parentType, creator=user, progress=ctx)
