# Copyright (c) 2015 Mirantis, 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.

from glanceclient.common import utils
from glanceclient import exc
from oslo_utils import encodeutils
from urllib import parse

from muranoclient.glance import ArtifactType


glare_urls = {
    'create': '/v{version}/artifacts/{type_name}/v{type_version}/drafts',
    'update_get_delete': '/v{version}/artifacts/{type_name}/v{type_version}'
                         '/{artifact_id}',
    'list_drafts': '/v{version}/artifacts/{type_name}/v{type_version}/drafts?',
    'list_no_drafts': '/v{version}/artifacts/{type_name}/v{type_version}?',
    'publish': '/v{version}/artifacts/{type_name}/v{type_version}/'
               '{artifact_id}/publish',
    'blob': '/v{version}/artifacts/{type_name}/v{type_version}/{artifact_id}'
            '/{blob_property}',
}


class Controller(object):
    def __init__(self, http_client, type_name=None, type_version=None,
                 version='0.1'):
        self.http_client = http_client
        self.type_name = type_name
        self.type_version = type_version
        self.version = version
        self.default_page_size = 20
        self.sort_dir_values = ('asc', 'desc')

    def _check_type_params(self, type_name, type_version):
        """Check that type name and type versions were specified"""
        type_name = type_name or self.type_name
        type_version = type_version or self.type_version

        if type_name is None:
            msg = "Type name must be specified"
            raise exc.HTTPBadRequest(msg)

        if type_version is None:
            msg = "Type version must be specified"
            raise exc.HTTPBadRequest(msg)

        return type_name, type_version

    def _validate_sort_param(self, sort):
        """Validates sorting argument for invalid keys and directions values.

        :param sort: comma-separated list of sort keys with optional <:dir>
        after each key
        """
        for sort_param in sort.strip().split(','):
            key, _sep, dir = sort_param.partition(':')
            if dir and dir not in self.sort_dir_values:
                msg = ('Invalid sort direction: %(sort_dir)s.'
                       ' It must be one of the following: %(available)s.'
                       ) % {'sort_dir': dir,
                            'available': ', '.join(self.sort_dir_values)}
                raise exc.HTTPBadRequest(msg)
        return sort

    def create(self, name, version, type_name=None, type_version=None,
               **kwargs):
        """Create an artifact of given type and version.

        :param name: name of creating artifact.
        :param version: semver string describing an artifact version
        """
        type_name, type_version = self._check_type_params(type_name,
                                                          type_version)
        kwargs.update({'name': name, 'version': version})
        url = glare_urls['create'].format(version=self.version,
                                          type_name=type_name,
                                          type_version=type_version)
        resp, body = self.http_client.post(url, data=kwargs)
        return ArtifactType(**body)

    def update(self, artifact_id, type_name=None, type_version=None,
               remove_props=None, **kwargs):
        """Update attributes of an artifact.

        :param artifact_id: ID of the artifact to modify.
        :param remove_props: List of property names to remove
        :param **kwargs: Artifact attribute names and their new values.
        """
        type_name, type_version = self._check_type_params(type_name,
                                                          type_version)
        url = glare_urls['update_get_delete'].format(version=self.version,
                                                     type_name=type_name,
                                                     type_version=type_version,
                                                     artifact_id=artifact_id)
        hdrs = {
            'Content-Type': 'application/openstack-images-v2.1-json-patch'}

        artifact_obj = self.get(artifact_id, type_name, type_version)

        changes = []
        if remove_props:
            for prop in remove_props:
                if prop in ArtifactType.generic_properties:
                    msg = "Generic properties cannot be removed"
                    raise exc.HTTPBadRequest(msg)
                if prop not in kwargs:
                    changes.append({'op': 'remove',
                                    'path': '/' + prop})

        for prop in kwargs:
            if prop in artifact_obj.generic_properties:
                op = 'add' if getattr(artifact_obj,
                                      prop) is None else 'replace'
            elif prop in artifact_obj.type_specific_properties:
                if artifact_obj.type_specific_properties[prop] is None:
                    op = 'add'
                else:
                    op = 'replace'
            else:
                msg = ("Property '%s' doesn't exist in type '%s' with version"
                       " '%s'" % (prop, type_name, type_version))
                raise exc.HTTPBadRequest(msg)
            changes.append({'op': op, 'path': '/' + prop,
                            'value': kwargs[prop]})

        resp, body = self.http_client.patch(url, headers=hdrs, data=changes)
        return ArtifactType(**body)

    def get(self, artifact_id, type_name=None, type_version=None,
            show_level=None):
        """Get information about an artifact.

        :param artifact_id: ID of the artifact to get.
        :param show_level: value of datalization. Possible values:
                           "none", "basic", "direct", "transitive"
        """
        type_name, type_version = self._check_type_params(type_name,
                                                          type_version)

        url = glare_urls['update_get_delete'].format(version=self.version,
                                                     type_name=type_name,
                                                     type_version=type_version,
                                                     artifact_id=artifact_id)
        if show_level:
            if show_level not in ArtifactType.supported_show_levels:
                msg = "Invalid show level: %s" % show_level
                raise exc.HTTPBadRequest(msg)
            url += '?show_level=%s' % show_level
        resp, body = self.http_client.get(url)
        return ArtifactType(**body)

    def list(self, **kwargs):
        return self._list(drafts=False, **kwargs)

    def drafts(self, **kwargs):
        return self._list(drafts=True, **kwargs)

    def _list(self, drafts, type_name=None, type_version=None, **kwargs):
        """Retrieve a listing of Image objects.

        :param page_size: Number of images to request in each
                          paginated request.
        :returns: generator over list of artifacts.
        """
        type_name, type_version = self._check_type_params(type_name,
                                                          type_version)

        limit = kwargs.get('limit')
        page_size = kwargs.get('page_size') or self.default_page_size

        def paginate(url, page_size, limit=None):
            next_url = url

            while True:
                if limit and page_size > limit:
                    next_url = next_url.replace("limit=%s" % page_size,
                                                "limit=%s" % limit)

                resp, body = self.http_client.get(next_url)
                for artifact in body['artifacts']:
                    yield ArtifactType(**artifact)

                    if limit:
                        limit -= 1
                        if limit <= 0:
                            raise StopIteration

                try:
                    next_url = body['next']
                except KeyError:
                    return

        filters = kwargs.get('filters', {})
        filters['limit'] = page_size

        url_params = []
        for param, items in filters.items():
            values = [items] if not isinstance(items, list) else items
            for value in values:
                if isinstance(value, str):
                    value = encodeutils.safe_encode(value)
                url_params.append({param: value})

        if drafts:
            url = glare_urls['list_drafts'].format(version=self.version,
                                                   type_name=type_name,
                                                   type_version=type_version)
        else:
            url = glare_urls['list_no_drafts'].format(
                version=self.version,
                type_name=type_name,
                type_version=type_version
            )

        for param in url_params:
            url = '%s&%s' % (url, parse.urlencode(param))

        if 'sort' in kwargs:
            url = '%s&sort=%s' % (url, self._validate_sort_param(
                kwargs['sort']))

        for artifact in paginate(url, page_size, limit):
            yield artifact

    def active(self, artifact_id, type_name=None, type_version=None):
        """Set artifact status to 'active'.

        :param artifact_id: ID of the artifact to get.
        """
        type_name, type_version = self._check_type_params(type_name,
                                                          type_version)

        url = glare_urls['publish'].format(version=self.version,
                                           type_name=type_name,
                                           type_version=type_version,
                                           artifact_id=artifact_id)

        resp, body = self.http_client.post(url)
        return ArtifactType(**body)

    def deactivate(self, artifact_id, type_name=None, type_version=None):
        raise NotImplementedError()

    def delete(self, artifact_id, type_name=None, type_version=None):
        """Delete an artifact and all its data.

        :param artifact_id: ID of the artifact to delete.
        """
        type_name, type_version = self._check_type_params(type_name,
                                                          type_version)
        url = glare_urls['update_get_delete'].format(version=self.version,
                                                     type_name=type_name,
                                                     type_version=type_version,
                                                     artifact_id=artifact_id)
        self.http_client.delete(url)

    def upload_blob(self, artifact_id, blob_property, data, position=None,
                    type_name=None, type_version=None):
        """Upload blob data.

        :param artifact_id: ID of the artifact to download a blob
        :param blob_property: blob property name
        :param position: if blob_property is a list then the
        position must be specified
        """
        type_name, type_version = self._check_type_params(type_name,
                                                          type_version)
        hdrs = {'Content-Type': 'application/octet-stream'}

        url = glare_urls['blob'].format(version=self.version,
                                        type_name=type_name,
                                        type_version=type_version,
                                        artifact_id=artifact_id,
                                        blob_property=blob_property)
        if position:
            url += "/%s" % position

        self.http_client.put(url, headers=hdrs, data=data)

    def download_blob(self, artifact_id, blob_property, position=None,
                      type_name=None, type_version=None, do_checksum=True):
        """Get blob data.

        :param artifact_id: ID of the artifact to download a blob
        :param blob_property: blob property name
        :param position: if blob_property is a list then the
        position must be specified
        :param do_checksum: Enable/disable checksum validation.
        """
        type_name, type_version = self._check_type_params(type_name,
                                                          type_version)
        url = glare_urls['blob'].format(version=self.version,
                                        type_name=type_name,
                                        type_version=type_version,
                                        artifact_id=artifact_id,
                                        blob_property=blob_property)
        if position:
            url += '/%s' % position

        url += '/download'

        resp, body = self.http_client.get(url)
        checksum = resp.headers.get('content-md5', None)
        content_length = int(resp.headers.get('content-length', 0))

        if checksum is not None and do_checksum:
            body = utils.integrity_iter(body, checksum)

        return utils.IterableWithLength(body, content_length)

    def delete_blob(self, artifact_id, blob_property, position=None,
                    type_name=None, type_version=None):
        """Delete blob and related data.

        :param artifact_id: ID of the artifact to delete a blob
        :param blob_property: blob property name
        :param position: if blob_property is a list then the
        position must be specified
        """
        type_name, type_version = self._check_type_params(type_name,
                                                          type_version)
        url = glare_urls['blob'].format(version=self.version,
                                        type_name=type_name,
                                        type_version=type_version,
                                        artifact_id=artifact_id,
                                        blob_property=blob_property)
        if position:
            url += '/%s' % position
        self.http_client.delete(url)

    def add_property(self, artifact_id, dependency_id, position=None,
                     type_name=None, type_version=None):
        raise NotImplementedError()

    def replace_property(self, artifact_id, dependency_id, position=None,
                         type_name=None, type_version=None):
        raise NotImplementedError()

    def remove_property(self, artifact_id, dependency_id, position=None,
                        type_name=None, type_version=None):
        raise NotImplementedError()

    def artifact_export(self, artifact_id,
                        type_name=None, type_version=None):
        raise NotImplementedError()

    def artifact_import(self, data, type_name=None, type_version=None):
        raise NotImplementedError()
