"""Instrumentation module for Bottle framework.

"""

import functools

from newrelic.api.function_trace import (FunctionTrace, FunctionTraceWrapper,
        wrap_function_trace)
from newrelic.api.transaction import current_transaction
from newrelic.api.wsgi_application import wrap_wsgi_application
from newrelic.common.object_names import callable_name
from newrelic.common.object_wrapper import (wrap_out_function,
        function_wrapper, ObjectProxy, wrap_object_attribute,
        wrap_function_wrapper)
from newrelic.core.config import ignore_status_code

module_bottle = None

def should_ignore(exc, value, tb):
    # The HTTPError class derives from HTTPResponse and so we do not
    # need to check for it seperately as isinstance() will pick it up.

    if isinstance(value, module_bottle.HTTPResponse):
        if hasattr(value, 'status_code'):
            if ignore_status_code(value.status_code):
                return True
        elif hasattr(value, 'status'):
            if ignore_status_code(value.status):
                return True
        elif hasattr(value, 'http_status_code'):
            if ignore_status_code(value.http_status_code):
                return True

    elif hasattr(module_bottle, 'RouteReset'):
        if isinstance(value, module_bottle.RouteReset):
            return True

@function_wrapper
def callback_wrapper(wrapped, instance, args, kwargs):
    transaction = current_transaction()

    if transaction is None:
        return wrapped(*args, **kwargs)

    name = callable_name(wrapped)

    # Needs to be at a higher priority so that error handler processing
    # below will not override the web transaction being named after the
    # actual request handler.

    transaction.set_transaction_name(name, priority=2)

    with FunctionTrace(transaction, name):
        try:
            return wrapped(*args, **kwargs)

        except:  # Catch all
            # In most cases this seems like it will never be invoked as
            # bottle will internally capture the exception before we
            # get a chance and rather than propagate the exception will
            # return it instead. This doesn't always seem to be the case
            # though when plugins are used, although that may depend on
            # the specific bottle version being used.

            transaction.record_exception(ignore_errors=should_ignore)
            raise

def output_wrapper_Bottle_match(result):
    callback, args = result
    return callback_wrapper(callback), args

def output_wrapper_Route_make_callback(callback):
    return callback_wrapper(callback)

class proxy_Bottle_error_handler(ObjectProxy):
    # This proxy wraps the error_handler attribute of the Bottle class.
    # The attribute is a dictionary of handlers for HTTP status codes.
    # We specifically override the get() method of the dictionary so
    # that we can determine if the dictionary actually held a handler
    # for a specific HTTP status code. If it didn't, we name the web
    # transaction based on the status code as a fallback if not already
    # set based on a specific request handler. Otherwise, if there was
    # an error handler we will name the web transaction after the error
    # handler instead if not already set based on a specific request
    # handler.

    def get(self, status, default=None):
        transaction = current_transaction()

        if transaction is None:
            return self.__wrapped__.get(status, default)

        handler = self.__wrapped__.get(status)

        if handler:
            name = callable_name(handler)
            transaction.set_transaction_name(name, priority=1)
            handler = FunctionTraceWrapper(handler, name=name)
        else:
            transaction.set_transaction_name(str(status),
                    group='StatusCode', priority=1)

        return handler or default

def wrapper_auth_basic(wrapped, instance, args, kwargs):
    # Bottle has a bug whereby functools.wraps() is not used on the
    # nested wrapper function in the implementation of auth_basic()
    # decorator. We apply it ourself to try and workaround the issue.
    # Note that this is dependent though on the agent having been
    # initialised before the auth_basic() decorator is used. The issue
    # exists in bottle up to version 0.12.3, but shouldn't matter that
    # we reapply wraps() even in newer versions which address issue.

    decorator = wrapped(*args, **kwargs)

    def _decorator(func):
        return functools.wraps(func)(decorator(func))

    return _decorator

def instrument_bottle(module):
    global module_bottle
    module_bottle = module

    framework_details = ('Bottle', getattr(module, '__version__'))

    if hasattr(module.Bottle, 'wsgi'): # version >= 0.9
        wrap_wsgi_application(module, 'Bottle.wsgi',
                framework=framework_details)
    elif hasattr(module.Bottle, '__call__'): # version < 0.9
        wrap_wsgi_application(module, 'Bottle.__call__',
                framework=framework_details)

    if (hasattr(module, 'Route') and
            hasattr(module.Route, '_make_callback')): # version >= 0.10
        wrap_out_function(module, 'Route._make_callback',
                output_wrapper_Route_make_callback)
    elif hasattr(module.Bottle, '_match'): # version >= 0.9
        wrap_out_function(module, 'Bottle._match',
                output_wrapper_Bottle_match)
    elif hasattr(module.Bottle, 'match_url'): # version < 0.9
        wrap_out_function(module, 'Bottle.match_url',
                output_wrapper_Bottle_match)

    wrap_object_attribute(module, 'Bottle.error_handler',
            proxy_Bottle_error_handler)

    if hasattr(module, 'auth_basic'):
        wrap_function_wrapper(module, 'auth_basic', wrapper_auth_basic)

    if hasattr(module, 'SimpleTemplate'):
        wrap_function_trace(module, 'SimpleTemplate.render')

    if hasattr(module, 'MakoTemplate'):
        wrap_function_trace(module, 'MakoTemplate.render')

    if hasattr(module, 'CheetahTemplate'):
        wrap_function_trace(module, 'CheetahTemplate.render')

    if hasattr(module, 'Jinja2Template'):
        wrap_function_trace(module, 'Jinja2Template.render')

    if hasattr(module, 'SimpleTALTemplate'):
        wrap_function_trace(module, 'SimpleTALTemplate.render')
