# Copyright (c) 2016 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.

import os

from oslo_serialization import jsonutils
from oslo_utils import encodeutils
import six
from six.moves.urllib import parse

from glareclient.common import utils
from glareclient import exc


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

    def _check_type_name(self, type_name):
        """Check that type name and type versions were specified."""
        type_name = type_name or self.type_name
        if type_name is None:
            msg = "Type name must be specified"
            raise exc.HTTPBadRequest(msg)
        return type_name

    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='0.0.0', type_name=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 = self._check_type_name(type_name)
        kwargs.update({'name': name, 'version': version})
        url = '/artifacts/%s' % type_name
        resp, body = self.http_client.post(url, json=kwargs)
        return body

    def update(self, artifact_id, type_name=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 = self._check_type_name(type_name)
        url = '/artifacts/%s/%s' % (type_name, artifact_id)
        hdrs = {'Content-Type': 'application/json-patch+json'}
        changes = []
        if remove_props:
            for prop_name in remove_props:
                if prop_name not in kwargs:
                    if '/' in prop_name:
                        # we remove all values in dicts and lists explicitly,
                        # i.e. matadata/key or releases/1
                        changes.append({'op': 'remove',
                                        'path': '/%s' % prop_name})
                    else:
                        # in other cases we just replace the value with None
                        changes.append({'op': 'replace',
                                        'path': '/%s' % prop_name,
                                        'value': None})
        for prop_name in kwargs:
            changes.append({'op': 'add', 'path': '/%s' % prop_name,
                            'value': kwargs[prop_name]})
        resp, body = self.http_client.patch(url, headers=hdrs, json=changes)
        return body

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

        :param artifact_id: ID of the artifact to get.

        """
        type_name = self._check_type_name(type_name)
        url = '/artifacts/%s/%s' % (type_name, artifact_id)
        resp, body = self.http_client.get(url)
        return body

    def get_by_name(self, name, version='latest', type_name=None):
        """Get information about an artifact by name.

        :param name: name of the artifact to get.
        :param version: version of the artifact to get
        :param type_name: type name of the artifact
        """
        type_name = self._check_type_name(type_name)
        url = '/artifacts/%s?version=%s&name=%s' % (type_name, version, name)
        resp, body = self.http_client.get(url)
        arts = body.get('artifacts', body.get(type_name))
        if not arts:
            msg = ('Artifact with name=%s and version=%s not found.' %
                   (name, version))
            raise exc.BadRequest(msg)
        if len(arts) > 1:
            if type_name != "all":
                output = "\n".join([
                    "Artifact: %s, owner: %s, visibility: %s" % (
                        i['id'], i['owner'], i['visibility']) for i in arts])
            else:
                output = "\n".join([
                    "Artifact: %s, owner: %s, visibility: %s, type: %s" % (
                        i['id'], i['owner'], i['visibility'], i['type_name'])
                    for i in arts])
            msg = (
                'There are more then one artifact with name=%s and version=%s.'
                ' Please provide the concrete id from the list:\n%s' %
                (name, version, output))
            raise exc.BadRequest(msg)
        return arts[0]

    def list(self, type_name=None, **kwargs):
        """Retrieve a listing of artifacts objects.

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

        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 backward compatibility we also look for the list of
                # artifacts under the type_name section.
                # In current versions it should be located in 'artifacts'.
                for artifact in body.get('artifacts', body.get(type_name)):
                    yield artifact

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

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

        filters = kwargs.get('filters', [])
        filters.append(('limit', page_size))
        if kwargs.get('marker'):
            filters.append(('marker', kwargs.get('marker')))

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

        url = '/artifacts/%s?' % type_name

        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 activate(self, artifact_id, type_name=None):
        """Set artifact status to 'active'.

        :param artifact_id: ID of the artifact to get.
        """
        return self.update(artifact_id, type_name,
                           status='active')

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

        :param artifact_id: ID of the artifact to get.
        """
        return self.update(artifact_id, type_name,
                           status='deactivated')

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

        :param artifact_id: ID of the artifact to get.
        """
        return self.update(artifact_id, type_name,
                           status='active')

    def publish(self, artifact_id, type_name=None):
        """Set artifact visibility to 'public'.

        :param artifact_id: ID of the artifact to get.
        """
        return self.update(artifact_id, type_name,
                           visibility='public')

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

        :param artifact_id: ID of the artifact to delete.
        """
        type_name = self._check_type_name(type_name)
        url = '/artifacts/%s/%s' % (type_name, artifact_id)
        self.http_client.delete(url)

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

        :param artifact_id: ID of the artifact to download a blob
        :param blob_property: blob property name
        """
        content_type = content_type or 'application/octet-stream'
        hdrs = {'Content-Type': content_type}
        type_name = self._check_type_name(type_name)

        content_length = None
        if isinstance(data, six.string_types):
            content_length = len(data)
        else:
            try:
                content_length = os.path.getsize(data.name)
            except Exception:
                # if for some reason we can't get the file size, then we just
                # ignore it.
                pass
        if content_length is not None:
            hdrs['Content-Length'] = str(content_length)

        url = '/artifacts/%s/%s/%s' % (type_name, artifact_id, blob_property)
        self.http_client.put(url, headers=hdrs, data=data)

    def add_external_location(self, artifact_id, blob_property, data,
                              type_name=None):
        """Add external location.

        :param artifact_id: ID of the artifact to download a blob
        :param blob_property: blob property name
        """
        content_type = 'application/vnd+openstack.glare-custom-location+json'

        type_name = self._check_type_name(type_name)
        hdrs = {'Content-Type': content_type}
        url = '/artifacts/%s/%s/%s' % (type_name, artifact_id, blob_property)
        try:
            data = jsonutils.dumps(data)
        except TypeError:
            raise exc.HTTPBadRequest("json is malformed.")
        self.http_client.put(url, headers=hdrs, data=data)

    def remove_external_location(self, artifact_id, blob_property,
                                 type_name=None):
        """Remove external location.

        :param artifact_id: ID of the artifact with external location
         to be removed
        :param blob_property: blob property name
        """
        type_name = self._check_type_name(type_name)
        url = '/artifacts/%s/%s/%s' % (type_name, artifact_id, blob_property)
        self.http_client.delete(url)

    def download_blob(self, artifact_id, blob_property, type_name=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 do_checksum: Enable/disable checksum validation.
        """
        type_name = self._check_type_name(type_name)
        url = '/artifacts/%s/%s/%s' % (type_name, artifact_id, blob_property)
        resp, body = self.http_client.get(url, redirect=False,
                                          stream=True,
                                          headers={"Accept": "*/*"})
        return utils.ResponseBlobWrapper(resp, do_checksum)

    def get_type_list(self):
        """Get list of type names."""
        resp, body = self.http_client.get('/schemas')
        type_list = []
        for type_name, type_schema in six.iteritems(body['schemas']):
            type_list.append((type_name, type_schema['version']))
        return type_list

    def get_type_schema(self, type_name=None):
        """Show schema of type name."""

        type_name = self._check_type_name(type_name)
        url = '/schemas/%s' % type_name
        resp, body = self.http_client.get(url)
        return body['schemas'][type_name]

    def add_tag(self, artifact_id, tag_value, type_name=None):
        """Add tag to artifact.

        :param artifact_id: ID of the artifact to add a tag
        :param tag_value: value of the tag to add
        """
        type_name = self._check_type_name(type_name)
        url = '/artifacts/%s/%s' % (type_name, artifact_id)
        resp, body = self.http_client.get(url)
        tags = body['tags']
        if tag_value in tags:
            return body
        tags.append(tag_value)
        return self.update(artifact_id, type_name, tags=tags)

    def remove_tag(self, artifact_id, tag_value, type_name=None):
        """Remove tag from artifact.

        :param artifact_id: ID of the artifact to remove a tag
        :param tag_value: value of the tag to remove
        """
        type_name = self._check_type_name(type_name)
        url = '/artifacts/%s/%s' % (type_name, artifact_id)
        resp, body = self.http_client.get(url)
        tags = body['tags']
        if tag_value not in tags:
            return body
        tags.remove(tag_value)
        return self.update(artifact_id, type_name, tags=tags)
