import sys
from pprint import pformat

import pyramid.tweens
from pyramid.settings import aslist
from pyramid.settings import asbool
from pyramid.util import DottedNameResolver
from pyramid.httpexceptions import WSGIHTTPException

resolver = DottedNameResolver(None)

PY3 = sys.version_info[0] == 3

if PY3: # pragma: no cover
    import builtins
    _text_type = str
else:
    import __builtin__ as builtins
    _text_type = unicode

def as_globals_list(value):
    L = []
    value = aslist(value)
    for dottedname in value:
        if dottedname in builtins.__dict__:
            if PY3: # pragma: no cover
                dottedname = 'builtins.%s' % dottedname
            else:
                dottedname = '__builtin__.%s' % dottedname
        obj = resolver.maybe_resolve(dottedname)
        L.append(obj)
    return L

def _get_url(request):
    try:
        url = repr(request.url)
    except UnicodeDecodeError:
        # do the best we can
        url = (request.host_url +
               request.environ.get('SCRIPT_NAME') +
               request.environ.get('PATH_INFO'))
        qs = request.environ.get('QUERY_STRING')
        if qs:
            url += '?' + qs
        url = 'could not decode url: %r' % url
    url = _text_type(url)
    return url

_MESSAGE_TEMPLATE = _text_type("""

%(url)s

ENVIRONMENT

%(env)s


PARAMETERS

%(params)s


UNAUTHENTICATED USER

%(usr)s

""")

def _get_message(request):
    """
    Return a string with useful information from the request.

    On python 2 this method will return ``unicode`` and on Python 3 ``str``
    will be returned. This seems to be what the logging module expects.

    """
    url = _get_url(request)
    unauth = request.unauthenticated_userid

    try:
        params = request.params
    except UnicodeDecodeError:
        params = 'could not decode params'
    except IOError as ex:
        params = 'IOError while decoding params: %s' % ex

    if not isinstance(unauth, _text_type):
        unauth = repr(unauth)

    return _MESSAGE_TEMPLATE % dict(
            url=url,
            env=pformat(request.environ),
            params=pformat(params),
            usr=unauth)

class ErrorHandler(object):
    def __init__(self, ignored, getLogger, get_message):
        self.ignored = ignored
        self.getLogger = getLogger
        self.get_message = get_message

    def __call__(self, request, exc_info=None):
        # save the traceback as it may get lost when we get the message.
        # _handle_error is not in the traceback, so calling sys.exc_info
        # does NOT create a circular reference
        if exc_info is None:
            exc_info = sys.exc_info()

        if isinstance(exc_info[1], self.ignored):
            return
        try:
            logger = self.getLogger('exc_logger')
            message = self.get_message(request)
            logger.error(message, exc_info=exc_info)
        except:
            logger.exception("Exception while logging")

def exclog_tween_factory(handler, registry):
    get = registry.settings.get

    ignored = get('exclog.ignore', (WSGIHTTPException,))
    get_message = _get_url
    if get('exclog.extra_info', False):
        get_message = _get_message
    get_message = get('exclog.get_message', get_message)

    getLogger = get('exclog.getLogger', 'logging.getLogger')
    getLogger = resolver.maybe_resolve(getLogger)

    handle_error = ErrorHandler(ignored, getLogger, get_message)

    def exclog_tween(request):
        try:
            response = handler(request)
            exc_info = getattr(request, 'exc_info', None)
            if exc_info is not None:
                handle_error(request, exc_info)
            return response

        except:
            handle_error(request)
            raise
    return exclog_tween

def includeme(config):
    """
    Set up am implicit :term:`tween` to log exception information that is
    generated by your Pyramid application.  The logging data will be sent to
    the Python logger named ``exc_logger``.

    This tween configured to be placed 'over' the exception view tween.  It
    will log all exceptions (even those caught by a Pyramid exception view)
    except 'http exceptions' (any exception that derives from
    ``pyramid.httpexceptions.WSGIHTTPException`` such as ``HTTPFound``).  You
    can instruct ``pyramid_exclog`` to ignore custom exception types by using
    the ``exclog.ignore`` configuration setting.

    """
    get = config.registry.settings.get
    ignored = as_globals_list(get(
        'exclog.ignore',
        'pyramid.httpexceptions.WSGIHTTPException',
    ))
    extra_info = asbool(get('exclog.extra_info', False))
    get_message = get('exclog.get_message', None)
    if get_message is not None:
        get_message = config.maybe_dotted(get_message)
        config.registry.settings['exclog.get_message'] = get_message
    config.registry.settings['exclog.ignore'] = tuple(ignored)
    config.registry.settings['exclog.extra_info'] = extra_info
    config.add_tween('pyramid_exclog.exclog_tween_factory', over=[
        pyramid.tweens.EXCVIEW,
        # if pyramid_tm is in the pipeline we want to track errors caused
        # by commit/abort so we try to place ourselves over it
        'pyramid_tm.tm_tween_factory',
    ])
