'''
logging.py - this file is part of S3QL.

Copyright © 2008 Nikolaus Rath <Nikolaus@rath.org>

This work can be distributed under the terms of the GNU GPLv3.
'''

import logging
import logging.handlers
import sys
import os.path

class QuietError(Exception):
    '''
    QuietError is the base class for exceptions that should not result
    in a stack trace being printed.

    It is typically used for exceptions that are the result of the user
    supplying invalid input data. The exception argument should be a
    string containing sufficient information about the problem.
    '''

    def __init__(self, msg='', exitcode=1):
        super().__init__()
        self.msg = msg

        #: Exit code to use when terminating process
        self.exitcode = exitcode

    def __str__(self):
        return self.msg


SYSTEMD_LOG_LEVEL_MAP = {
    logging.CRITICAL: 0,
    logging.ERROR: 3,
    logging.WARNING: 4,
    logging.INFO: 6,
    logging.DEBUG: 7,
}

class SystemdFormatter(logging.Formatter):
    def format(self, record):
        s = super().format(record)
        prefix = SYSTEMD_LOG_LEVEL_MAP.get(record.levelno, None)
        if prefix:
            s = '<%d>%s' % (prefix, s)
        return s

class MyFormatter(logging.Formatter):
    '''Prepend severity to log message if it exceeds threshold'''

    def format(self, record):
        s = super().format(record)
        if record.levelno > logging.INFO:
            s = '%s: %s' % (record.levelname, s)
        return s

def create_handler(target):
    '''Create logging handler for given target'''

    if target.lower() == 'syslog':
        handler = logging.handlers.SysLogHandler('/dev/log')
        formatter = logging.Formatter(os.path.basename(sys.argv[0])
                                       + '[%(process)s:%(threadName)s] '
                                       + '%(name)s.%(funcName)s: %(message)s')

    else:
        fullpath = os.path.expanduser(target)
        dirname = os.path.dirname(fullpath)
        if dirname and not os.path.exists(dirname):
            try:
                os.makedirs(dirname)
            except PermissionError:
                raise QuietError('No permission to create log file %s' % fullpath,
                                 exitcode=10)

        try:
            handler = logging.handlers.RotatingFileHandler(fullpath,
                                                           maxBytes=10 * 1024**2, backupCount=5)
        except PermissionError:
            raise QuietError('No permission to write log file %s' % fullpath,
                             exitcode=10)

        formatter = logging.Formatter('%(asctime)s.%(msecs)03d %(process)s:%(threadName)s '
                                      '%(name)s.%(funcName)s: %(message)s',
                                      datefmt="%Y-%m-%d %H:%M:%S")

    handler.setFormatter(formatter)
    return handler

def setup_logging(options):

    # We want to be able to detect warnings and higher severities
    # in the captured test output. 'critical' has too many potential
    # false positives, so we rename this level to "FATAL".
    logging.addLevelName(logging.CRITICAL, 'FATAL')

    root_logger = logging.getLogger()
    if root_logger.handlers:
        root_logger.debug("Logging already initialized.")
        return

    stdout_handler = add_stdout_logging(options.quiet, getattr(options, 'systemd', False))
    if hasattr(options, 'log') and options.log:
        root_logger.addHandler(create_handler(options.log))
    elif options.debug and (not hasattr(options, 'log') or not options.log):
        # When we have debugging enabled but no separate log target,
        # make stdout logging more detailed.
        formatter = logging.Formatter('%(asctime)s.%(msecs)03d %(process)s %(levelname)-8s '
                                      '%(threadName)s %(name)s.%(funcName)s: %(message)s',
                                      datefmt="%Y-%m-%d %H:%M:%S")
        stdout_handler.setFormatter(formatter)
        stdout_handler.setLevel(logging.NOTSET)

    setup_excepthook()

    if options.debug:
        if 'all' in options.debug:
            root_logger.setLevel(logging.DEBUG)
        else:
            root_logger.setLevel(logging.INFO)
            for module in options.debug:
                logging.getLogger(module).setLevel(logging.DEBUG)

        logging.disable(logging.NOTSET)
    else:
        root_logger.setLevel(logging.INFO)
        logging.disable(logging.DEBUG)
    logging.captureWarnings(capture=True)

    return stdout_handler

def setup_excepthook():
    '''Modify sys.excepthook to log exceptions

    Also makes sure that exceptions derived from `QuietException`
    do not result in stacktraces.
    '''

    def excepthook(type_, val, tb):
        root_logger = logging.getLogger()
        if isinstance(val, QuietError):
            root_logger.error(val.msg)
            sys.exit(val.exitcode)
        else:
            root_logger.error('Uncaught top-level exception:',
                              exc_info=(type_, val, tb))
            sys.exit(1)

    sys.excepthook = excepthook

def add_stdout_logging(quiet=False, systemd=False):
    '''Add stdout logging handler to root logger'''

    root_logger = logging.getLogger()
    if systemd:
        formatter = SystemdFormatter('%(message)s')
    else:
        formatter = MyFormatter('%(message)s')
    handler = logging.StreamHandler(sys.stderr)
    if not systemd and quiet:
        handler.setLevel(logging.WARNING)
    else:
        handler.setLevel(logging.INFO)
    handler.setFormatter(formatter)
    root_logger.addHandler(handler)
    return handler

class Logger(logging.getLoggerClass()):
    '''
    This class has the following features in addition to `logging.Logger`:

    * Log messages that are emitted with an *log_once* attribute in the
      *extra* parameter are only emitted once per logger.
    '''

    def __init__(self, name):
        super().__init__(name)
        self.log_cache = set()

    def handle(self, record):
        if hasattr(record, 'log_once') and record.log_once:
            id_ = hash((record.name, record.levelno, record.msg,
                        record.args, record.exc_info))
            if id_ in self.log_cache:
                return
            self.log_cache.add(id_)

        return super().handle(record)
logging.setLoggerClass(Logger)

# Convenience object for use in logging calls, e.g.
# log.warning('This will be printed only once', extra=LOG_ONCE)
LOG_ONCE = { 'log_once': True }