# Copyright 2015 VMware, Inc.
# All Rights Reserved
#
#    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 re
import time
from urllib import parse as urlparse

from oslo_log import log
from oslo_serialization import jsonutils
import requests

from vmware_nsxlib._i18n import _
from vmware_nsxlib.v3 import exceptions
from vmware_nsxlib.v3 import utils

LOG = log.getLogger(__name__)

NULL_CURSOR_PREFIX = '0000'


def get_http_error_details(response):
    msg = response.json() if response.content else ''
    error_code = None
    related_error_codes = []
    related_status_codes = []

    if isinstance(msg, dict) and 'error_message' in msg:
        error_code = msg.get('error_code')
        related_errors = [error['error_message'] for error in
                          msg.get('related_errors', [])]
        related_error_codes = [str(error['error_code']) for error in
                               msg.get('related_errors', []) if
                               error.get('error_code')]
        related_status_codes = [getattr(requests.codes, error['httpStatus'])
                                for error in msg.get('related_errors', []) if
                                error.get('httpStatus')]

        msg = msg['error_message']
        if related_errors:
            msg += " relatedErrors: %s" % ' '.join(related_errors)

    return {'status_code': response.status_code,
            'error_code': error_code,
            'related_error_codes': related_error_codes,
            'related_status_codes': related_status_codes,
            'details': msg}


def init_http_exception_from_response(response):
    if response is None or response:
        # The response object has a __bool__ method that return True for
        # status code under 400. In that case there is no need for exception
        return None

    error_details = get_http_error_details(response)
    if not error_details['error_code']:
        return None

    error = http_error_to_exception(error_details['status_code'],
                                    error_details['error_code'],
                                    error_details['related_error_codes'])

    return error(manager='', **error_details)


def http_error_to_exception(status_code, error_code, related_error_codes=None):
    errors = {
        requests.codes.NOT_FOUND:
            {'202': exceptions.BackendResourceNotFound,
             '500090': exceptions.StaleRevision,
             'default': exceptions.ResourceNotFound},
        requests.codes.BAD_REQUEST:
            {'60508': exceptions.NsxIndexingInProgress,
             '60514': exceptions.NsxSearchTimeout,
             '60515': exceptions.NsxSearchOutOfSync,
             '8327': exceptions.NsxOverlapVlan,
             '500045': exceptions.NsxPendingDelete,
             '500030': exceptions.ResourceInUse,
             '500087': exceptions.StaleRevision,
             '500105': exceptions.NsxOverlapAddresses,
             '500232': exceptions.StaleRevision,  # Missing dependent objects
             '503040': exceptions.NsxSegemntWithVM,
             '100148': exceptions.StaleRevision,
             '500012': exceptions.NsxInvalidPath},
        requests.codes.CONFLICT: exceptions.StaleRevision,
        requests.codes.PRECONDITION_FAILED: exceptions.StaleRevision,
        requests.codes.INTERNAL_SERVER_ERROR:
            {'98': exceptions.CannotConnectToServer,
             '99': exceptions.ClientCertificateNotTrusted,
             '607': exceptions.APITransactionAborted,
             '610': exceptions.APITransactionAborted},
        requests.codes.FORBIDDEN:
            {'98': exceptions.BadXSRFToken,
             '403': exceptions.InvalidCredentials,
             '505': exceptions.InvalidLicense},
        requests.codes.TOO_MANY_REQUESTS: exceptions.TooManyRequests,
        requests.codes.SERVICE_UNAVAILABLE: exceptions.ServiceUnavailable}

    if status_code in errors:
        if isinstance(errors[status_code], dict):
            # choose based on error code
            if error_code and str(error_code) in errors[status_code]:
                return errors[status_code][str(error_code)]
            # try the related errors
            if related_error_codes:
                for err in related_error_codes:
                    if err and str(err) in errors[status_code]:
                        return errors[status_code][str(err)]

            if 'default' in errors[status_code]:
                return errors[status_code]['default']
        else:
            return errors[status_code]

    # default exception
    return exceptions.ManagerError


class RESTClient(object):

    _VERB_RESP_CODES = {
        'get': [requests.codes.ok],
        'post': [requests.codes.created, requests.codes.ok],
        'put': [requests.codes.created, requests.codes.ok],
        'patch': [requests.codes.created, requests.codes.ok],
        'delete': [requests.codes.ok]
    }

    def __init__(self, connection, url_prefix=None,
                 default_headers=None,
                 client_obj=None):
        self._conn = connection
        self._url_prefix = url_prefix or ""
        self._default_headers = default_headers or {}

    def new_client_for(self, *uri_segments):
        uri = self._build_url('/'.join(uri_segments))

        return self.__class__(
            self._conn,
            url_prefix=uri,
            default_headers=self._default_headers,
            client_obj=self)

    def list(self, resource='', headers=None, silent=False):
        return self.url_list(resource, headers=headers, silent=silent)

    def get(self, uuid, headers=None, silent=False, with_retries=False):
        return self.url_get(uuid, headers=headers, silent=silent,
                            with_retries=with_retries)

    def delete(self, uuid, headers=None, expected_results=None):
        return self.url_delete(uuid, headers=headers,
                               expected_results=expected_results)

    def update(self, uuid, body=None, headers=None, expected_results=None):
        return self.url_put(uuid, body, headers=headers,
                            expected_results=expected_results)

    def create(self, resource='', body=None, headers=None,
               expected_results=None):
        return self.url_post(resource, body, headers=headers,
                             expected_results=expected_results)

    def patch(self, resource='', body=None, headers=None):
        return self.url_patch(resource, body, headers=headers)

    def url_list(self, url, headers=None, silent=False):
        concatenate_response = self.url_get(url, headers=headers,
                                            silent=silent)
        cursor = concatenate_response.get('cursor', NULL_CURSOR_PREFIX)
        op = '&' if urlparse.urlparse(url).query else '?'
        url += op + 'cursor='

        while cursor and not cursor.startswith(NULL_CURSOR_PREFIX):
            page = self.url_get(url + cursor, headers=headers, silent=silent)
            concatenate_response['results'].extend(page.get('results', []))
            cursor = page.get('cursor', NULL_CURSOR_PREFIX)
        return concatenate_response

    def url_get(self, url, headers=None, silent=False, with_retries=False):
        return self._rest_call(url, method='GET', headers=headers,
                               silent=silent, with_retries=with_retries)

    def url_delete(self, url, headers=None, expected_results=None):
        return self._rest_call(url, method='DELETE', headers=headers,
                               expected_results=expected_results)

    def url_put(self, url, body, headers=None, expected_results=None):
        return self._rest_call(url, method='PUT', body=body, headers=headers,
                               expected_results=expected_results)

    def url_post(self, url, body, headers=None, expected_results=None):
        return self._rest_call(url, method='POST', body=body, headers=headers,
                               expected_results=expected_results)

    def url_patch(self, url, body, headers=None):
        return self._rest_call(url, method='PATCH', body=body, headers=headers)

    def _raise_error(self, operation, status_code, details,
                     error_code=None, related_error_codes=None,
                     related_status_codes=None):
        error = http_error_to_exception(status_code, error_code,
                                        related_error_codes)
        raise error(manager='', operation=operation, details=details,
                    error_code=error_code,
                    related_error_codes=related_error_codes,
                    status_code=status_code,
                    related_status_codes=related_status_codes)

    def _validate_result(self, result, expected, operation, silent=False):
        if result.status_code not in expected:
            result_msg = result.json() if result.content else ''
            if not silent:
                LOG.warning("The HTTP request returned error code "
                            "%(result)s, whereas %(expected)s response "
                            "codes were expected. Response body %(body)s",
                            {'result': result.status_code,
                             'expected': '/'.join([str(code)
                                                   for code in expected]),
                             'body': result_msg})

            error_details = get_http_error_details(result)

            self._raise_error(operation, **error_details)

    @classmethod
    def merge_headers(cls, *headers):
        merged = {}
        for header in headers:
            if header:
                merged.update(header)
        return merged

    def _build_url(self, uri):
        prefix = urlparse.urlparse(self._url_prefix)
        uri = ("/%s/%s" % (prefix.path, uri)).replace('//', '/').strip('/')
        if prefix.netloc:
            uri = "%s/%s" % (prefix.netloc, uri)
        if prefix.scheme:
            uri = "%s://%s" % (prefix.scheme, uri)
        return uri

    def _mask_password(self, json):
        '''Mask password value in json format'''
        if not json:
            return json

        pattern = r'\"password\": [^,}]*'
        return re.sub(pattern, '"password": "********"', json)

    def _rest_call(self, url, method='GET', body=None, headers=None,
                   silent=False, expected_results=None, **kwargs):
        request_headers = headers.copy() if headers else {}
        request_headers.update(self._default_headers)

        if utils.INJECT_HEADERS_CALLBACK:
            inject_headers = utils.INJECT_HEADERS_CALLBACK()
            request_headers.update(inject_headers)

        request_url = self._build_url(url)
        do_request = getattr(self._conn, method.lower())
        if silent:
            self._conn.set_silent(True)
        ts = time.time()
        result = do_request(
            request_url,
            data=body,
            headers=request_headers)
        te = time.time()
        if silent:
            self._conn.set_silent(False)

        if not silent:
            LOG.debug("[%x] REST call: %s %s. Headers: %s. Body: %s. "
                      "Response: %s. Took %2.4f",
                      id(self._conn), method, request_url,
                      utils.censor_headers(request_headers),
                      self._mask_password(body),
                      result.json() if result.content else '',
                      te - ts)

        if not expected_results:
            expected_results = RESTClient._VERB_RESP_CODES[method.lower()]
        self._validate_result(
            result, expected_results,
            _("%(verb)s %(url)s") % {'verb': method, 'url': request_url},
            silent=silent)
        return result


class JSONRESTClient(RESTClient):

    _DEFAULT_HEADERS = {
        'Accept': 'application/json',
        'Content-Type': 'application/json'
    }

    def __init__(self, connection, url_prefix=None,
                 default_headers=None,
                 client_obj=None):

        super(JSONRESTClient, self).__init__(
            connection,
            url_prefix=url_prefix,
            default_headers=RESTClient.merge_headers(
                JSONRESTClient._DEFAULT_HEADERS, default_headers),
            client_obj=None)

    def _rest_call(self, *args, **kwargs):
        if kwargs.get('body') is not None:
            kwargs['body'] = jsonutils.dumps(kwargs['body'], sort_keys=True)
        result = super(JSONRESTClient, self)._rest_call(*args, **kwargs)
        return result.json() if result.content else result


class NSX3Client(JSONRESTClient):

    NSX_V1_API_PREFIX = 'api/v1/'
    NSX_POLICY_V1_API_PREFIX = 'policy/api/v1/'

    # NOTE: For user-facing client, NsxClusteredAPI instance
    # will be passed as connection parameter below, thus all
    # requests on this client will pass via cluster code to
    # determine endpoint
    # For validation client, TimeoutSession with specific
    # endpoint parameters will be passed as connection.
    def __init__(self, connection, url_prefix=None,
                 default_headers=None,
                 nsx_api_managers=None,
                 max_attempts=utils.DEFAULT_MAX_ATTEMPTS,
                 rate_limit_retry=True,
                 client_obj=None,
                 url_path_base=NSX_V1_API_PREFIX):

        # If the client obj is defined - copy configuration from it
        if client_obj:
            self.nsx_api_managers = client_obj.nsx_api_managers or []
            self.max_attempts = client_obj.max_attempts
            self.rate_limit_retry = client_obj.rate_limit_retry
        else:
            self.nsx_api_managers = nsx_api_managers or []
            self.max_attempts = max_attempts
            self.rate_limit_retry = rate_limit_retry

        url_prefix = url_prefix or url_path_base
        if url_prefix and url_path_base not in url_prefix:
            if url_prefix.startswith('http'):
                url_prefix += '/' + url_path_base
            else:
                url_prefix = "%s/%s" % (url_path_base,
                                        url_prefix or '')

        super(NSX3Client, self).__init__(
            connection, url_prefix=url_prefix,
            default_headers=default_headers,
            client_obj=client_obj)

    def _raise_error(self, operation, status_code, details,
                     error_code=None, related_error_codes=None,
                     related_status_codes=None):
        """Override the Rest client errors to add the manager IPs"""
        error = http_error_to_exception(status_code, error_code,
                                        related_error_codes)
        raise error(manager=self.nsx_api_managers,
                    operation=operation,
                    details=details,
                    error_code=error_code,
                    related_error_codes=related_error_codes,
                    related_status_codes=related_status_codes,
                    status_code=status_code)

    def _rest_call(self, url, **kwargs):
        if 'with_retries' in kwargs and kwargs['with_retries']:
            LOG.warning("with_retries setting is deprecated and will be "
                        "removed. Please use exceptions setting in nsxlib "
                        "config instead")

        return super(NSX3Client, self)._rest_call(url, **kwargs)
