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

''' Wrapper around keystoneauth Adapter to wrap calls in TaskManager '''

import functools
try:
    import simplejson
    JSONDecodeError = simplejson.scanner.JSONDecodeError
except ImportError:
    JSONDecodeError = ValueError

from six.moves import urllib

from keystoneauth1 import adapter

from openstack import exceptions
from openstack import task_manager as _task_manager


def _extract_name(url, service_type=None):
    '''Produce a key name to use in logging/metrics from the URL path.

    We want to be able to logic/metric sane general things, so we pull
    the url apart to generate names. The function returns a list because
    there are two different ways in which the elements want to be combined
    below (one for logging, one for statsd)

    Some examples are likely useful:

    /servers -> ['servers']
    /servers/{id} -> ['servers']
    /servers/{id}/os-security-groups -> ['servers', 'os-security-groups']
    /v2.0/networks.json -> ['networks']
    '''

    url_path = urllib.parse.urlparse(url).path.strip()
    # Remove / from the beginning to keep the list indexes of interesting
    # things consistent
    if url_path.startswith('/'):
        url_path = url_path[1:]

    # Special case for neutron, which puts .json on the end of urls
    if url_path.endswith('.json'):
        url_path = url_path[:-len('.json')]

    url_parts = url_path.split('/')
    if url_parts[-1] == 'detail':
        # Special case detail calls
        # GET /servers/detail
        # returns ['servers', 'detail']
        name_parts = url_parts[-2:]
    else:
        # Strip leading version piece so that
        # GET /v2.0/networks
        # returns ['networks']
        if url_parts[0] in ('v1', 'v2', 'v2.0'):
            url_parts = url_parts[1:]
        name_parts = []
        # Pull out every other URL portion - so that
        # GET /servers/{id}/os-security-groups
        # returns ['servers', 'os-security-groups']
        for idx in range(0, len(url_parts)):
            if not idx % 2 and url_parts[idx]:
                name_parts.append(url_parts[idx])

    # Keystone Token fetching is a special case, so we name it "tokens"
    if url_path.endswith('tokens'):
        name_parts = ['tokens']

    # Getting the root of an endpoint is doing version discovery
    if not name_parts:
        if service_type == 'object-store':
            name_parts = ['account']
        else:
            name_parts = ['discovery']

    # Strip out anything that's empty or None
    return [part for part in name_parts if part]


def _json_response(response, result_key=None, error_message=None):
    """Temporary method to use to bridge from ShadeAdapter to SDK calls."""
    exceptions.raise_from_response(response, error_message=error_message)

    if not response.content:
        # This doesn't have any content
        return response

    # Some REST calls do not return json content. Don't decode it.
    if 'application/json' not in response.headers.get('Content-Type'):
        return response

    try:
        result_json = response.json()
    except JSONDecodeError:
        return response
    return result_json


class OpenStackSDKAdapter(adapter.Adapter):
    """Wrapper around keystoneauth1.adapter.Adapter.

    Uses task_manager to run tasks rather than executing them directly.
    This allows using the nodepool MultiThreaded Rate Limiting TaskManager.
    """

    def __init__(self, session=None, task_manager=None, *args, **kwargs):
        super(OpenStackSDKAdapter, self).__init__(
            session=session, *args, **kwargs)
        if not task_manager:
            task_manager = _task_manager.TaskManager(name=self.service_type)

        self.task_manager = task_manager

    def request(
            self, url, method, run_async=False, error_message=None,
            raise_exc=False, connect_retries=1, *args, **kwargs):
        name_parts = _extract_name(url, self.service_type)
        # TODO(mordred) This if is in service of unit tests that are making
        # calls without a service_type. It should be fixable once we shift
        # to requests-mock and stop mocking internals.
        if self.service_type:
            name = '.'.join([self.service_type, method] + name_parts)
        else:
            name = '.'.join([method] + name_parts)

        request_method = functools.partial(
            super(OpenStackSDKAdapter, self).request, url, method)

        return self.task_manager.submit_function(
            request_method, run_async=run_async, name=name,
            connect_retries=connect_retries, raise_exc=raise_exc,
            **kwargs)

    def _version_matches(self, version):
        api_version = self.get_api_major_version()
        if api_version:
            return api_version[0] == version
        return False


class ShadeAdapter(OpenStackSDKAdapter):
    """Wrapper for shade methods that expect json unpacking."""

    def request(self, url, method,
                run_async=False, error_message=None, **kwargs):
        response = super(ShadeAdapter, self).request(
            url, method, run_async=run_async, **kwargs)
        if run_async:
            return response
        else:
            return _json_response(response, error_message=error_message)
