import sys

import abc
import functools
import uuid

import config
from _setting import Settings

settings = Settings()

# compatible with Python 2 *and* 3:
ABC = abc.ABCMeta('ABC', (object,), {'__slots__': ()})


def configure(**kwargs):
    global settings
    settings = Settings(**kwargs)
    if settings.get_env() == 'dev':
        config.development_cfg(**kwargs)
    else:
        config.production_cfg(**kwargs)


class LoggerFactory(ABC):
    @abc.abstractmethod
    def bind(self, **kw):
        pass

    @abc.abstractmethod
    def get_logger(self, *args, **kw):
        """Returns a new logger bounded with the given context"""
        pass


try:
    import structlog
except ImportError as e:
    raise e


def generate_trace_id():
    return "{0}={1}".format(str(settings.get_app()), str(uuid.uuid1()))


class StructlogFactoryImpl(LoggerFactory):
    _logger = None

    def __init__(self, *args, **kw):
        self._logger = structlog.get_logger(*args).new(**kw)

    def bind(self, **kw):
        self._logger = self._logger.bind(**kw)

    def get_logger(self, *args, **kw):
        # Bind module specific keys with the fresh values.
        # I mean, who does't like fresh!
        if 'module' not in kw and args:
            kw['module'] = args[0]

        return self._logger.bind(**kw)


def compose(*functions):
    """
    Composes n functions. Composition of two functions `f` and `g`
    would be `f(g(x))`. Similarly, for *n* functions, f1, f2, ..., fn,
    the composition would be: fn(fn-1(...f2(f1(x))...)) for input `x`.
    @param functions: list of functions to be composed
    @return: the composed function
    """
    return functools.reduce(lambda f, g: lambda x: f(g(x)), functions, lambda x: x)


class Proxy(object):
    def __init__(self, delegate):
        """
        @param delegate: the object for which this class
        acts as a proxy. It could be an object or a callable
        """
        self._delegate = delegate

    def __getattr__(self, name):
        """
        Fetches the desired attribute from the delegate
        @param name: name of the attribute
        @return: the requested attribute if present, else `None`
        """
        self._before_advice()
        return getattr(self._get_delegate(), name, None)

    def _get_delegate(self):
        """
        Returns the delegated object. If the object is callable,
        it invokes the function to get the delegated object
        @return: the delegated object
        """
        if callable(self._delegate):
            return self._delegate()
        return self._delegate

    def _before_advice(self):
        pass


def find_caller():
    """
    Finds the first caller frame outside of glogger so that the caller
    info is populated for wrapping stdlib.
    """
    f, _ = _find_first_app_frame_and_name(['logging'])
    return f.f_code.co_filename, f.f_lineno, f.f_code.co_name


def _find_first_app_frame_and_name(additional_ignores=None):
    """
    Remove all intra-glogger calls and return the relevant app frame.

    :param additional_ignores: Additional names with which the first frame must
        not start.
    :type additional_ignores: `list` of `str` or `None`

    :rtype: tuple of (frame, name)
    """
    ignores = ["glogger"] + (additional_ignores or [])
    f = sys._getframe()
    name = f.f_globals.get("__name__") or "?"
    while any(name.startswith(i) for i in ignores):
        if f.f_back is None:
            name = "?"
            break
        f = f.f_back
        name = f.f_globals.get("__name__") or "?"
    return f, name
