# Licensed to the StackStorm, Inc ('StackStorm') under one or more
# contributor license agreements.  See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You 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 __future__ import absolute_import
import os
import json
import logging
from functools import wraps

import six
from six.moves import urllib
from six.moves import http_client

from st2client.utils import httpclient


LOG = logging.getLogger(__name__)


def add_auth_token_to_kwargs_from_env(func):
    @wraps(func)
    def decorate(*args, **kwargs):
        if not kwargs.get('token') and os.environ.get('ST2_AUTH_TOKEN', None):
            kwargs['token'] = os.environ.get('ST2_AUTH_TOKEN')
        if not kwargs.get('api_key') and os.environ.get('ST2_API_KEY', None):
            kwargs['api_key'] = os.environ.get('ST2_API_KEY')

        return func(*args, **kwargs)
    return decorate


class Resource(object):

    # An alias to use for the resource if different than the class name.
    _alias = None

    # Display name of the resource. This may be different than its resource
    # name specifically when the resource name is composed of multiple words.
    _display_name = None

    # URL path for the resource.
    _url_path = None

    # Plural form of the resource name. This will be used to build the
    # latter part of the REST URL.
    _plural = None

    # Plural form of the resource display name.
    _plural_display_name = None

    # A list of class attributes which will be included in __repr__ return value
    _repr_attributes = []

    def __init__(self, *args, **kwargs):
        for k, v in six.iteritems(kwargs):
            setattr(self, k, v)

    def to_dict(self, exclude_attributes=None):
        """
        Return a dictionary representation of this object.

        :param exclude_attributes: Optional list of attributes to exclude.
        :type exclude_attributes: ``list``

        :rtype: ``dict``
        """
        exclude_attributes = exclude_attributes or []

        attributes = list(self.__dict__.keys())
        attributes = [attr for attr in attributes if not attr.startswith('__') and
                      attr not in exclude_attributes]

        result = {}
        for attribute in attributes:
            value = getattr(self, attribute, None)
            result[attribute] = value

        return result

    @classmethod
    def get_alias(cls):
        return cls._alias if cls._alias else cls.__name__

    @classmethod
    def get_display_name(cls):
        return cls._display_name if cls._display_name else cls.__name__

    @classmethod
    def get_plural_name(cls):
        if not cls._plural:
            raise Exception('The %s class is missing class attributes '
                            'in its definition.' % cls.__name__)
        return cls._plural

    @classmethod
    def get_plural_display_name(cls):
        return (cls._plural_display_name
                if cls._plural_display_name
                else cls._plural)

    @classmethod
    def get_url_path_name(cls):
        if cls._url_path:
            return cls._url_path

        return cls.get_plural_name().lower()

    def serialize(self):
        return dict((k, v)
                    for k, v in six.iteritems(self.__dict__)
                    if not k.startswith('_'))

    @classmethod
    def deserialize(cls, doc):
        if type(doc) is not dict:
            doc = json.loads(doc)
        return cls(**doc)

    def __str__(self):
        return str(self.__repr__())

    def __repr__(self):
        if not self._repr_attributes:
            return super(Resource, self).__repr__()

        attributes = []
        for attribute in self._repr_attributes:
            value = getattr(self, attribute, None)
            attributes.append('%s=%s' % (attribute, value))

        attributes = ','.join(attributes)
        class_name = self.__class__.__name__
        result = '<%s %s>' % (class_name, attributes)
        return result


class ResourceManager(object):

    def __init__(self, resource, endpoint, cacert=None, debug=False):
        self.resource = resource
        self.debug = debug
        self.client = httpclient.HTTPClient(endpoint, cacert=cacert, debug=debug)

    @staticmethod
    def handle_error(response):
        try:
            content = response.json()
            fault = content.get('faultstring', '') if content else ''
            if fault:
                response.reason += '\nMESSAGE: %s' % fault
        except Exception as e:
            response.reason += ('\nUnable to retrieve detailed message '
                                'from the HTTP response. %s\n' % str(e))
        response.raise_for_status()

    @add_auth_token_to_kwargs_from_env
    def get_all(self, **kwargs):
        # TODO: This is ugly, stop abusing kwargs
        url = '/%s' % self.resource.get_url_path_name()
        limit = kwargs.pop('limit', None)
        pack = kwargs.pop('pack', None)
        prefix = kwargs.pop('prefix', None)
        user = kwargs.pop('user', None)

        params = kwargs.pop('params', {})

        if limit:
            params['limit'] = limit

        if pack:
            params['pack'] = pack

        if prefix:
            params['prefix'] = prefix

        if user:
            params['user'] = user

        response = self.client.get(url=url, params=params, **kwargs)
        if response.status_code != http_client.OK:
            self.handle_error(response)
        return [self.resource.deserialize(item)
                for item in response.json()]

    @add_auth_token_to_kwargs_from_env
    def get_by_id(self, id, **kwargs):
        url = '/%s/%s' % (self.resource.get_url_path_name(), id)
        response = self.client.get(url, **kwargs)
        if response.status_code == http_client.NOT_FOUND:
            return None
        if response.status_code != http_client.OK:
            self.handle_error(response)
        return self.resource.deserialize(response.json())

    @add_auth_token_to_kwargs_from_env
    def get_property(self, id_, property_name, self_deserialize=True, **kwargs):
        """
        Gets a property of a Resource.
        id_ : Id of the resource
        property_name: Name of the property
        self_deserialize: #Implies use the deserialize method implemented by this resource.
        """
        token = kwargs.pop('token', None)
        api_key = kwargs.pop('api_key', None)

        if kwargs:
            url = '/%s/%s/%s/?%s' % (self.resource.get_url_path_name(), id_, property_name,
                                     urllib.parse.urlencode(kwargs))
        else:
            url = '/%s/%s/%s/' % (self.resource.get_url_path_name(), id_, property_name)

        if token:
            response = self.client.get(url, token=token)
        elif api_key:
            response = self.client.get(url, api_key=api_key)
        else:
            response = self.client.get(url)

        if response.status_code == http_client.NOT_FOUND:
            return None
        if response.status_code != http_client.OK:
            self.handle_error(response)

        if self_deserialize:
            return [self.resource.deserialize(item) for item in response.json()]
        else:
            return response.json()

    @add_auth_token_to_kwargs_from_env
    def get_by_ref_or_id(self, ref_or_id, **kwargs):
        return self.get_by_id(id=ref_or_id, **kwargs)

    def _query_details(self, **kwargs):
        if not kwargs:
            raise Exception('Query parameter is not provided.')

        token = kwargs.get('token', None)
        api_key = kwargs.get('api_key', None)
        params = kwargs.get('params', {})

        for k, v in six.iteritems(kwargs):
            # Note: That's a special case to support api_key and token kwargs
            if k not in ['token', 'api_key', 'params']:
                params[k] = v

        url = '/%s/?%s' % (self.resource.get_url_path_name(),
                           urllib.parse.urlencode(params))

        if token:
            response = self.client.get(url, token=token)
        elif api_key:
            response = self.client.get(url, api_key=api_key)
        else:
            response = self.client.get(url)

        if response.status_code == http_client.NOT_FOUND:
            # for query and query_with_count
            return [], None
        if response.status_code != http_client.OK:
            self.handle_error(response)
        items = response.json()
        instances = [self.resource.deserialize(item) for item in items]
        return instances, response

    @add_auth_token_to_kwargs_from_env
    def query(self, **kwargs):
        instances, _ = self._query_details(**kwargs)
        return instances

    @add_auth_token_to_kwargs_from_env
    def query_with_count(self, **kwargs):
        instances, response = self._query_details(**kwargs)
        if response and 'X-Total-Count' in response.headers:
            return (instances, int(response.headers['X-Total-Count']))
        else:
            return (instances, None)

    @add_auth_token_to_kwargs_from_env
    def get_by_name(self, name, **kwargs):
        instances = self.query(name=name, **kwargs)
        if not instances:
            return None
        else:
            if len(instances) > 1:
                raise Exception('More than one %s named "%s" are found.' %
                                (self.resource.__name__.lower(), name))
            return instances[0]

    @add_auth_token_to_kwargs_from_env
    def create(self, instance, **kwargs):
        url = '/%s' % self.resource.get_url_path_name()
        response = self.client.post(url, instance.serialize(), **kwargs)
        if response.status_code != http_client.OK:
            self.handle_error(response)
        instance = self.resource.deserialize(response.json())
        return instance

    @add_auth_token_to_kwargs_from_env
    def update(self, instance, **kwargs):
        url = '/%s/%s' % (self.resource.get_url_path_name(), instance.id)
        response = self.client.put(url, instance.serialize(), **kwargs)
        if response.status_code != http_client.OK:
            self.handle_error(response)
        instance = self.resource.deserialize(response.json())
        return instance

    @add_auth_token_to_kwargs_from_env
    def delete(self, instance, **kwargs):
        url = '/%s/%s' % (self.resource.get_url_path_name(), instance.id)
        response = self.client.delete(url, **kwargs)

        if response.status_code not in [http_client.OK,
                                        http_client.NO_CONTENT,
                                        http_client.NOT_FOUND]:
            self.handle_error(response)
            return False

        return True

    @add_auth_token_to_kwargs_from_env
    def delete_by_id(self, instance_id, **kwargs):
        url = '/%s/%s' % (self.resource.get_url_path_name(), instance_id)
        response = self.client.delete(url, **kwargs)
        if response.status_code not in [http_client.OK,
                                        http_client.NO_CONTENT,
                                        http_client.NOT_FOUND]:
            self.handle_error(response)
            return False
        try:
            resp_json = response.json()
            if resp_json:
                return resp_json
        except:
            pass
        return True


class ActionAliasResourceManager(ResourceManager):
    def __init__(self, resource, endpoint, cacert=None, debug=False):
        self.resource = resource
        self.debug = debug
        self.client = httpclient.HTTPClient(root=endpoint, cacert=cacert, debug=debug)

    @add_auth_token_to_kwargs_from_env
    def match(self, instance, **kwargs):
        url = '/%s/match' % self.resource.get_url_path_name()
        response = self.client.post(url, instance.serialize(), **kwargs)
        if response.status_code != http_client.OK:
            self.handle_error(response)
        match = response.json()
        return (self.resource.deserialize(match['actionalias']), match['representation'])


class ActionAliasExecutionManager(ResourceManager):
    @add_auth_token_to_kwargs_from_env
    def match_and_execute(self, instance, **kwargs):
        url = '/%s/match_and_execute' % self.resource.get_url_path_name()
        response = self.client.post(url, instance.serialize(), **kwargs)

        if response.status_code != http_client.OK:
            self.handle_error(response)
        instance = self.resource.deserialize(response.json())
        return instance


class LiveActionResourceManager(ResourceManager):
    @add_auth_token_to_kwargs_from_env
    def re_run(self, execution_id, parameters=None, tasks=None, no_reset=None, **kwargs):
        url = '/%s/%s/re_run' % (self.resource.get_url_path_name(), execution_id)

        tasks = tasks or []
        no_reset = no_reset or []

        if list(set(no_reset) - set(tasks)):
            raise ValueError('List of tasks to reset does not match the tasks to rerun.')

        data = {
            'parameters': parameters or {},
            'tasks': tasks,
            'reset': list(set(tasks) - set(no_reset))
        }

        response = self.client.post(url, data, **kwargs)
        if response.status_code != http_client.OK:
            self.handle_error(response)

        instance = self.resource.deserialize(response.json())
        return instance

    @add_auth_token_to_kwargs_from_env
    def get_output(self, execution_id, output_type=None, **kwargs):
        url = '/%s/%s/output' % (self.resource.get_url_path_name(), execution_id)

        if output_type:
            url += '?' + urllib.parse.urlencode({'output_type': output_type})

        response = self.client.get(url, **kwargs)
        if response.status_code != http_client.OK:
            self.handle_error(response)

        return response.text

    @add_auth_token_to_kwargs_from_env
    def pause(self, execution_id, **kwargs):
        url = '/%s/%s' % (self.resource.get_url_path_name(), execution_id)
        data = {'status': 'pausing'}

        response = self.client.put(url, data, **kwargs)

        if response.status_code != http_client.OK:
            self.handle_error(response)

        return self.resource.deserialize(response.json())

    @add_auth_token_to_kwargs_from_env
    def resume(self, execution_id, **kwargs):
        url = '/%s/%s' % (self.resource.get_url_path_name(), execution_id)
        data = {'status': 'resuming'}

        response = self.client.put(url, data, **kwargs)

        if response.status_code != http_client.OK:
            self.handle_error(response)

        return self.resource.deserialize(response.json())


class InquiryResourceManager(ResourceManager):

    @add_auth_token_to_kwargs_from_env
    def respond(self, inquiry_id, inquiry_response, **kwargs):
        """
        Update st2.inquiry.respond action
        Update st2client respond command to use this?
        """
        url = '/%s/%s' % (self.resource.get_url_path_name(), inquiry_id)

        payload = {
            "id": inquiry_id,
            "response": inquiry_response
        }

        response = self.client.put(url, payload, **kwargs)

        if response.status_code != http_client.OK:
            self.handle_error(response)

        return self.resource.deserialize(response.json())


class TriggerInstanceResourceManager(ResourceManager):
    @add_auth_token_to_kwargs_from_env
    def re_emit(self, trigger_instance_id, **kwargs):
        url = '/%s/%s/re_emit' % (self.resource.get_url_path_name(), trigger_instance_id)
        response = self.client.post(url, None, **kwargs)
        if response.status_code != http_client.OK:
            self.handle_error(response)
        return response.json()


class AsyncRequest(Resource):
    pass


class PackResourceManager(ResourceManager):
    @add_auth_token_to_kwargs_from_env
    def install(self, packs, force=False, python3=False, **kwargs):
        url = '/%s/install' % (self.resource.get_url_path_name())
        payload = {
            'packs': packs,
            'force': force,
            'python3': python3
        }
        response = self.client.post(url, payload, **kwargs)
        if response.status_code != http_client.OK:
            self.handle_error(response)
        instance = AsyncRequest.deserialize(response.json())
        return instance

    @add_auth_token_to_kwargs_from_env
    def remove(self, packs, **kwargs):
        url = '/%s/uninstall' % (self.resource.get_url_path_name())
        response = self.client.post(url, {'packs': packs}, **kwargs)
        if response.status_code != http_client.OK:
            self.handle_error(response)
        instance = AsyncRequest.deserialize(response.json())
        return instance

    @add_auth_token_to_kwargs_from_env
    def search(self, args, ignore_errors=False, **kwargs):
        url = '/%s/index/search' % (self.resource.get_url_path_name())
        if 'query' in vars(args):
            payload = {'query': args.query}
        else:
            payload = {'pack': args.pack}

            response = self.client.post(url, payload, **kwargs)

        if response.status_code != http_client.OK:
            if ignore_errors:
                return None

            self.handle_error(response)
        data = response.json()
        if isinstance(data, list):
            return [self.resource.deserialize(item) for item in data]
        else:
            return self.resource.deserialize(data) if data else None

    @add_auth_token_to_kwargs_from_env
    def register(self, packs=None, types=None, **kwargs):
        url = '/%s/register' % (self.resource.get_url_path_name())
        payload = {}
        if types:
            payload['types'] = types
        if packs:
            payload['packs'] = packs
        response = self.client.post(url, payload, **kwargs)
        if response.status_code != http_client.OK:
            self.handle_error(response)
        instance = self.resource.deserialize(response.json())
        return instance


class ConfigManager(ResourceManager):
    @add_auth_token_to_kwargs_from_env
    def update(self, instance, **kwargs):
        url = '/%s/%s' % (self.resource.get_url_path_name(), instance.pack)
        response = self.client.put(url, instance.values, **kwargs)
        if response.status_code != http_client.OK:
            self.handle_error(response)
        instance = self.resource.deserialize(response.json())
        return instance


class StreamManager(object):
    def __init__(self, endpoint, cacert, debug):
        self._url = httpclient.get_url_without_trailing_slash(endpoint) + '/stream'
        self.debug = debug
        self.cacert = cacert

    @add_auth_token_to_kwargs_from_env
    def listen(self, events=None, **kwargs):
        # Late import to avoid very expensive in-direct import (~1 second) when this function is
        # not called / used
        from sseclient import SSEClient

        url = self._url
        query_params = {}

        if events and isinstance(events, six.string_types):
            events = [events]

        if 'token' in kwargs:
            query_params['x-auth-token'] = kwargs.get('token')

        if 'api_key' in kwargs:
            query_params['st2-api-key'] = kwargs.get('api_key')

        if events:
            query_params['events'] = ','.join(events)

        query_string = '?' + urllib.parse.urlencode(query_params)
        url = url + query_string

        for message in SSEClient(url):

            # If the execution on the API server takes too long, the message
            # can be empty. In this case, rerun the query.
            if not message.data:
                continue

            yield json.loads(message.data)

st2client/st2client/models/core.py
# Licensed to the StackStorm, Inc ('StackStorm') under one or more
# contributor license agreements.  See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You 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 __future__ import absolute_import
import os
import json
import logging
from functools import wraps

import six
from six.moves import urllib
from six.moves import http_client

from st2client.utils import httpclient


LOG = logging.getLogger(__name__)


def add_auth_token_to_kwargs_from_env(func):
    @wraps(func)
    def decorate(*args, **kwargs):
        if not kwargs.get('token') and os.environ.get('ST2_AUTH_TOKEN', None):
            kwargs['token'] = os.environ.get('ST2_AUTH_TOKEN')
        if not kwargs.get('api_key') and os.environ.get('ST2_API_KEY', None):
            kwargs['api_key'] = os.environ.get('ST2_API_KEY')

        return func(*args, **kwargs)
    return decorate


class Resource(object):

    # An alias to use for the resource if different than the class name.
    _alias = None

    # Display name of the resource. This may be different than its resource
    # name specifically when the resource name is composed of multiple words.
    _display_name = None

    # URL path for the resource.
    _url_path = None

    # Plural form of the resource name. This will be used to build the
    # latter part of the REST URL.
    _plural = None

    # Plural form of the resource display name.
    _plural_display_name = None

    # A list of class attributes which will be included in __repr__ return value
    _repr_attributes = []

    def __init__(self, *args, **kwargs):
        for k, v in six.iteritems(kwargs):
            setattr(self, k, v)

    def to_dict(self, exclude_attributes=None):
        """
        Return a dictionary representation of this object.

        :param exclude_attributes: Optional list of attributes to exclude.
        :type exclude_attributes: ``list``

        :rtype: ``dict``
        """
        exclude_attributes = exclude_attributes or []

        attributes = list(self.__dict__.keys())
        attributes = [attr for attr in attributes if not attr.startswith('__') and
                      attr not in exclude_attributes]

        result = {}
        for attribute in attributes:
            value = getattr(self, attribute, None)
            result[attribute] = value

        return result

    @classmethod
    def get_alias(cls):
        return cls._alias if cls._alias else cls.__name__

    @classmethod
    def get_display_name(cls):
        return cls._display_name if cls._display_name else cls.__name__

    @classmethod
    def get_plural_name(cls):
        if not cls._plural:
            raise Exception('The %s class is missing class attributes '
                            'in its definition.' % cls.__name__)
        return cls._plural

    @classmethod
    def get_plural_display_name(cls):
        return (cls._plural_display_name
                if cls._plural_display_name
                else cls._plural)

    @classmethod
    def get_url_path_name(cls):
        if cls._url_path:
            return cls._url_path

        return cls.get_plural_name().lower()

    def serialize(self):
        return dict((k, v)
                    for k, v in six.iteritems(self.__dict__)
                    if not k.startswith('_'))

    @classmethod
    def deserialize(cls, doc):
        if type(doc) is not dict:
            doc = json.loads(doc)
        return cls(**doc)

    def __str__(self):
        return str(self.__repr__())

    def __repr__(self):
        if not self._repr_attributes:
            return super(Resource, self).__repr__()

        attributes = []
        for attribute in self._repr_attributes:
            value = getattr(self, attribute, None)
            attributes.append('%s=%s' % (attribute, value))

        attributes = ','.join(attributes)
        class_name = self.__class__.__name__
        result = '<%s %s>' % (class_name, attributes)
        return result


class ResourceManager(object):

    def __init__(self, resource, endpoint, cacert=None, debug=False):
        self.resource = resource
        self.debug = debug
        self.client = httpclient.HTTPClient(endpoint, cacert=cacert, debug=debug)

    @staticmethod
    def handle_error(response):
        try:
            content = response.json()
            fault = content.get('faultstring', '') if content else ''
            if fault:
                response.reason += '\nMESSAGE: %s' % fault
        except Exception as e:
            response.reason += ('\nUnable to retrieve detailed message '
                                'from the HTTP response. %s\n' % str(e))
        response.raise_for_status()

    @add_auth_token_to_kwargs_from_env
    def get_all(self, **kwargs):
        # TODO: This is ugly, stop abusing kwargs
        url = '/%s' % self.resource.get_url_path_name()
        limit = kwargs.pop('limit', None)
        pack = kwargs.pop('pack', None)
        prefix = kwargs.pop('prefix', None)
        user = kwargs.pop('user', None)

        params = kwargs.pop('params', {})

        if limit:
            params['limit'] = limit

        if pack:
            params['pack'] = pack

        if prefix:
            params['prefix'] = prefix

        if user:
            params['user'] = user

        response = self.client.get(url=url, params=params, **kwargs)
        if response.status_code != http_client.OK:
            self.handle_error(response)
        return [self.resource.deserialize(item)
                for item in response.json()]

    @add_auth_token_to_kwargs_from_env
    def get_by_id(self, id, **kwargs):
        url = '/%s/%s' % (self.resource.get_url_path_name(), id)
        response = self.client.get(url, **kwargs)
        if response.status_code == http_client.NOT_FOUND:
            return None
        if response.status_code != http_client.OK:
            self.handle_error(response)
        return self.resource.deserialize(response.json())

    @add_auth_token_to_kwargs_from_env
    def get_property(self, id_, property_name, self_deserialize=True, **kwargs):
        """
        Gets a property of a Resource.
        id_ : Id of the resource
        property_name: Name of the property
        self_deserialize: #Implies use the deserialize method implemented by this resource.
        """
        token = kwargs.pop('token', None)
        api_key = kwargs.pop('api_key', None)

        if kwargs:
            url = '/%s/%s/%s/?%s' % (self.resource.get_url_path_name(), id_, property_name,
                                     urllib.parse.urlencode(kwargs))
        else:
            url = '/%s/%s/%s/' % (self.resource.get_url_path_name(), id_, property_name)

        if token:
            response = self.client.get(url, token=token)
        elif api_key:
            response = self.client.get(url, api_key=api_key)
        else:
            response = self.client.get(url)

        if response.status_code == http_client.NOT_FOUND:
            return None
        if response.status_code != http_client.OK:
            self.handle_error(response)

        if self_deserialize:
            return [self.resource.deserialize(item) for item in response.json()]
        else:
            return response.json()

    @add_auth_token_to_kwargs_from_env
    def get_by_ref_or_id(self, ref_or_id, **kwargs):
        return self.get_by_id(id=ref_or_id, **kwargs)

    def _query_details(self, **kwargs):
        if not kwargs:
            raise Exception('Query parameter is not provided.')

        token = kwargs.get('token', None)
        api_key = kwargs.get('api_key', None)
        params = kwargs.get('params', {})

        for k, v in six.iteritems(kwargs):
            # Note: That's a special case to support api_key and token kwargs
            if k not in ['token', 'api_key', 'params']:
                params[k] = v

        url = '/%s/?%s' % (self.resource.get_url_path_name(),
                           urllib.parse.urlencode(params))

        if token:
            response = self.client.get(url, token=token)
        elif api_key:
            response = self.client.get(url, api_key=api_key)
        else:
            response = self.client.get(url)

        if response.status_code == http_client.NOT_FOUND:
            # for query and query_with_count
            return [], None
        if response.status_code != http_client.OK:
            self.handle_error(response)
        items = response.json()
        instances = [self.resource.deserialize(item) for item in items]
        return instances, response

    @add_auth_token_to_kwargs_from_env
    def query(self, **kwargs):
        instances, _ = self._query_details(**kwargs)
        return instances

    @add_auth_token_to_kwargs_from_env
    def query_with_count(self, **kwargs):
        instances, response = self._query_details(**kwargs)
        if response and 'X-Total-Count' in response.headers:
            return (instances, int(response.headers['X-Total-Count']))
        else:
            return (instances, None)

    @add_auth_token_to_kwargs_from_env
    def get_by_name(self, name, **kwargs):
        instances = self.query(name=name, **kwargs)
        if not instances:
            return None
        else:
            if len(instances) > 1:
                raise Exception('More than one %s named "%s" are found.' %
                                (self.resource.__name__.lower(), name))
            return instances[0]

    @add_auth_token_to_kwargs_from_env
    def create(self, instance, **kwargs):
        url = '/%s' % self.resource.get_url_path_name()
        response = self.client.post(url, instance.serialize(), **kwargs)
        if response.status_code != http_client.OK:
            self.handle_error(response)
        instance = self.resource.deserialize(response.json())
        return instance

    @add_auth_token_to_kwargs_from_env
    def update(self, instance, **kwargs):
        url = '/%s/%s' % (self.resource.get_url_path_name(), instance.id)
        response = self.client.put(url, instance.serialize(), **kwargs)
        if response.status_code != http_client.OK:
            self.handle_error(response)
        instance = self.resource.deserialize(response.json())
        return instance

    @add_auth_token_to_kwargs_from_env
    def delete(self, instance, **kwargs):
        url = '/%s/%s' % (self.resource.get_url_path_name(), instance.id)
        response = self.client.delete(url, **kwargs)

        if response.status_code not in [http_client.OK,
                                        http_client.NO_CONTENT,
                                        http_client.NOT_FOUND]:
            self.handle_error(response)
            return False

        return True

    @add_auth_token_to_kwargs_from_env
    def delete_by_id(self, instance_id, **kwargs):
        url = '/%s/%s' % (self.resource.get_url_path_name(), instance_id)
        response = self.client.delete(url, **kwargs)
        if response.status_code not in [http_client.OK,
                                        http_client.NO_CONTENT,
                                        http_client.NOT_FOUND]:
            self.handle_error(response)
            return False
        try:
            resp_json = response.json()
            if resp_json:
                return resp_json
        except:
            pass
        return True


class ActionAliasResourceManager(ResourceManager):
    def __init__(self, resource, endpoint, cacert=None, debug=False):
        self.resource = resource
        self.debug = debug
        self.client = httpclient.HTTPClient(root=endpoint, cacert=cacert, debug=debug)

    @add_auth_token_to_kwargs_from_env
    def match(self, instance, **kwargs):
        url = '/%s/match' % self.resource.get_url_path_name()
        response = self.client.post(url, instance.serialize(), **kwargs)
        if response.status_code != http_client.OK:
            self.handle_error(response)
        match = response.json()
        return (self.resource.deserialize(match['actionalias']), match['representation'])


class ActionAliasExecutionManager(ResourceManager):
    @add_auth_token_to_kwargs_from_env
    def match_and_execute(self, instance, **kwargs):
        url = '/%s/match_and_execute' % self.resource.get_url_path_name()
        response = self.client.post(url, instance.serialize(), **kwargs)

        if response.status_code != http_client.OK:
            self.handle_error(response)
        instance = self.resource.deserialize(response.json())
        return instance


class LiveActionResourceManager(ResourceManager):
    @add_auth_token_to_kwargs_from_env
    def re_run(self, execution_id, parameters=None, tasks=None, no_reset=None, **kwargs):
        url = '/%s/%s/re_run' % (self.resource.get_url_path_name(), execution_id)

        tasks = tasks or []
        no_reset = no_reset or []

        if list(set(no_reset) - set(tasks)):
            raise ValueError('List of tasks to reset does not match the tasks to rerun.')

        data = {
            'parameters': parameters or {},
            'tasks': tasks,
            'reset': list(set(tasks) - set(no_reset))
        }

        response = self.client.post(url, data, **kwargs)
        if response.status_code != http_client.OK:
            self.handle_error(response)

        instance = self.resource.deserialize(response.json())
        return instance

    @add_auth_token_to_kwargs_from_env
    def get_output(self, execution_id, output_type=None, **kwargs):
        url = '/%s/%s/output' % (self.resource.get_url_path_name(), execution_id)

        if output_type:
            url += '?' + urllib.parse.urlencode({'output_type': output_type})

        response = self.client.get(url, **kwargs)
        if response.status_code != http_client.OK:
            self.handle_error(response)

        return response.text

    @add_auth_token_to_kwargs_from_env
    def pause(self, execution_id, **kwargs):
        url = '/%s/%s' % (self.resource.get_url_path_name(), execution_id)
        data = {'status': 'pausing'}

        response = self.client.put(url, data, **kwargs)

        if response.status_code != http_client.OK:
            self.handle_error(response)

        return self.resource.deserialize(response.json())

    @add_auth_token_to_kwargs_from_env
    def resume(self, execution_id, **kwargs):
        url = '/%s/%s' % (self.resource.get_url_path_name(), execution_id)
        data = {'status': 'resuming'}

        response = self.client.put(url, data, **kwargs)

        if response.status_code != http_client.OK:
            self.handle_error(response)

        return self.resource.deserialize(response.json())


class InquiryResourceManager(ResourceManager):

    @add_auth_token_to_kwargs_from_env
    def respond(self, inquiry_id, inquiry_response, **kwargs):
        """
        Update st2.inquiry.respond action
        Update st2client respond command to use this?
        """
        url = '/%s/%s' % (self.resource.get_url_path_name(), inquiry_id)

        payload = {
            "id": inquiry_id,
            "response": inquiry_response
        }

        response = self.client.put(url, payload, **kwargs)

        if response.status_code != http_client.OK:
            self.handle_error(response)

        return self.resource.deserialize(response.json())


class TriggerInstanceResourceManager(ResourceManager):
    @add_auth_token_to_kwargs_from_env
    def re_emit(self, trigger_instance_id, **kwargs):
        url = '/%s/%s/re_emit' % (self.resource.get_url_path_name(), trigger_instance_id)
        response = self.client.post(url, None, **kwargs)
        if response.status_code != http_client.OK:
            self.handle_error(response)
        return response.json()


class AsyncRequest(Resource):
    pass


class PackResourceManager(ResourceManager):
    @add_auth_token_to_kwargs_from_env
    def install(self, packs, force=False, python3=False, **kwargs):
        url = '/%s/install' % (self.resource.get_url_path_name())
        payload = {
            'packs': packs,
            'force': force,
            'python3': python3
        }
        response = self.client.post(url, payload, **kwargs)
        if response.status_code != http_client.OK:
            self.handle_error(response)
        instance = AsyncRequest.deserialize(response.json())
        return instance

    @add_auth_token_to_kwargs_from_env
    def remove(self, packs, **kwargs):
        url = '/%s/uninstall' % (self.resource.get_url_path_name())
        response = self.client.post(url, {'packs': packs}, **kwargs)
        if response.status_code != http_client.OK:
            self.handle_error(response)
        instance = AsyncRequest.deserialize(response.json())
        return instance

    @add_auth_token_to_kwargs_from_env
    def search(self, args, ignore_errors=False, **kwargs):
        url = '/%s/index/search' % (self.resource.get_url_path_name())
        if 'query' in vars(args):
            payload = {'query': args.query}
        else:
            payload = {'pack': args.pack}

        response = self.client.post(url, payload, **kwargs)

        if response.status_code != http_client.OK:
            if ignore_errors:
                return None

            self.handle_error(response)
        data = response.json()
        if isinstance(data, list):
            return [self.resource.deserialize(item) for item in data]
        else:
            return self.resource.deserialize(data) if data else None

    @add_auth_token_to_kwargs_from_env
    def register(self, packs=None, types=None, **kwargs):
        url = '/%s/register' % (self.resource.get_url_path_name())
        payload = {}
        if types:
            payload['types'] = types
        if packs:
            payload['packs'] = packs
        response = self.client.post(url, payload, **kwargs)
        if response.status_code != http_client.OK:
            self.handle_error(response)
        instance = self.resource.deserialize(response.json())
        return instance


class ConfigManager(ResourceManager):
    @add_auth_token_to_kwargs_from_env
    def update(self, instance, **kwargs):
        url = '/%s/%s' % (self.resource.get_url_path_name(), instance.pack)
        response = self.client.put(url, instance.values, **kwargs)
        if response.status_code != http_client.OK:
            self.handle_error(response)
        instance = self.resource.deserialize(response.json())
        return instance


class StreamManager(object):
    def __init__(self, endpoint, cacert, debug):
        self._url = httpclient.get_url_without_trailing_slash(endpoint) + '/stream'
        self.debug = debug
        self.cacert = cacert

    @add_auth_token_to_kwargs_from_env
    def listen(self, events=None, **kwargs):
        # Late import to avoid very expensive in-direct import (~1 second) when this function is
        # not called / used
        from sseclient import SSEClient

        url = self._url
        query_params = {}

        if events and isinstance(events, six.string_types):
            events = [events]

        if 'token' in kwargs:
            query_params['x-auth-token'] = kwargs.get('token')

        if 'api_key' in kwargs:
            query_params['st2-api-key'] = kwargs.get('api_key')

        if events:
            query_params['events'] = ','.join(events)

        query_string = '?' + urllib.parse.urlencode(query_params)
        url = url + query_string

        for message in SSEClient(url):

            # If the execution on the API server takes too long, the message
            # can be empty. In this case, rerun the query.
            if not message.data:
                continue

            yield json.loads(message.data)

st2client/st2client/models/core.py
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index b0cce2a678..3324f95d4e 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -8,8 +8,8 @@ Changed
 ~~~~~~~
 
 * When creating a pack environment during the pack installation, we now pass ``--no-download`` flag
-  to the ``virtualenv`` binary. This way version of pip, wheel and distutils which is already
-  available on the system is used instead of downloading the latest stable versions from PyPi.
+  to the ``virtualenv`` binary. This way version of pip, wheel and distutils which is enforced by
+  virtualenv is used instead of downloading the latest stable versions from PyPi.
 
   This results in more reproducible pack virtual environments and we also ensure pip 9.0 is used (
   there are some known issues with pip 10.0).
@@ -17,6 +17,12 @@ Changed
   If for some reason you want to revert to the old behavior, you can do that by passing
   ``no_download=False`` parameter to the ``packs.setup_virtualenv`` action. #4085
 
+Fixed
+~~~~~
+
+* Fix ``st2 pack search`` and ``POST /api/v1/packs/index/search`` API endpoint so it doesn't
+  return internal server error when a single pack search term is provided. (bug fix) #4083
+
 2.7.0 - April 12, 2018
 ----------------------
 
diff --git a/st2api/tests/unit/controllers/v1/test_packs.py b/st2api/tests/unit/controllers/v1/test_packs.py
index b87f7c6415..30a319f036 100644
--- a/st2api/tests/unit/controllers/v1/test_packs.py
+++ b/st2api/tests/unit/controllers/v1/test_packs.py
@@ -180,8 +180,7 @@ def test_uninstall(self, _handle_schedule_execution):
 
     @mock.patch.object(pack_service, 'fetch_pack_index',
                        mock.MagicMock(return_value=(PACK_INDEX, {})))
-    def test_search(self):
-
+    def test_search_with_query(self):
         test_scenarios = [
             {
                 'input': {'query': 'test'},
@@ -234,11 +233,26 @@ def test_search(self):
                 'expected_result': []
             }
         ]
+
         for scenario in test_scenarios:
             resp = self.app.post_json('/v1/packs/index/search', scenario['input'])
             self.assertEqual(resp.status_int, scenario['expected_code'])
             self.assertEqual(resp.json, scenario['expected_result'])
 
+    @mock.patch.object(pack_service, 'get_pack_from_index',
+                       mock.MagicMock(return_value=PACK_INDEX['test']))
+    def test_search_with_pack_has_result(self):
+        resp = self.app.post_json('/v1/packs/index/search', {'pack': 'st2-dev'})
+        self.assertEqual(resp.status_int, 200)
+        self.assertEqual(resp.json, PACK_INDEX['test'])
+
+    @mock.patch.object(pack_service, 'get_pack_from_index',
+                       mock.MagicMock(return_value=None))
+    def test_search_with_pack_no_result(self):
+        resp = self.app.post_json('/v1/packs/index/search', {'pack': 'not-found'})
+        self.assertEqual(resp.status_int, 200)
+        self.assertEqual(resp.json, [])
+
     @mock.patch.object(pack_service, 'fetch_pack_index',
                        mock.MagicMock(return_value=(PACK_INDEX, {})))
     def test_show(self):
diff --git a/st2client/st2client/models/core.py b/st2client/st2client/models/core.py
index e50820cfbc..c3d47b1043 100644
--- a/st2client/st2client/models/core.py
+++ b/st2client/st2client/models/core.py
@@ -506,7 +506,7 @@ def search(self, args, ignore_errors=False, **kwargs):
         else:
             payload = {'pack': args.pack}
 
-            response = self.client.post(url, payload, **kwargs)
+        response = self.client.post(url, payload, **kwargs)
 
         if response.status_code != http_client.OK:
             if ignore_errors:

`st2 pack search` is broken in 2.7
EDIT: Added full debug output
```shell
root@EWC:~/.ssh# st2 --debug pack search clicrud
2018-04-13 17:12:20,760  DEBUG - Using cached token from file "/root/.st2/token-st2admin"
ERROR: local variable 'response' referenced before assignment

CLI settings:
----------------
Config file path: /root/.st2/config
Client settings:
----------------
ST2_BASE_URL: http://127.0.0.1
ST2_AUTH_URL: http://127.0.0.1:9100
ST2_API_URL: http://127.0.0.1:9101/v1
ST2_STREAM_URL: http://127.0.0.1:9102/v1
ST2_AUTH_TOKEN: e67164f509424e70a6244e97059cf76d

Proxy settings:
---------------
HTTP_PROXY:
HTTPS_PROXY:

Traceback (most recent call last):
  File "/opt/stackstorm/st2/local/lib/python2.7/site-packages/st2client/shell.py", line 343, in run
    func(args)
  File "/opt/stackstorm/st2/local/lib/python2.7/site-packages/st2client/commands/resource.py", line 259, in run_and_print
    instances = self.run(args, **kwargs)
  File "/opt/stackstorm/st2/local/lib/python2.7/site-packages/st2client/commands/resource.py", line 47, in decorate
    return func(*args, **kwargs)
  File "/opt/stackstorm/st2/local/lib/python2.7/site-packages/st2client/commands/pack.py", line 359, in run
    return self.manager.search(args, **kwargs)
  File "/opt/stackstorm/st2/local/lib/python2.7/site-packages/st2client/models/core.py", line 40, in decorate
    return func(*args, **kwargs)
  File "/opt/stackstorm/st2/local/lib/python2.7/site-packages/st2client/models/core.py", line 511, in search
    if response.status_code != http_client.OK:
UnboundLocalError: local variable 'response' referenced before assignment```
