
from __future__ import absolute_import
from __future__ import division

import collections
import datetime
import functools
import grp
import logging
import logging.handlers
import os
import pwd
import threading
import time

from dateutil import tz
from inspect import ismethod

import six

from . import concurrent


def funcName(func):
    if ismethod(func):
        return func.__func__.__name__

    if hasattr(func, 'func'):
        return func.func.__name__

    return func.__name__


def call2str(func, args, kwargs, printers={}):
    kwargs = kwargs.copy()
    varnames = func.__code__.co_varnames[:func.__code__.co_argcount]
    if ismethod(func):
        args = [func.__self__] + list(args)
        func = func.__func__

    for name, val in zip(varnames, args):
        kwargs[name] = val

    defaults = func.__defaults__ if func.__defaults__ else []

    for name, val in zip(varnames[-len(defaults):], defaults):
        if name not in kwargs:
            kwargs[name] = val

    argsStrs = []
    for i, argName in enumerate(varnames):
        if i == 0 and argName == "self":
            continue

        val = kwargs[argName]
        printer = printers.get(argName, repr)
        argsStrs.append("%s=%s" % (argName, printer(val)))

    return "%s(%s)" % (func.__name__, ", ".join(argsStrs))


class SimpleLogAdapter(logging.LoggerAdapter):
    warn = logging.LoggerAdapter.warning

    def __init__(self, logger, context):
        self.logger = logger
        items = ", ".join(
            "%s='%s'" % (k, v) for k, v in six.viewitems(context))
        self.prefix = "(%s) " % items

    def process(self, msg, kwargs):
        return self.prefix + msg, kwargs


class UserGroupEnforcingHandler(logging.handlers.WatchedFileHandler):

    def __init__(self, user, group, *args, **kwargs):
        self._uid = pwd.getpwnam(user).pw_uid
        self._gid = grp.getgrnam(group).gr_gid
        logging.handlers.WatchedFileHandler.__init__(self, *args, **kwargs)

        self.buffering = False

        self._open()

    def _open(self):
        if (os.geteuid() != self._uid) or (os.getegid() != self._gid):
            raise RuntimeError(
                "Attempt to open log with incorrect credentials")
        return logging.handlers.WatchedFileHandler._open(self)

    def flush(self):
        if self.buffering:
            return
        logging.handlers.WatchedFileHandler.flush(self)


class TimezoneFormatter(logging.Formatter):
    def converter(self, timestamp):
        return datetime.datetime.fromtimestamp(timestamp,
                                               tz.tzlocal())

    def formatTime(self, record, datefmt=None):
        ct = self.converter(record.created)
        if datefmt:
            s = ct.strftime(datefmt, ct)
        else:
            s = "%s,%03d%s" % (
                ct.strftime('%Y-%m-%d %H:%M:%S'),
                record.msecs,
                ct.strftime('%z')
            )
        return s


class ThreadedHandler(logging.handlers.MemoryHandler):

    STATS_INTERVAL = 60

    _CLOSED = object()

    def __init__(self, capacity=2000, adaptive=True, start=True):
        logging.handlers.MemoryHandler.__init__(self, 0)
        if adaptive:
            self._limits = [
                (logging.DEBUG, int(capacity * 0.6)),
                (logging.INFO, int(capacity * 0.7)),
                (logging.WARNING, int(capacity * 0.8)),
                (logging.ERROR, int(capacity * 0.9)),
                (logging.CRITICAL, capacity),
            ]
        else:
            self._limits = [(logging.CRITICAL, capacity)]
        self._target = _DROPPER
        self._queue = collections.deque()
        self._cond = threading.Condition(threading.Lock())
        self._last_report = time.time()
        self._dropped_records = 0
        self._max_pending = 0
        self._thread = concurrent.thread(self._run, name="logfile")
        if start:
            self.start()


    def createLock(self):
        self.lock = None

    def handle(self, record):
        with self._cond:
            if self._can_handle(record):
                self._queue.append(record)
                self._cond.notify()
            else:
                self._dropped_records += 1

            interval = record.created - self._last_report
            if interval < self.STATS_INTERVAL:
                return

            dropped_records = self._dropped_records
            max_pending = self._max_pending
            self._last_report = record.created
            self._dropped_records = 0
            self._max_pending = 0

        self._report_stats(interval, dropped_records, max_pending)

    def close(self):
        logging.Handler.close(self)
        self._queue.append(self._CLOSED)
        with self._cond:
            self._cond.notify()
        self._thread.join()
        self._target = _DROPPER


    def setTarget(self, target):
        self._target = target

    def flush(self):
        pass


    def start(self):
        self._thread.start()


    def _can_handle(self, record):
        size = len(self._queue)
        self._max_pending = max(size, self._max_pending)
        for level, limit in self._limits:
            if record.levelno <= level:
                return size < limit
        return True

    def _report_stats(self, interval, dropped_records, max_pending):
        if dropped_records:
            logging.critical(
                "ThreadedHandler is overloaded, dropped %d log messages in "
                "the last %d seconds (max pending: %d)",
                dropped_records, interval, max_pending)
        else:
            logging.debug(
                "ThreadedHandler is ok in the last %d seconds "
                "(max pending: %d)",
                interval, max_pending)

    def _run(self):
        while True:
            with self._cond:
                while len(self._queue) == 0:
                    self._cond.wait()

            self._target.buffering = True
            try:
                while len(self._queue):
                    record = self._queue.popleft()
                    if record is self._CLOSED:
                        return
                    self._target.handle(record)
            finally:
                self._target.buffering = False
                self._target.flush()

            record = None


class _Dropper(object):

    def handle(self, record):
        pass


_DROPPER = _Dropper()


class Suppressed(object):

    def __init__(self, value):
        self._value = value

    @property
    def value(self):
        return self._value

    def __repr__(self):
        return '(suppressed)'


class AllVmStatsValue(Suppressed):

    def __repr__(self):
        return repr({vm.get('vmId'): vm.get('status') for vm in self._value})


def set_level(level_name, name=''):
    log_level = logging.getLevelName(level_name)
    if not isinstance(log_level, type(logging.DEBUG)):
        raise ValueError("unknown log level: %r" % level_name)

    log_name = None if not name else name
    logger = logging.getLogger(log_name)
    logging.warning('Setting loglevel on %r to %s (%d)',
                    logger.name, level_name, log_level)
    logger.setLevel(log_level)


def volume_chain_to_str(base_first_chain):
    return ' < '.join(base_first_chain) + " (top)"


def traceback(log=None, msg="Unhandled exception"):
    def decorator(f):
        @functools.wraps(f)
        def wrapper(*a, **kw):
            try:
                return f(*a, **kw)
            except Exception:
                logger = log or logging.getLogger()
                logger.exception(msg)
        return wrapper
    return decorator
