"""
elasticapm.contrib.django.models
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Acts as an implicit hook for Django installs.

:copyright: (c) 2011-2017 Elasticsearch

Large portions are
:copyright: (c) 2010 by the Sentry Team, see AUTHORS for more details.
:license: BSD, see LICENSE for more details.
"""

from __future__ import absolute_import

import logging
import os
import sys
import warnings

from django.conf import settings as django_settings

import elasticapm.instrumentation.control
from elasticapm.utils import disabled_due_to_debug, six

logger = logging.getLogger('elasticapm.errors.client')


def get_installed_apps():
    """
    Generate a list of modules in settings.INSTALLED_APPS.
    """
    out = set()
    for app in django_settings.INSTALLED_APPS:
        out.add(app)
    return out

_client = (None, None)


class ProxyClient(object):
    """
    A proxy which represents the currenty client at all times.
    """
    # introspection support:
    __members__ = property(lambda x: x.__dir__())

    # Need to pretend to be the wrapped class, for the sake of objects that care
    # about this (especially in equality tests)
    __class__ = property(lambda x: get_client().__class__)

    __dict__ = property(lambda o: get_client().__dict__)

    __repr__ = lambda: repr(get_client())
    __getattr__ = lambda x, o: getattr(get_client(), o)
    __setattr__ = lambda x, o, v: setattr(get_client(), o, v)
    __delattr__ = lambda x, o: delattr(get_client(), o)

    __lt__ = lambda x, o: get_client() < o
    __le__ = lambda x, o: get_client() <= o
    __eq__ = lambda x, o: get_client() == o
    __ne__ = lambda x, o: get_client() != o
    __gt__ = lambda x, o: get_client() > o
    __ge__ = lambda x, o: get_client() >= o
    if six.PY2:
        __cmp__ = lambda x, o: cmp(get_client(), o)
    __hash__ = lambda x: hash(get_client())
    # attributes are currently not callable
    # __call__ = lambda x, *a, **kw: get_client()(*a, **kw)
    __nonzero__ = lambda x: bool(get_client())
    __len__ = lambda x: len(get_client())
    __getitem__ = lambda x, i: get_client()[i]
    __iter__ = lambda x: iter(get_client())
    __contains__ = lambda x, i: i in get_client()
    __getslice__ = lambda x, i, j: get_client()[i:j]
    __add__ = lambda x, o: get_client() + o
    __sub__ = lambda x, o: get_client() - o
    __mul__ = lambda x, o: get_client() * o
    __floordiv__ = lambda x, o: get_client() // o
    __mod__ = lambda x, o: get_client() % o
    __divmod__ = lambda x, o: get_client().__divmod__(o)
    __pow__ = lambda x, o: get_client() ** o
    __lshift__ = lambda x, o: get_client() << o
    __rshift__ = lambda x, o: get_client() >> o
    __and__ = lambda x, o: get_client() & o
    __xor__ = lambda x, o: get_client() ^ o
    __or__ = lambda x, o: get_client() | o
    __div__ = lambda x, o: get_client().__div__(o)
    __truediv__ = lambda x, o: get_client().__truediv__(o)
    __neg__ = lambda x: -(get_client())
    __pos__ = lambda x: +(get_client())
    __abs__ = lambda x: abs(get_client())
    __invert__ = lambda x: ~(get_client())
    __complex__ = lambda x: complex(get_client())
    __int__ = lambda x: int(get_client())
    if not six.PY2:
        __long__ = lambda x: long(get_client())
    __float__ = lambda x: float(get_client())
    __str__ = lambda x: str(get_client())
    __unicode__ = lambda x: six.text_type(get_client())
    __oct__ = lambda x: oct(get_client())
    __hex__ = lambda x: hex(get_client())
    __index__ = lambda x: get_client().__index__()
    __coerce__ = lambda x, o: x.__coerce__(x, o)
    __enter__ = lambda x: x.__enter__()
    __exit__ = lambda x, *a, **kw: x.__exit__(*a, **kw)

client = ProxyClient()

default_client_class = 'elasticapm.contrib.django.DjangoClient'


def get_client_class(client_path=default_client_class):
    module, class_name = client_path.rsplit('.', 1)
    return getattr(__import__(module, {}, {}, class_name), class_name)


def get_client_config():
    config = getattr(django_settings, 'ELASTICAPM', {})
    if 'ASYNC' in config:
        warnings.warn(
            'Usage of "ASYNC" configuration is deprecated. Use "ASYNC_MODE"',
            category=DeprecationWarning,
            stacklevel=2,
        )
        config['ASYNC_MODE'] = config['ASYNC']
    return dict(
        servers=config.get('SERVERS', None),
        include_paths=set(
            config.get('INCLUDE_PATHS', [])) | get_installed_apps(),
        exclude_paths=config.get('EXCLUDE_PATHS', None),
        filter_exception_types=config.get('FILTER_EXCEPTION_TYPES', None),
        timeout=config.get('TIMEOUT', None),
        hostname=config.get('HOSTNAME', None),
        auto_log_stacks=config.get('AUTO_LOG_STACKS', None),
        string_max_length=config.get('MAX_LENGTH_STRING', None),
        list_max_length=config.get('MAX_LENGTH_LIST', None),
        app_name=config.get('APP_NAME', None),
        secret_token=config.get('SECRET_TOKEN', None),
        transport_class=config.get('TRANSPORT_CLASS', None),
        processors=config.get('PROCESSORS', None),
        traces_send_freq_secs=config.get('TRACES_SEND_FREQ_SEC', None),
        async_mode=config.get('ASYNC_MODE', None),
        instrument_django_middleware=config.get('INSTRUMENT_DJANGO_MIDDLEWARE'),
        transactions_ignore_patterns=config.get('TRANSACTIONS_IGNORE_PATTERNS', None),
    )


def get_client(client=None):
    """
    Get an ElasticAPM client.

    :param client:
    :return:
    :rtype: elasticapm.base.Client
    """
    global _client

    tmp_client = client is not None
    if not tmp_client:
        config = getattr(django_settings, 'ELASTICAPM', {})
        client = config.get('CLIENT', default_client_class)

    if _client[0] != client:
        client_class = get_client_class(client)
        instance = client_class(**get_client_config())
        if not tmp_client:
            _client = (client, instance)
        return instance
    return _client[1]


def exception_handler(request=None, **kwargs):
    def actually_do_stuff(request=None, **kwargs):
        exc_info = sys.exc_info()
        try:
            if (
                disabled_due_to_debug(
                    getattr(django_settings, 'ELASTICAPM', {}),
                    django_settings.DEBUG
                )
                or getattr(exc_info[1], 'skip_elasticapm', False)
            ):
                return

            get_client().capture('Exception', exc_info=exc_info,
                                 request=request)
        except Exception as exc:
            try:
                logger.exception(u'Unable to process log entry: %s' % (exc,))
            except Exception as exc:
                warnings.warn(u'Unable to process log entry: %s' % (exc,))
        finally:
            try:
                del exc_info
            except Exception as e:
                logger.exception(e)

    return actually_do_stuff(request, **kwargs)


def register_handlers():
    from django.core.signals import got_request_exception

    # Connect to Django's internal signal handler
    got_request_exception.connect(exception_handler)

    # If Celery is installed, register a signal handler
    if 'djcelery' in django_settings.INSTALLED_APPS:
        from elasticapm.contrib.celery import register_signal

        try:
            register_signal(client)
        except Exception as e:
            logger.exception('Failed installing django-celery hook: %s' % e)

    # Instrument to get traces
    skip_env_var = 'SKIP_INSTRUMENT'
    if skip_env_var in os.environ:
        logger.debug("Skipping instrumentation. %s is set.", skip_env_var)
    else:
        elasticapm.instrumentation.control.instrument()


if 'elasticapm.contrib.django' in django_settings.INSTALLED_APPS:
    register_handlers()
