# Copyright 2010 New Relic, 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.

import functools
import logging
import time
import warnings

import urllib.parse as urlparse

from newrelic.api.application import Application, application_instance
from newrelic.api.transaction import Transaction, current_transaction
from newrelic.common.async_proxy import TransactionContext, async_proxy
from newrelic.common.encoding_utils import (
    decode_newrelic_header,
    ensure_str,
    json_encode,
    obfuscate,
)
from newrelic.common.object_names import callable_name
from newrelic.common.object_wrapper import FunctionWrapper, wrap_object
from newrelic.core.attribute_filter import DST_BROWSER_MONITORING

_logger = logging.getLogger(__name__)

_js_agent_header_fragment = '<script type="text/javascript"%s>window.NREUM||(NREUM={});NREUM.info=%s;\n%s</script>'

# Seconds since epoch for Jan 1 2000
JAN_1_2000 = time.mktime((2000, 1, 1, 0, 0, 0, 0, 0, 0))
MICROSECOND_MIN = JAN_1_2000 * 1000000.0
MILLISECOND_MIN = JAN_1_2000 * 1000.0


def _parse_time_stamp(time_stamp):
    """
    Converts time_stamp to seconds. Input can be microseconds,
    milliseconds or seconds

    Divide the timestamp by the highest resolution divisor. If
    the result is older than Jan 1 2000, then pick a lower
    resolution divisor and repeat.  It is safe to assume no
    requests were queued for more than 10 years.

    """

    now = time.time()

    if time_stamp > MICROSECOND_MIN:
        divisor = 1000000.0
    elif time_stamp > MILLISECOND_MIN:
        divisor = 1000.0
    elif time_stamp > JAN_1_2000:
        divisor = 1.0
    else:
        return 0.0

    converted_time = time_stamp / divisor

    # If queue_start is in the future, return 0.0.
    if converted_time > now:
        return 0.0

    return converted_time


TRUE_VALUES = {"on", "true", "1"}
FALSE_VALUES = {"off", "false", "0"}


def _lookup_environ_setting(environ, name, default=False):
    if name not in environ:
        return default

    flag = environ[name]

    if isinstance(flag, str):
        flag = flag.lower()

        if flag in TRUE_VALUES:
            return True
        elif flag in FALSE_VALUES:
            return False

    return flag


def _parse_synthetics_header(header):
    # Return a dictionary of values from Synthetics header
    # Returns empty dict, if version is not supported.

    synthetics = {}
    version = None

    try:
        if len(header) > 0:
            version = int(header[0])

        if version == 1:
            synthetics["version"] = version
            synthetics["account_id"] = int(header[1])
            synthetics["resource_id"] = header[2]
            synthetics["job_id"] = header[3]
            synthetics["monitor_id"] = header[4]
    except Exception:
        return

    return synthetics


def _parse_synthetics_info_header(header):
    # Return a dictionary of values from SyntheticsInfo header
    # Returns empty dict, if version is not supported.

    synthetics_info = {}
    version = None

    try:
        version = int(header.get("version"))

        if version == 1:
            synthetics_info["version"] = version
            synthetics_info["type"] = header.get("type")
            synthetics_info["initiator"] = header.get("initiator")
            synthetics_info["attributes"] = header.get("attributes")
    except Exception:
        return

    return synthetics_info


def _remove_query_string(url):
    url = ensure_str(url)
    out = urlparse.urlsplit(url)
    return urlparse.urlunsplit((out.scheme, out.netloc, out.path, "", ""))


def _is_websocket(environ):
    return environ.get("HTTP_UPGRADE", "").lower() == "websocket"


def _encode_nonce(nonce):
    if not nonce:
        return ""
    else:
        return f' nonce="{ensure_str(nonce)}"'  # Extra space intentional


class WebTransaction(Transaction):
    unicode_error_reported = False
    QUEUE_TIME_HEADERS = ("x-request-start", "x-queue-start")

    def __init__(
        self,
        application,
        name,
        group=None,
        scheme=None,
        host=None,
        port=None,
        request_method=None,
        request_path=None,
        query_string=None,
        headers=None,
        enabled=None,
        source=None,
    ):
        super(WebTransaction, self).__init__(application, enabled, source=source)

        # Flag for tracking whether RUM header has been generated.
        self.rum_header_generated = False

        if not self.enabled:
            return

        # Inputs
        self._request_uri = request_path
        self._request_method = request_method
        self._request_scheme = scheme
        self._request_host = host
        self._request_params = {}
        self._request_headers = {}

        try:
            self._port = int(port)
        except Exception:
            self._port = None

        # Response
        self._response_headers = {}
        self._response_code = None

        if headers is not None:
            try:
                headers = headers.items()
            except Exception:
                pass

            for k, v in headers:
                k = ensure_str(k)
                if k is not None:
                    self._request_headers[k.lower()] = v

        # Capture query request string parameters, unless we're in
        # High Security Mode.
        if query_string and not self._settings.high_security:
            query_string = ensure_str(query_string)
            try:
                params = urlparse.parse_qs(query_string, keep_blank_values=True)
                self._request_params.update(params)
            except Exception:
                pass

        self._process_queue_time()
        self._process_synthetics_header()
        self._process_context_headers()

        if name is not None:
            self.set_transaction_name(name, group, priority=1)
        elif request_path is not None:
            self.set_transaction_name(request_path, "Uri", priority=1)

    def _process_queue_time(self):
        for queue_time_header in self.QUEUE_TIME_HEADERS:
            value = self._request_headers.get(queue_time_header)
            if not value:
                continue
            value = ensure_str(value)

            try:
                if value.startswith("t="):
                    self.queue_start = _parse_time_stamp(float(value[2:]))
                else:
                    self.queue_start = _parse_time_stamp(float(value))
            except Exception:
                pass

            if self.queue_start > 0.0:
                break

    def _process_synthetics_header(self):
        # Check for Synthetics header

        settings = self._settings

        if settings.synthetics.enabled and settings.trusted_account_ids and settings.encoding_key:
            # Synthetics Header
            encoded_header = self._request_headers.get("x-newrelic-synthetics")
            encoded_header = encoded_header and ensure_str(encoded_header)
            if not encoded_header:
                return

            decoded_header = decode_newrelic_header(encoded_header, settings.encoding_key)
            synthetics = _parse_synthetics_header(decoded_header)

            # Synthetics Info Header
            encoded_info_header = self._request_headers.get("x-newrelic-synthetics-info")
            encoded_info_header = encoded_info_header and ensure_str(encoded_info_header)

            decoded_info_header = decode_newrelic_header(encoded_info_header, settings.encoding_key)
            synthetics_info = _parse_synthetics_info_header(decoded_info_header)

            if synthetics and synthetics["account_id"] in settings.trusted_account_ids:
                # Save obfuscated headers, because we will pass them along
                # unchanged in all external requests.

                self.synthetics_header = encoded_header
                self.synthetics_resource_id = synthetics["resource_id"]
                self.synthetics_job_id = synthetics["job_id"]
                self.synthetics_monitor_id = synthetics["monitor_id"]

                if synthetics_info:
                    self.synthetics_info_header = encoded_info_header
                    self.synthetics_type = synthetics_info["type"]
                    self.synthetics_initiator = synthetics_info["initiator"]
                    self.synthetics_attributes = synthetics_info["attributes"]

    def _process_context_headers(self):
        # Process the New Relic cross process ID header and extract
        # the relevant details.
        if self._settings.distributed_tracing.enabled:
            self.accept_distributed_trace_headers(self._request_headers)
        else:
            client_cross_process_id = self._request_headers.get("x-newrelic-id")
            txn_header = self._request_headers.get("x-newrelic-transaction")
            self._process_incoming_cat_headers(client_cross_process_id, txn_header)

    def process_response(self, status_code, response_headers):
        """Processes response status and headers, extracting any
        details required and returning a set of additional headers
        to merge into that being returned for the web transaction.

        """

        if not self.enabled:
            return []

        # Extract response headers
        if response_headers:
            try:
                response_headers = response_headers.items()
            except Exception:
                pass

            for header, value in response_headers:
                header = ensure_str(header)
                if header is not None:
                    self._response_headers[header.lower()] = value

        try:
            self._response_code = int(status_code)

            # If response code is 304 do not insert CAT headers. See:
            # https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5
            if self._response_code == 304:
                return []
        except Exception:
            pass

        if self.client_cross_process_id is None:
            return []

        # Generate CAT response headers
        try:
            read_length = int(self._request_headers.get("content-length"))
        except Exception:
            read_length = -1

        return self._generate_response_headers(read_length)

    def _update_agent_attributes(self):
        if "accept" in self._request_headers:
            self._add_agent_attribute("request.headers.accept", self._request_headers["accept"])
        try:
            content_length = int(self._request_headers["content-length"])
            self._add_agent_attribute("request.headers.contentLength", content_length)
        except:
            pass
        if "content-type" in self._request_headers:
            self._add_agent_attribute("request.headers.contentType", self._request_headers["content-type"])
        if "host" in self._request_headers:
            self._add_agent_attribute("request.headers.host", self._request_headers["host"])
        if "referer" in self._request_headers:
            self._add_agent_attribute("request.headers.referer", _remove_query_string(self._request_headers["referer"]))
        if "user-agent" in self._request_headers:
            self._add_agent_attribute("request.headers.userAgent", self._request_headers["user-agent"])
        if self._request_method:
            self._add_agent_attribute("request.method", self._request_method)
        if self._request_uri:
            self._add_agent_attribute("request.uri", self._request_uri)
        try:
            content_length = int(self._response_headers["content-length"])
            self._add_agent_attribute("response.headers.contentLength", content_length)
        except:
            pass
        if "content-type" in self._response_headers:
            self._add_agent_attribute("response.headers.contentType", self._response_headers["content-type"])
        if self._response_code is not None:
            self._add_agent_attribute("response.status", str(self._response_code))

        return super(WebTransaction, self)._update_agent_attributes()

    def browser_timing_header(self, nonce=None):
        """Returns the JavaScript header to be included in any HTML
        response to perform real user monitoring. This function returns
        the header as a native Python string.
        """

        if not self.enabled:
            return ""

        if self._state != self.STATE_RUNNING:
            return ""

        if self.background_task:
            return ""

        if self.ignore_transaction:
            return ""

        if not self._settings:
            return ""

        if not self._settings.browser_monitoring.enabled:
            return ""

        if not self._settings.license_key:
            return ""

        # Don't return the header a second time if it has already
        # been generated.

        if self.rum_header_generated:
            return ""

        # Requirement is that the first 13 characters of the account
        # license key is used as the key when obfuscating values for
        # the RUM configuration. Will not be able to perform the obfuscation
        # if license key isn't that long for some reason.

        if len(self._settings.license_key) < 13:
            return ""

        # Return the RUM header only if the agent received a valid value
        # for js_agent_loader from the data collector. The data
        # collector is not meant to send a non empty value for the
        # js_agent_loader value if browser_monitoring.loader is set to
        # 'none'.

        if self._settings.js_agent_loader:
            # Make sure we freeze the path.

            self._freeze_path()

            # When obfuscating values for the browser agent configuration, we only use the
            # first 13 characters of the account license key.

            obfuscation_key = self._settings.license_key[:13]

            attributes = {}

            user_attributes = {}
            for attr in self.user_attributes:
                if attr.destinations & DST_BROWSER_MONITORING:
                    user_attributes[attr.name] = attr.value

            if user_attributes:
                attributes["u"] = user_attributes

            request_parameters = self.request_parameters
            request_parameter_attributes = self.filter_request_parameters(request_parameters)
            agent_attributes = {}
            for attr in request_parameter_attributes:
                if attr.destinations & DST_BROWSER_MONITORING:
                    agent_attributes[attr.name] = attr.value

            if agent_attributes:
                attributes["a"] = agent_attributes

            # create the data structure that pull all our data in

            browser_agent_configuration = self.browser_monitoring_intrinsics(obfuscation_key)

            if attributes:
                attributes = obfuscate(json_encode(attributes), obfuscation_key)
                browser_agent_configuration["atts"] = attributes

            header = _js_agent_header_fragment % (
                _encode_nonce(nonce),
                json_encode(browser_agent_configuration),
                self._settings.js_agent_loader,
            )

            # To avoid any issues with browser encodings, we will make sure
            # that the javascript we inject for the browser agent is ASCII
            # encodable. Since we obfuscate all agent and user attributes, and
            # the transaction name with base 64 encoding, this will preserve
            # those strings, if they have values outside of the ASCII character
            # set. If encoding as ASCII fails we will return an empty
            # string.

            try:
                header.encode("ascii")

            except UnicodeError:
                if not WebTransaction.unicode_error_reported:
                    _logger.error("ASCII encoding of js-agent-header failed.", header)
                    WebTransaction.unicode_error_reported = True

                header = ""

        else:
            header = ""

        # We remember if we have returned a non empty string value and
        # if called a second time we will not return it again.

        if header:
            self.rum_header_generated = True

        return header

    def browser_timing_footer(self, nonce=None):
        """Deprecated API that has been replaced entirely by browser_timing_header()."""
        warnings.warn(
            "The browser_timing_footer function is deprecated. Please migrate to only using the browser_timing_header api instead.",
            DeprecationWarning,
        )
        return ""

    def browser_monitoring_intrinsics(self, obfuscation_key):
        txn_name = obfuscate(self.path, obfuscation_key)

        queue_start = self.queue_start or self.start_time
        start_time = self.start_time
        end_time = time.time()

        queue_duration = int((start_time - queue_start) * 1000)
        request_duration = int((end_time - start_time) * 1000)

        intrinsics = {
            "beacon": self._settings.beacon,
            "errorBeacon": self._settings.error_beacon,
            "licenseKey": self._settings.browser_key,
            "applicationID": self._settings.application_id,
            "transactionName": txn_name,
            "queueTime": queue_duration,
            "applicationTime": request_duration,
            "agent": self._settings.js_agent_file,
        }

        if self._settings.browser_monitoring.ssl_for_http is not None:
            ssl_for_http = self._settings.browser_monitoring.ssl_for_http
            intrinsics["sslForHttp"] = ssl_for_http

        return intrinsics


class WSGIHeaderProxy():
    def __init__(self, environ):
        self.environ = environ
        self.length = None

    @staticmethod
    def _to_wsgi(key):
        key = key.upper()
        if key == "CONTENT-LENGTH":
            return "CONTENT_LENGTH"
        elif key == "CONTENT-TYPE":
            return "CONTENT_TYPE"
        return f"HTTP_{key.replace('-', '_')}"

    @staticmethod
    def _from_wsgi(key):
        key = key.lower()
        return key[5:].replace("_", "-")

    def __getitem__(self, key):
        wsgi_key = self._to_wsgi(key)
        return self.environ[wsgi_key]

    def __iter__(self):
        for key in self.environ:
            if key == "CONTENT_LENGTH":
                yield "content-length", self.environ["CONTENT_LENGTH"]
            elif key == "CONTENT_TYPE":
                yield "content-type", self.environ["CONTENT_TYPE"]
            elif key in ("HTTP_CONTENT_LENGTH", "HTTP_CONTENT_TYPE"):
                # These keys are illegal and should be ignored
                continue
            elif key.startswith("HTTP_"):
                yield self._from_wsgi(key), self.environ[key]

    def __len__(self):
        if self.length is None:
            self.length = sum(1 for _ in iter(self))
        return self.length


class WSGIWebTransaction(WebTransaction):
    MOD_WSGI_HEADERS = ("mod_wsgi.request_start", "mod_wsgi.queue_start")

    def __init__(self, application, environ, source=None):
        # The web transaction can be enabled/disabled by
        # the value of the variable "newrelic.enabled"
        # in the WSGI environ dictionary. We need to check
        # this before initialising the transaction as needs
        # to be passed in base class constructor. The
        # default is None, which would then result in the
        # base class making the decision based on whether
        # application or agent as a whole are enabled.

        enabled = _lookup_environ_setting(environ, "newrelic.enabled", None)

        # Initialise the common transaction base class.

        super(WSGIWebTransaction, self).__init__(
            application,
            name=None,
            port=environ.get("SERVER_PORT"),
            request_method=environ.get("REQUEST_METHOD"),
            query_string=environ.get("QUERY_STRING"),
            headers=iter(WSGIHeaderProxy(environ)),
            enabled=enabled,
            source=source,
        )

        # Disable transactions for websocket connections.
        # Also disable autorum if this is a websocket. This is a good idea for
        # two reasons. First, RUM is unnecessary for websocket transactions
        # anyway. Secondly, due to a bug in the gevent-websocket (0.9.5)
        # package, if our _WSGIApplicationMiddleware is applied a websocket
        # connection cannot be made.

        if _is_websocket(environ):
            self.autorum_disabled = True
            self.enabled = False

        # Bail out if the transaction is running in a
        # disabled state.

        if not self.enabled:
            return

        # Will need to check the settings a number of times.

        settings = self._settings

        # Check for override settings from WSGI environ.

        self.background_task = _lookup_environ_setting(environ, "newrelic.set_background_task", False)

        self.ignore_transaction = _lookup_environ_setting(environ, "newrelic.ignore_transaction", False)
        self.suppress_apdex = _lookup_environ_setting(environ, "newrelic.suppress_apdex_metric", False)
        self.suppress_transaction_trace = _lookup_environ_setting(environ, "newrelic.suppress_transaction_trace", False)
        self.capture_params = _lookup_environ_setting(
            environ, "newrelic.capture_request_params", settings.capture_params
        )
        self.autorum_disabled = _lookup_environ_setting(
            environ, "newrelic.disable_browser_autorum", not settings.browser_monitoring.auto_instrument
        )

        # Make sure that if high security mode is enabled that
        # capture of request params is still being disabled.
        # No warning is issued for this in the logs because it
        # is a per request configuration and would create a lot
        # of noise.

        if settings.high_security:
            self.capture_params = False

        # LEGACY: capture_params = False
        #
        #    Don't add request parameters at all, which means they will not
        #    go through the AttributeFilter.
        if self.capture_params is False:
            self._request_params.clear()

        # Extract from the WSGI environ dictionary
        # details of the URL path. This will be set as
        # default path for the web transaction. This can
        # be overridden by framework to be more specific
        # to avoid metrics explosion problem resulting
        # from too many distinct URLs for same resource
        # due to use of REST style URL concepts or
        # otherwise.

        request_uri = environ.get("REQUEST_URI", None)

        if request_uri is None:
            # The gunicorn WSGI server uses RAW_URI instead
            # of the more typical REQUEST_URI used by Apache
            # and other web servers.

            request_uri = environ.get("RAW_URI", None)

        script_name = environ.get("SCRIPT_NAME", None)
        path_info = environ.get("PATH_INFO", None)

        self._request_uri = request_uri

        if self._request_uri is not None:
            # Need to make sure we drop off any query string
            # arguments on the path if we have to fallback
            # to using the original REQUEST_URI. Can't use
            # attribute access on result as only support for
            # Python 2.5+.

            self._request_uri = urlparse.urlparse(self._request_uri)[2]

        if script_name is not None or path_info is not None:
            if path_info is None:
                path = script_name
            elif script_name is None:
                path = path_info
            else:
                path = script_name + path_info

            self.set_transaction_name(path, "Uri", priority=1)

            if self._request_uri is None:
                self._request_uri = path
        else:
            if self._request_uri is not None:
                self.set_transaction_name(self._request_uri, "Uri", priority=1)

        # mod_wsgi sets its own distinct variables for queue time
        # automatically. Initially it set mod_wsgi.queue_start,
        # which equated to when Apache first accepted the
        # request. This got changed to mod_wsgi.request_start
        # however, and mod_wsgi.queue_start was instead used
        # just for when requests are to be queued up for the
        # daemon process and corresponded to the point at which
        # they are being proxied, after Apache does any
        # authentication etc. We check for both so older
        # versions of mod_wsgi will still work, although we
        # don't try and use the fact that it is possible to
        # distinguish the two points and just pick up the
        # earlier of the two.
        for queue_time_header in self.MOD_WSGI_HEADERS:
            if self.queue_start > 0.0:
                break

            value = environ.get(queue_time_header)
            if not value:
                continue

            try:
                if value.startswith("t="):
                    try:
                        self.queue_start = _parse_time_stamp(float(value[2:]))
                    except Exception:
                        pass
                else:
                    try:
                        self.queue_start = _parse_time_stamp(float(value))
                    except Exception:
                        pass

            except Exception:
                pass

    def __exit__(self, exc, value, tb):
        self.record_custom_metric("Python/WSGI/Input/Bytes", self._bytes_read)
        self.record_custom_metric("Python/WSGI/Input/Time", self.read_duration)
        self.record_custom_metric("Python/WSGI/Input/Calls/read", self._calls_read)
        self.record_custom_metric("Python/WSGI/Input/Calls/readline", self._calls_readline)
        self.record_custom_metric("Python/WSGI/Input/Calls/readlines", self._calls_readlines)

        self.record_custom_metric("Python/WSGI/Output/Bytes", self._bytes_sent)
        self.record_custom_metric("Python/WSGI/Output/Time", self.sent_duration)
        self.record_custom_metric("Python/WSGI/Output/Calls/yield", self._calls_yield)
        self.record_custom_metric("Python/WSGI/Output/Calls/write", self._calls_write)

        return super(WSGIWebTransaction, self).__exit__(exc, value, tb)

    def _update_agent_attributes(self):
        # Add WSGI agent attributes
        if self.read_duration != 0:
            self._add_agent_attribute("wsgi.input.seconds", self.read_duration)
        if self._bytes_read != 0:
            self._add_agent_attribute("wsgi.input.bytes", self._bytes_read)
        if self._calls_read != 0:
            self._add_agent_attribute("wsgi.input.calls.read", self._calls_read)
        if self._calls_readline != 0:
            self._add_agent_attribute("wsgi.input.calls.readline", self._calls_readline)
        if self._calls_readlines != 0:
            self._add_agent_attribute("wsgi.input.calls.readlines", self._calls_readlines)

        if self.sent_duration != 0:
            self._add_agent_attribute("wsgi.output.seconds", self.sent_duration)
        if self._bytes_sent != 0:
            self._add_agent_attribute("wsgi.output.bytes", self._bytes_sent)
        if self._calls_write != 0:
            self._add_agent_attribute("wsgi.output.calls.write", self._calls_write)
        if self._calls_yield != 0:
            self._add_agent_attribute("wsgi.output.calls.yield", self._calls_yield)

        return super(WSGIWebTransaction, self)._update_agent_attributes()

    def process_response(self, status, response_headers, *args):
        """Processes response status and headers, extracting any
        details required and returning a set of additional headers
        to merge into that being returned for the web transaction.

        """

        # Set our internal response code based on WSGI status.
        # Per spec, it is expected that this is a string. If this is not
        # the case, skip setting the internal response code as we cannot
        # make the determination. (An integer 200 for example when passed
        # would raise as a 500 for WSGI applications).

        try:
            status = status.split(" ", 1)[0]
        except Exception:
            status = None

        return super(WSGIWebTransaction, self).process_response(status, response_headers)


def WebTransactionWrapper(
    wrapped,
    application=None,
    name=None,
    group=None,
    scheme=None,
    host=None,
    port=None,
    request_method=None,
    request_path=None,
    query_string=None,
    headers=None,
    source=None,
):
    def wrapper(wrapped, instance, args, kwargs):
        if type(application) != Application:
            _application = application_instance(application)
        else:
            _application = application

        if callable(name):
            if instance is not None:
                _name = name(instance, *args, **kwargs)
            else:
                _name = name(*args, **kwargs)
        elif name is None:
            _name = callable_name(wrapped)
        else:
            _name = name

        if callable(group):
            if instance is not None:
                _group = group(instance, *args, **kwargs)
            else:
                _group = group(*args, **kwargs)
        else:
            _group = group

        if callable(scheme):
            if instance is not None:
                _scheme = scheme(instance, *args, **kwargs)
            else:
                _scheme = scheme(*args, **kwargs)
        else:
            _scheme = scheme

        if callable(host):
            if instance is not None:
                _host = host(instance, *args, **kwargs)
            else:
                _host = host(*args, **kwargs)
        else:
            _host = host

        if callable(port):
            if instance is not None:
                _port = port(instance, *args, **kwargs)
            else:
                _port = port(*args, **kwargs)
        else:
            _port = port

        if callable(request_method):
            if instance is not None:
                _request_method = request_method(instance, *args, **kwargs)
            else:
                _request_method = request_method(*args, **kwargs)
        else:
            _request_method = request_method

        if callable(request_path):
            if instance is not None:
                _request_path = request_path(instance, *args, **kwargs)
            else:
                _request_path = request_path(*args, **kwargs)
        else:
            _request_path = request_path

        if callable(query_string):
            if instance is not None:
                _query_string = query_string(instance, *args, **kwargs)
            else:
                _query_string = query_string(*args, **kwargs)
        else:
            _query_string = query_string

        if callable(headers):
            if instance is not None:
                _headers = headers(instance, *args, **kwargs)
            else:
                _headers = headers(*args, **kwargs)
        else:
            _headers = headers

        proxy = async_proxy(wrapped)

        source_arg = source or wrapped

        def create_transaction(transaction):
            if transaction:
                return None
            return WebTransaction(
                _application,
                _name,
                _group,
                _scheme,
                _host,
                _port,
                _request_method,
                _request_path,
                _query_string,
                _headers,
                source=source_arg,
            )

        if proxy:
            context_manager = TransactionContext(create_transaction)
            return proxy(wrapped(*args, **kwargs), context_manager)

        transaction = WebTransaction(
            _application,
            _name,
            _group,
            _scheme,
            _host,
            _port,
            _request_method,
            _request_path,
            _query_string,
            _headers,
            source=source_arg,
        )

        transaction = create_transaction(current_transaction(active_only=False))

        if not transaction:
            return wrapped(*args, **kwargs)

        with transaction:
            return wrapped(*args, **kwargs)

    return FunctionWrapper(wrapped, wrapper)


def web_transaction(
    application=None,
    name=None,
    group=None,
    scheme=None,
    host=None,
    port=None,
    request_method=None,
    request_path=None,
    query_string=None,
    headers=None,
):
    return functools.partial(
        WebTransactionWrapper,
        application=application,
        name=name,
        group=group,
        scheme=scheme,
        host=host,
        port=port,
        request_method=request_method,
        request_path=request_path,
        query_string=query_string,
        headers=headers,
    )


def wrap_web_transaction(
    module,
    object_path,
    application=None,
    name=None,
    group=None,
    scheme=None,
    host=None,
    port=None,
    request_method=None,
    request_path=None,
    query_string=None,
    headers=None,
):
    return wrap_object(
        module,
        object_path,
        WebTransactionWrapper,
        (application, name, group, scheme, host, port, request_method, request_path, query_string, headers),
    )
