#!/usr/bin/env python
# -*- coding: utf-8 -*-

###############################################################################
#  Copyright 2015 Kitware Inc.
#
#  Licensed under the Apache License, Version 2.0 ( the "License" );
#  you may not use this file except in compliance with the License.
#  You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
#  Unless required by applicable law or agreed to in writing, software
#  distributed under the License is distributed on an "AS IS" BASIS,
#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#  See the License for the specific language governing permissions and
#  limitations under the License.
###############################################################################

import json
import six

from bson.objectid import ObjectId, InvalidId
from girder import logger
from girder.api import access
from girder.api.describe import Description, describeRoute
from girder.api.rest import Resource, RestException, loadmodel
from girder.constants import AccessType
from girder.models.model_base import GirderException
from girder.utility import assetstore_utilities
from girder.utility.system import formatSize
from . import constants


QUOTA_FIELD = 'quota'


def ValidateSizeQuota(value):
    """
    Validate a quota value.  This may be blank or a non-negative integer.

    :param value: The proposed value.
    :type value: int
    :returns: The validated value or None,
              and a recommended error message or None.
    :rtype: (int or None, str or None)
    """
    if value is None or value == '' or value == 0:
        return None, None
    error = False
    try:
        value = int(value)
        if value < 0:
            error = True
    except ValueError:
        error = True
    if error:
        return (value, 'Invalid quota.  Must be blank or a positive integer '
                       'representing the limit in bytes.')
    if value == 0:
        return None, None
    return value, None


class QuotaPolicy(Resource):
    def _filter(self, model, resource):
        """
        Filter a resource to include only the ordinary data and the quota
        field.

        :param model: the type of resource (e.g., user or collection)
        :param resource: the resource document.
        :returns: filtered field of the resource with the quota data, if any.
        """
        filtered = self.model(model).filter(resource, self.getCurrentUser())
        filtered[QUOTA_FIELD] = resource.get(QUOTA_FIELD, {})
        return filtered

    def _setResourceQuota(self, model, resource, params):
        """
        Handle setting quota policies for any resource that supports them.

        :param model: the type of resource (e.g., user or collection)
        :param resource: the resource document.
        :param params: the query parameters.  'policy' is required and used.
        :returns: the updated resource document.
        """
        self.requireParams(('policy', ), params)
        policy = self._validatePolicy(params['policy'])
        if QUOTA_FIELD not in resource:
            resource[QUOTA_FIELD] = {}
        resource[QUOTA_FIELD].update(policy)
        self.model(model).save(resource, validate=False)
        return self._filter(model, resource)

    def _validate_fallbackAssetstore(self, value):
        """Validate the fallbackAssetstore parameter.

        :param value: the proposed value.
        :returns: the validated value: either None or 'current' to use the
                  current assetstore, 'none' to disable a fallback assetstore,
                  or an assetstore ID.
        """
        if not value or value == 'current':
            return None
        if value == 'none':
            return value
        try:
            value = ObjectId(value)
        except InvalidId:
            raise RestException(
                'Invalid fallbackAssetstore.  Must either be an assetstore '
                'ID, be blank or "current" to use the current assetstore, or '
                'be "none" to disable fallback usage.',
                extra='fallbackAssetstore')
        return value

    def _validate_fileSizeQuota(self, value):
        """Validate the fileSizeQuota parameter.

        :param value: the proposed value.
        :returns: the validated value
        :rtype: None or int
        """
        (value, err) = ValidateSizeQuota(value)
        if err:
            raise RestException(err, extra='fileSizeQuota')
        return value

    def _validate_preferredAssetstore(self, value):
        """Validate the preferredAssetstore parameter.

        :param value: the proposed value.
        :returns: the validated value: either None or 'current' to use the
                  current assetstore or an assetstore ID.
        """
        if not value or value == 'current':
            return None
        try:
            value = ObjectId(value)
        except InvalidId:
            raise RestException(
                'Invalid preferredAssetstore.  Must either be an assetstore '
                'ID, or be blank or "current" to use the current assetstore.',
                extra='preferredAssetstore')
        return value

    def _validate_useQuotaDefault(self, value):
        """Validate the useQuotaDefault parameter.

        :param value: the proposed value.
        :returns: the validated value
        :rtype: None or bool
        """
        if str(value).lower() in ('none', 'true', 'yes', '1'):
            return True
        if str(value).lower() in ('false', 'no', '0'):
            return False
        raise RestException(
            'Invalid useQuotaDefault.  Must either be true or false.',
            extra='useQuotaDefault')

    def _validatePolicy(self, policy):
        """
        Validate a policy JSON object.  Only a limited set of keys is
        supported, and each of them has a restricted data type.

        :param policy: JSON object to validate.  This may also be a Python
                           dictionary as if the JSON was already decoded.
        :returns: a validate policy dictionary.
        """
        if not isinstance(policy, dict):
            try:
                policy = json.loads(policy)
            except ValueError:
                raise RestException('The policy parameter must be JSON.')
        if not isinstance(policy, dict):
            raise RestException('The policy parameter must be a dictionary.')
        validKeys = []
        for key in dir(self):
            if key.startswith('_validate_'):
                validKeys.append(key.split('_validate_', 1)[1])
        for key in list(policy):
            if key.startswith('_'):
                del policy[key]
        for key in policy:
            if key not in validKeys:
                raise RestException(
                    '%s is not a valid quota policy key.  Valid keys are '
                    '%s.' % (key, ', '.join(sorted(validKeys))))
            funcName = '_validate_' + key
            policy[key] = getattr(self, funcName)(policy[key])
        return policy

    @access.public
    @loadmodel(model='collection', level=AccessType.READ)
    @describeRoute(
        Description('Get quota and assetstore policies for the collection.')
        .param('id', 'The collection ID', paramType='path')
        .errorResponse('ID was invalid.')
        .errorResponse('Read permission denied on the collection.', 403)
    )
    def getCollectionQuota(self, collection, params):
        if QUOTA_FIELD not in collection:
            collection[QUOTA_FIELD] = {}
        collection[QUOTA_FIELD][
            '_currentFileSizeQuota'] = self._getFileSizeQuota(
            'collection', collection)
        return self._filter('collection', collection)

    @access.public
    @loadmodel(model='collection', level=AccessType.ADMIN)
    @describeRoute(
        Description('Set quota and assetstore policies for the collection.')
        .param('id', 'The collection ID', paramType='path')
        .param('policy', 'A JSON object containing the policies.  This is a '
               'dictionary of keys and values.  Any key that is not specified '
               'does not change.', required=True)
        .errorResponse('ID was invalid.')
        .errorResponse('Read permission denied on the collection.', 403)
    )
    def setCollectionQuota(self, collection, params):
        return self._setResourceQuota('collection', collection, params)

    @access.public
    @loadmodel(model='user', level=AccessType.READ)
    @describeRoute(
        Description('Get quota and assetstore policies for the user.')
        .param('id', 'The user ID', paramType='path')
        .errorResponse('ID was invalid.')
        .errorResponse('Read permission denied on the user.', 403)
    )
    def getUserQuota(self, user, params):
        if QUOTA_FIELD not in user:
            user[QUOTA_FIELD] = {}
        user[QUOTA_FIELD]['_currentFileSizeQuota'] = self._getFileSizeQuota(
            'user', user)
        return self._filter('user', user)

    @access.public
    @loadmodel(model='user', level=AccessType.ADMIN)
    @describeRoute(
        Description('Set quota and assetstore policies for the user.')
        .param('id', 'The user ID', paramType='path')
        .param('policy', 'A JSON object containing the policies.  This is a '
               'dictionary of keys and values.  Any key that is not specified '
               'does not change.', required=True)
        .errorResponse('ID was invalid.')
        .errorResponse('Read permission denied on the user.', 403)
    )
    def setUserQuota(self, user, params):
        return self._setResourceQuota('user', user, params)

    def _checkAssetstore(self, assetstoreSpec):
        """
        Check is a specified assetstore is available.

        :param assetstoreSpec: None for use current assetstore, 'none' to
                               disallow the assetstore, or an assetstore ID to
                               check if that assetstore exists and is nominally
                               available.
        :returns: None to use the current assetstore, False to indicate no
                  assetstore is allowed, or an assetstore document of an
                  allowed assetstore.
        """
        if assetstoreSpec is None:
            return None
        if assetstoreSpec == 'none':
            return False
        assetstore = self.model('assetstore').load(id=assetstoreSpec)
        if not assetstore:
            return False
        adapter = assetstore_utilities.getAssetstoreAdapter(assetstore)
        if getattr(adapter, 'unavailable', False):