# Copyright (c) 2015 Uber Technologies, Inc.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.

from __future__ import absolute_import

from future import standard_library
standard_library.install_aliases()
from builtins import object
import logging
import urllib.parse
import opentracing
import six
from opentracing import Format
from opentracing.ext import tags
from opentracing_instrumentation import config


def before_request(request, tracer=None):
    """
    Attempts to extract a tracing span from incoming request.
    If no tracing context is passed in the headers, or the data
    cannot be parsed, a new root span is started.

    :param request: HTTP request with `.headers` property exposed
        that satisfies a regular dictionary interface
    :param tracer: optional tracer instance to use. If not specified
        the global opentracing.tracer will be used.
    :return: returns a new, already started span.
    """
    if tracer is None:
        tracer = opentracing.tracer

    # we need to prepare tags upfront, mainly because RPC_SERVER tag must be
    # set when starting the span, to support Zipkin's one-span-per-RPC model
    tags_dict = {
        tags.SPAN_KIND: tags.SPAN_KIND_RPC_SERVER,
        tags.HTTP_URL: request.full_url,
        tags.HTTP_METHOD: request.method,
    }

    remote_ip = request.remote_ip
    if remote_ip:
        tags_dict[tags.PEER_HOST_IPV4] = remote_ip

    caller_name = request.caller_name
    if caller_name:
        tags_dict[tags.PEER_SERVICE] = caller_name

    remote_port = request.remote_port
    if remote_port:
        tags_dict[tags.PEER_PORT] = remote_port

    operation = request.operation
    try:
        carrier = {}
        for key, value in six.iteritems(request.headers):
            carrier[key] = value
        parent_ctx = tracer.extract(
            format=Format.HTTP_HEADERS, carrier=carrier
        )
    except Exception as e:
        logging.exception('trace extract failed: %s' % e)
        parent_ctx = None

    span = tracer.start_span(
        operation_name=operation,
        child_of=parent_ctx,
        tags=tags_dict)

    return span


class AbstractRequestWrapper(object):
    """
    Exposes several properties used by the tracing methods.
    """

    @property
    def caller_name(self):
        for header in config.CONFIG.caller_name_headers:
            caller = self.headers.get(header.lower(), None)
            if caller is not None:
                return caller
        return None

    @property
    def full_url(self):
        raise NotImplementedError('full_url')

    @property
    def headers(self):
        raise NotImplementedError('headers')

    @property
    def method(self):
        raise NotImplementedError('method')

    @property
    def remote_ip(self):
        raise NotImplementedError('remote_ip')

    @property
    def remote_port(self):
        return None

    @property
    def server_port(self):
        return None

    @property
    def operation(self):
        return self.method


class TornadoRequestWrapper(AbstractRequestWrapper):
    """
    Wraps tornado.httputils.HTTPServerRequest and exposes several properties
    used by the tracing methods.
    """

    def __init__(self, request):
        self.request = request

    @property
    def full_url(self):
        return self.request.full_url()

    @property
    def headers(self):
        return self.request.headers

    @property
    def method(self):
        return self.request.method

    @property
    def remote_ip(self):
        return self.request.remote_ip


class WSGIRequestWrapper(AbstractRequestWrapper):
    """
    Wraps WSGI environment and exposes several properties
    used by the tracing methods.
    """

    def __init__(self, wsgi_environ, headers):
        self.wsgi_environ = wsgi_environ
        self._headers = headers

    @classmethod
    def from_wsgi_environ(cls, wsgi_environ):
        instance = cls(wsgi_environ=wsgi_environ,
                       headers=cls._parse_wsgi_headers(wsgi_environ))
        return instance

    @staticmethod
    def _parse_wsgi_headers(wsgi_environ):
        """
        HTTP headers are presented in WSGI environment with 'HTTP_' prefix.
        This method finds those headers, removes the prefix, converts
        underscores to dashes, and converts to lower case.

        :param wsgi_environ:
        :return: returns a dictionary of headers
        """
        prefix = 'HTTP_'
        p_len = len(prefix)
        # use .items() despite suspected memory pressure bc GC occasionally
        # collects wsgi_environ.iteritems() during iteration.
        headers = {
            key[p_len:].replace('_', '-').lower():
                val for (key, val) in wsgi_environ.items()
            if key.startswith(prefix)}
        return headers

    @property
    def full_url(self):
        """
        Taken from
        http://legacy.python.org/dev/peps/pep-3333/#url-reconstruction

        :return: Reconstructed URL from WSGI environment.
        """
        environ = self.wsgi_environ
        url = environ['wsgi.url_scheme'] + '://'

        if environ.get('HTTP_HOST'):
            url += environ['HTTP_HOST']
        else:
            url += environ['SERVER_NAME']

            if environ['wsgi.url_scheme'] == 'https':
                if environ['SERVER_PORT'] != '443':
                    url += ':' + environ['SERVER_PORT']
            else:
                if environ['SERVER_PORT'] != '80':
                    url += ':' + environ['SERVER_PORT']

        url += urllib.parse.quote(environ.get('SCRIPT_NAME', ''))
        url += urllib.parse.quote(environ.get('PATH_INFO', ''))
        if environ.get('QUERY_STRING'):
            url += '?' + environ['QUERY_STRING']
        return url

    @property
    def headers(self):
        return self._headers

    @property
    def method(self):
        return self.wsgi_environ.get('REQUEST_METHOD')

    @property
    def remote_ip(self):
        return self.wsgi_environ.get('REMOTE_ADDR', None)

    @property
    def remote_port(self):
        return self.wsgi_environ.get('REMOTE_PORT', None)

    @property
    def server_port(self):
        return self.wsgi_environ.get('SERVER_PORT', None)
