# flake8: noqa
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
# You can obtain one at http://mozilla.org/MPL/2.0/.

import json

# Some of the build slave environments don't see the following when doing
# 'from logging import *'
# see https://bugzilla.mozilla.org/show_bug.cgi?id=700415#c35
from logging import *
from logging import addLevelName, basicConfig, debug
from logging import getLogger as getSysLogger
from logging import getLoggerClass, info, setLoggerClass, shutdown

_default_level = INFO
_LoggerClass = getLoggerClass()

# Define mozlog specific log levels
START = _default_level + 1
END = _default_level + 2
PASS = _default_level + 3
KNOWN_FAIL = _default_level + 4
FAIL = _default_level + 5
CRASH = _default_level + 6
# Define associated text of log levels
addLevelName(START, "TEST-START")
addLevelName(END, "TEST-END")
addLevelName(PASS, "TEST-PASS")
addLevelName(KNOWN_FAIL, "TEST-KNOWN-FAIL")
addLevelName(FAIL, "TEST-UNEXPECTED-FAIL")
addLevelName(CRASH, "PROCESS-CRASH")


class MozLogger(_LoggerClass):
    """
    MozLogger class which adds some convenience log levels
    related to automated testing in Mozilla and ability to
    output structured log messages.
    """

    def testStart(self, message, *args, **kwargs):
        """Logs a test start message"""
        self.log(START, message, *args, **kwargs)

    def testEnd(self, message, *args, **kwargs):
        """Logs a test end message"""
        self.log(END, message, *args, **kwargs)

    def testPass(self, message, *args, **kwargs):
        """Logs a test pass message"""
        self.log(PASS, message, *args, **kwargs)

    def testFail(self, message, *args, **kwargs):
        """Logs a test fail message"""
        self.log(FAIL, message, *args, **kwargs)

    def testKnownFail(self, message, *args, **kwargs):
        """Logs a test known fail message"""
        self.log(KNOWN_FAIL, message, *args, **kwargs)

    def processCrash(self, message, *args, **kwargs):
        """Logs a process crash message"""
        self.log(CRASH, message, *args, **kwargs)

    def log_structured(self, action, params=None):
        """Logs a structured message object."""
        if params is None:
            params = {}

        level = params.get("_level", _default_level)
        if isinstance(level, int):
            params["_level"] = getLevelName(level)
        else:
            params["_level"] = level
            level = getLevelName(level.upper())

            # If the logger is fed a level number unknown to the logging
            # module, getLevelName will return a string. Unfortunately,
            # the logging module will raise a type error elsewhere if
            # the level is not an integer.
            if not isinstance(level, int):
                level = _default_level

        params["action"] = action

        # The can message be None. This is expected, and shouldn't cause
        # unstructured formatters to fail.
        message = params.get("_message")

        self.log(level, message, extra={"params": params})


class JSONFormatter(Formatter):
    """Log formatter for emitting structured JSON entries."""

    def format(self, record):
        # Default values determined by logger metadata
        # pylint: disable=W1633
        output = {
            "_time": int(round(record.created * 1000, 0)),
            "_namespace": record.name,
            "_level": getLevelName(record.levelno),
        }

        # If this message was created by a call to log_structured,
        # anything specified by the caller's params should act as
        # an override.
        output.update(getattr(record, "params", {}))

        if record.msg and output.get("_message") is None:
            # For compatibility with callers using the printf like
            # API exposed by python logging, call the default formatter.
            output["_message"] = Formatter.format(self, record)

        return json.dumps(output, indent=output.get("indent"))


class MozFormatter(Formatter):
    """
    MozFormatter class used to standardize formatting
    If a different format is desired, this can be explicitly
    overriden with the log handler's setFormatter() method
    """

    level_length = 0
    max_level_length = len("TEST-START")

    def __init__(self, include_timestamp=False):
        """
        Formatter.__init__ has fmt and datefmt parameters that won't have
        any affect on a MozFormatter instance.

        :param include_timestamp: if True, include formatted time at the
                                  beginning of the message
        """
        self.include_timestamp = include_timestamp
        Formatter.__init__(self)

    def format(self, record):
        # Handles padding so record levels align nicely
        if len(record.levelname) > self.level_length:
            pad = 0
            if len(record.levelname) <= self.max_level_length:
                self.level_length = len(record.levelname)
        else:
            pad = self.level_length - len(record.levelname) + 1
        sep = "|".rjust(pad)
        fmt = "%(name)s %(levelname)s " + sep + " %(message)s"
        if self.include_timestamp:
            fmt = "%(asctime)s " + fmt
        # this protected member is used to define the format
        # used by the base Formatter's method
        self._fmt = fmt
        return Formatter(fmt=fmt).format(record)


def getLogger(name, handler=None):
    """
    Returns the logger with the specified name.
    If the logger doesn't exist, it is created.
    If handler is specified, adds it to the logger. Otherwise a default handler
    that logs to standard output will be used.

    :param name: The name of the logger to retrieve
    :param handler: A handler to add to the logger. If the logger already exists,
                    and a handler is specified, an exception will be raised. To
                    add a handler to an existing logger, call that logger's
                    addHandler method.
    """
    setLoggerClass(MozLogger)

    if name in Logger.manager.loggerDict:
        if handler:
            raise ValueError(
                "The handler parameter requires "
                + "that a logger by this name does "
                + "not already exist"
            )
        return Logger.manager.loggerDict[name]

    logger = getSysLogger(name)
    logger.setLevel(_default_level)

    if handler is None:
        handler = StreamHandler()
        handler.setFormatter(MozFormatter())

    logger.addHandler(handler)
    logger.propagate = False
    return logger
