# Copyright 2012 OpenStack Foundation.
# 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 logging
import os

import debtcollector.renames
from keystoneauth1 import access
from keystoneauth1 import adapter
from oslo_serialization import jsonutils
from oslo_utils import importutils
import requests

from neutronclient._i18n import _
from neutronclient.common import exceptions
from neutronclient.common import utils

osprofiler_web = importutils.try_import("osprofiler.web")

_logger = logging.getLogger(__name__)

if os.environ.get('NEUTRONCLIENT_DEBUG'):
    ch = logging.StreamHandler()
    _logger.setLevel(logging.DEBUG)
    _logger.addHandler(ch)
    _requests_log_level = logging.DEBUG
else:
    _requests_log_level = logging.WARNING

logging.getLogger("requests").setLevel(_requests_log_level)
MAX_URI_LEN = 8192
USER_AGENT = 'python-neutronclient'
REQ_ID_HEADER = 'X-OpenStack-Request-ID'


class HTTPClient(object):
    """Handles the REST calls and responses, include authn."""

    CONTENT_TYPE = 'application/json'

    @debtcollector.renames.renamed_kwarg(
        'tenant_id', 'project_id', replace=True)
    @debtcollector.renames.renamed_kwarg(
        'tenant_name', 'project_name', replace=True)
    def __init__(self, username=None, user_id=None,
                 project_name=None, project_id=None,
                 password=None, auth_url=None,
                 token=None, region_name=None, timeout=None,
                 endpoint_url=None, insecure=False,
                 endpoint_type='publicURL',
                 auth_strategy='keystone', ca_cert=None, cert=None,
                 log_credentials=False, service_type='network',
                 global_request_id=None, **kwargs):

        self.username = username
        self.user_id = user_id
        self.project_name = project_name
        self.project_id = project_id
        self.password = password
        self.auth_url = auth_url.rstrip('/') if auth_url else None
        self.service_type = service_type
        self.endpoint_type = endpoint_type
        self.region_name = region_name
        self.timeout = timeout
        self.auth_token = token
        self.auth_tenant_id = None
        self.auth_user_id = None
        self.endpoint_url = endpoint_url
        self.auth_strategy = auth_strategy
        self.log_credentials = log_credentials
        self.global_request_id = global_request_id
        self.cert = cert
        if insecure:
            self.verify_cert = False
        else:
            self.verify_cert = ca_cert if ca_cert else True

    def _cs_request(self, *args, **kwargs):
        kargs = {}
        kargs.setdefault('headers', kwargs.get('headers', {}))
        kargs['headers']['User-Agent'] = USER_AGENT

        if 'body' in kwargs:
            kargs['body'] = kwargs['body']

        if self.log_credentials:
            log_kargs = kargs
        else:
            log_kargs = self._strip_credentials(kargs)

        utils.http_log_req(_logger, args, log_kargs)
        try:
            resp, body = self.request(*args, **kargs)
        except requests.exceptions.SSLError as e:
            raise exceptions.SslCertificateValidationError(reason=str(e))
        except Exception as e:
            # Wrap the low-level connection error (socket timeout, redirect
            # limit, decompression error, etc) into our custom high-level
            # connection exception (it is excepted in the upper layers of code)
            _logger.debug("throwing ConnectionFailed : %s", e)
            raise exceptions.ConnectionFailed(reason=str(e))
        utils.http_log_resp(_logger, resp, body)

        # log request-id for each api call
        request_id = resp.headers.get('x-openstack-request-id')
        if request_id:
            _logger.debug('%(method)s call to neutron for '
                          '%(url)s used request id '
                          '%(response_request_id)s',
                          {'method': resp.request.method,
                           'url': resp.url,
                           'response_request_id': request_id})

        if resp.status_code == 401:
            raise exceptions.Unauthorized(message=body)
        return resp, body

    def _strip_credentials(self, kwargs):
        if kwargs.get('body') and self.password:
            log_kwargs = kwargs.copy()
            log_kwargs['body'] = kwargs['body'].replace(self.password,
                                                        'REDACTED')
            return log_kwargs
        else:
            return kwargs

    def authenticate_and_fetch_endpoint_url(self):
        if not self.auth_token:
            self.authenticate()
        elif not self.endpoint_url:
            self.endpoint_url = self._get_endpoint_url()

    def request(self, url, method, body=None, headers=None, **kwargs):
        """Request without authentication."""

        content_type = kwargs.pop('content_type', None) or 'application/json'
        headers = headers or {}
        headers.setdefault('Accept', content_type)

        if body:
            headers.setdefault('Content-Type', content_type)

        if self.global_request_id:
            headers.setdefault(REQ_ID_HEADER, self.global_request_id)

        headers['User-Agent'] = USER_AGENT
        # NOTE(dbelova): osprofiler_web.get_trace_id_headers does not add any
        # headers in case if osprofiler is not initialized.
        if osprofiler_web:
            headers.update(osprofiler_web.get_trace_id_headers())

        resp = requests.request(
            method,
            url,
            data=body,
            headers=headers,
            verify=self.verify_cert,
            cert=self.cert,
            timeout=self.timeout,
            **kwargs)

        return resp, resp.text

    def _check_uri_length(self, action):
        uri_len = len(self.endpoint_url) + len(action)
        if uri_len > MAX_URI_LEN:
            raise exceptions.RequestURITooLong(
                excess=uri_len - MAX_URI_LEN)

    def do_request(self, url, method, **kwargs):
        # Ensure client always has correct uri - do not guesstimate anything
        self.authenticate_and_fetch_endpoint_url()
        self._check_uri_length(url)

        # Perform the request once. If we get a 401 back then it
        # might be because the auth token expired, so try to
        # re-authenticate and try again. If it still fails, bail.
        try:
            kwargs['headers'] = kwargs.get('headers') or {}
            if self.auth_token is None:
                self.auth_token = ""
            kwargs['headers']['X-Auth-Token'] = self.auth_token
            resp, body = self._cs_request(self.endpoint_url + url, method,
                                          **kwargs)
            return resp, body
        except exceptions.Unauthorized:
            self.authenticate()
            kwargs['headers'] = kwargs.get('headers') or {}
            kwargs['headers']['X-Auth-Token'] = self.auth_token
            resp, body = self._cs_request(
                self.endpoint_url + url, method, **kwargs)
            return resp, body

    def _extract_service_catalog(self, body):
        """Set the client's service catalog from the response data."""
        self.auth_ref = access.create(body=body)
        self.service_catalog = self.auth_ref.service_catalog
        self.auth_token = self.auth_ref.auth_token
        self.auth_tenant_id = self.auth_ref.tenant_id
        self.auth_user_id = self.auth_ref.user_id

        if not self.endpoint_url:
            self.endpoint_url = self.service_catalog.url_for(
                region_name=self.region_name,
                service_type=self.service_type,
                interface=self.endpoint_type)

    def _authenticate_keystone(self):
        if self.user_id:
            creds = {'userId': self.user_id,
                     'password': self.password}
        else:
            creds = {'username': self.username,
                     'password': self.password}

        if self.project_id:
            body = {'auth': {'passwordCredentials': creds,
                             'tenantId': self.project_id, }, }
        else:
            body = {'auth': {'passwordCredentials': creds,
                             'tenantName': self.project_name, }, }

        if self.auth_url is None:
            raise exceptions.NoAuthURLProvided()

        token_url = self.auth_url + "/tokens"
        resp, resp_body = self._cs_request(token_url, "POST",
                                           body=jsonutils.dumps(body),
                                           content_type="application/json",
                                           allow_redirects=True)
        if resp.status_code != 200:
            raise exceptions.Unauthorized(message=resp_body)
        if resp_body:
            try:
                resp_body = jsonutils.loads(resp_body)
            except ValueError:
                pass
        else:
            resp_body = None
        self._extract_service_catalog(resp_body)

    def _authenticate_noauth(self):
        if not self.endpoint_url:
            message = _('For "noauth" authentication strategy, the endpoint '
                        'must be specified either in the constructor or '
                        'using --os-url')
            raise exceptions.Unauthorized(message=message)

    def authenticate(self):
        if self.auth_strategy == 'keystone':
            self._authenticate_keystone()
        elif self.auth_strategy == 'noauth':
            self._authenticate_noauth()
        else:
            err_msg = _('Unknown auth strategy: %s') % self.auth_strategy
            raise exceptions.Unauthorized(message=err_msg)

    def _get_endpoint_url(self):
        if self.auth_url is None:
            raise exceptions.NoAuthURLProvided()

        url = self.auth_url + '/tokens/%s/endpoints' % self.auth_token
        try:
            resp, body = self._cs_request(url, "GET")
        except exceptions.Unauthorized:
            # rollback to authenticate() to handle case when neutron client
            # is initialized just before the token is expired
            self.authenticate()
            return self.endpoint_url

        body = jsonutils.loads(body)
        for endpoint in body.get('endpoints', []):
            if (endpoint['type'] == 'network' and
                    endpoint.get('region') == self.region_name):
                if self.endpoint_type not in endpoint:
                    raise exceptions.EndpointTypeNotFound(
                        type_=self.endpoint_type)
                return endpoint[self.endpoint_type]

        raise exceptions.EndpointNotFound()

    def get_auth_info(self):
        return {'auth_token': self.auth_token,
                'auth_tenant_id': self.auth_tenant_id,
                'auth_user_id': self.auth_user_id,
                'endpoint_url': self.endpoint_url}

    def get_auth_ref(self):
        return getattr(self, 'auth_ref', None)


class SessionClient(adapter.Adapter):

    def request(self, *args, **kwargs):
        kwargs.setdefault('authenticated', False)
        kwargs.setdefault('raise_exc', False)

        content_type = kwargs.pop('content_type', None) or 'application/json'

        headers = kwargs.get('headers') or {}
        headers.setdefault('Accept', content_type)

        # NOTE(dbelova): osprofiler_web.get_trace_id_headers does not add any
        # headers in case if osprofiler is not initialized.
        if osprofiler_web:
            headers.update(osprofiler_web.get_trace_id_headers())

        try:
            kwargs.setdefault('data', kwargs.pop('body'))
        except KeyError:
            pass

        if kwargs.get('data'):
            headers.setdefault('Content-Type', content_type)

        kwargs['headers'] = headers
        resp = super(SessionClient, self).request(*args, **kwargs)
        return resp, resp.text

    def _check_uri_length(self, url):
        uri_len = len(self.endpoint_url) + len(url)
        if uri_len > MAX_URI_LEN:
            raise exceptions.RequestURITooLong(
                excess=uri_len - MAX_URI_LEN)

    def do_request(self, url, method, **kwargs):
        kwargs.setdefault('authenticated', True)
        self._check_uri_length(url)
        return self.request(url, method, **kwargs)

    @property
    def endpoint_url(self):
        # NOTE(jamielennox): This is used purely by the CLI and should be
        # removed when the CLI gets smarter.
        return self.get_endpoint()

    @property
    def auth_token(self):
        # NOTE(jamielennox): This is used purely by the CLI and should be
        # removed when the CLI gets smarter.
        return self.get_token()

    def authenticate(self):
        # NOTE(jamielennox): This is used purely by the CLI and should be
        # removed when the CLI gets smarter.
        self.get_token()

    def get_auth_info(self):
        auth_info = {'auth_token': self.auth_token,
                     'endpoint_url': self.endpoint_url}

        # NOTE(jamielennox): This is the best we can do here. It will work
        # with identity plugins which is the primary case but we should
        # deprecate it's usage as much as possible.
        try:
            get_access = (self.auth or self.session.auth).get_access
        except AttributeError:
            pass
        else:
            auth_ref = get_access(self.session)

            auth_info['auth_tenant_id'] = auth_ref.project_id
            auth_info['auth_user_id'] = auth_ref.user_id

        return auth_info

    def get_auth_ref(self):
        return self.session.auth.get_auth_ref(self.session)


# FIXME(bklei): Should refactor this to use kwargs and only
# explicitly list arguments that are not None.
@debtcollector.renames.renamed_kwarg('tenant_id', 'project_id', replace=True)
@debtcollector.renames.renamed_kwarg(
    'tenant_name', 'project_name', replace=True)
def construct_http_client(username=None,
                          user_id=None,
                          project_name=None,
                          project_id=None,
                          password=None,
                          auth_url=None,
                          token=None,
                          region_name=None,
                          timeout=None,
                          endpoint_url=None,
                          insecure=False,
                          endpoint_type='public',
                          log_credentials=None,
                          auth_strategy='keystone',
                          ca_cert=None,
                          cert=None,
                          service_type='network',
                          session=None,
                          global_request_id=None,
                          **kwargs):

    if session:
        kwargs.setdefault('user_agent', USER_AGENT)
        kwargs.setdefault('interface', endpoint_type)
        return SessionClient(session=session,
                             service_type=service_type,
                             region_name=region_name,
                             global_request_id=global_request_id,
                             **kwargs)
    else:
        # FIXME(bklei): username and password are now optional. Need
        # to test that they were provided in this mode.  Should also
        # refactor to use kwargs.
        return HTTPClient(username=username,
                          password=password,
                          project_id=project_id,
                          project_name=project_name,
                          user_id=user_id,
                          auth_url=auth_url,
                          token=token,
                          endpoint_url=endpoint_url,
                          insecure=insecure,
                          timeout=timeout,
                          region_name=region_name,
                          endpoint_type=endpoint_type,
                          service_type=service_type,
                          ca_cert=ca_cert,
                          cert=cert,
                          log_credentials=log_credentials,
                          auth_strategy=auth_strategy,
                          global_request_id=global_request_id)
