from tornado.web import HTTPError

from .constants import CONFIG_KEY, REQUEST_CONTEXT_KEY, REQUEST_SPAN_KEY
from .stack_context import TracerStackContext
from ...constants import ANALYTICS_SAMPLE_RATE_KEY, SPAN_MEASURED_KEY
from ...ext import SpanTypes, http
from ...propagation.http import HTTPPropagator
from ...settings import config


def execute(func, handler, args, kwargs):
    """
    Wrap the handler execute method so that the entire request is within the same
    ``TracerStackContext``. This simplifies users code when the automatic ``Context``
    retrieval is used via ``Tracer.trace()`` method.
    """
    # retrieve tracing settings
    settings = handler.settings[CONFIG_KEY]
    tracer = settings['tracer']
    service = settings['default_service']
    distributed_tracing = settings['distributed_tracing']

    with TracerStackContext():
        # attach the context to the request
        setattr(handler.request, REQUEST_CONTEXT_KEY, tracer.get_call_context())

        # Read and use propagated context from HTTP headers
        if distributed_tracing:
            propagator = HTTPPropagator()
            context = propagator.extract(handler.request.headers)
            if context.trace_id:
                tracer.context_provider.activate(context)

        # store the request span in the request so that it can be used later
        request_span = tracer.trace(
            'tornado.request',
            service=service,
            span_type=SpanTypes.WEB,
        )
        request_span.set_tag(SPAN_MEASURED_KEY)
        # set analytics sample rate
        # DEV: tornado is special case maintains separate configuration from config api
        analytics_enabled = settings['analytics_enabled']
        if (config.analytics_enabled and analytics_enabled is not False) or analytics_enabled is True:
            request_span.set_tag(
                ANALYTICS_SAMPLE_RATE_KEY,
                settings.get('analytics_sample_rate', True)
            )

        setattr(handler.request, REQUEST_SPAN_KEY, request_span)

        return func(*args, **kwargs)


def on_finish(func, handler, args, kwargs):
    """
    Wrap the ``RequestHandler.on_finish`` method. This is the last executed method
    after the response has been sent, and it's used to retrieve and close the
    current request span (if available).
    """
    request = handler.request
    request_span = getattr(request, REQUEST_SPAN_KEY, None)
    if request_span:
        # use the class name as a resource; if an handler is not available, the
        # default handler class will be used so we don't pollute the resource
        # space here
        klass = handler.__class__
        request_span.resource = '{}.{}'.format(klass.__module__, klass.__name__)
        request_span.set_tag('http.method', request.method)
        request_span.set_tag('http.status_code', handler.get_status())
        request_span.set_tag(http.URL, request.full_url().rsplit('?', 1)[0])
        if config.tornado.trace_query_string:
            request_span.set_tag(http.QUERY_STRING, request.query)
        request_span.finish()

    return func(*args, **kwargs)


def log_exception(func, handler, args, kwargs):
    """
    Wrap the ``RequestHandler.log_exception``. This method is called when an
    Exception is not handled in the user code. In this case, we save the exception
    in the current active span. If the Tornado ``Finish`` exception is raised, this wrapper
    will not be called because ``Finish`` is not an exception.
    """
    # safe-guard: expected arguments -> log_exception(self, typ, value, tb)
    value = args[1] if len(args) == 3 else None
    if not value:
        return func(*args, **kwargs)

    # retrieve the current span
    tracer = handler.settings[CONFIG_KEY]['tracer']
    current_span = tracer.current_span()

    if isinstance(value, HTTPError):
        # Tornado uses HTTPError exceptions to stop and return a status code that
        # is not a 2xx. In this case we want to check the status code to be sure that
        # only 5xx are traced as errors, while any other HTTPError exception is handled as
        # usual.
        if 500 <= value.status_code <= 599:
            current_span.set_exc_info(*args)
    else:
        # any other uncaught exception should be reported as error
        current_span.set_exc_info(*args)

    return func(*args, **kwargs)
