"""The ColoredFormatter class."""

import logging
import os
import sys
import typing
import traceback
import io

import colorlog.escape_codes

__all__ = (
    "default_log_colors",
    "ColoredFormatter",
    "LevelFormatter",
    "TTYColoredFormatter",
)

# Type aliases used in function signatures.
EscapeCodes = typing.Mapping[str, str]
LogColors = typing.Mapping[str, str]
SecondaryLogColors = typing.Mapping[str, LogColors]
if sys.version_info >= (3, 8):
    _FormatStyle = typing.Literal["%", "{", "$"]
else:
    _FormatStyle = str

# The default colors to use for the debug levels
default_log_colors = {
    "DEBUG": "white",
    "INFO": "green",
    "WARNING": "yellow",
    "ERROR": "red",
    "CRITICAL": "bold_red",
}

# The default format to use for each style
default_formats = {
    "%": "%(log_color)s%(levelname)s:%(name)s:%(message)s",
    "{": "{log_color}{levelname}:{name}:{message}",
    "$": "${log_color}${levelname}:${name}:${message}",
}


class ColoredRecord:
    """
    Wraps a LogRecord, adding escape codes to the internal dict.

    The internal dict is used when formatting the message (by the PercentStyle,
    StrFormatStyle, and StringTemplateStyle classes).
    """

    def __init__(self, record: logging.LogRecord, escapes: EscapeCodes) -> None:
        self.__dict__.update(record.__dict__)
        self.__dict__.update(escapes)


class ColoredFormatter(logging.Formatter):
    """
    A formatter that allows colors to be placed in the format string.

    Intended to help in creating more readable logging output.
    """

    def __init__(
        self,
        fmt: typing.Optional[str] = None,
        datefmt: typing.Optional[str] = None,
        style: _FormatStyle = "%",
        log_colors: typing.Optional[LogColors] = None,
        reset: bool = True,
        secondary_log_colors: typing.Optional[SecondaryLogColors] = None,
        validate: bool = True,
        stream: typing.Optional[typing.IO] = None,
        no_color: bool = False,
        force_color: bool = False,
        defaults: typing.Optional[typing.Mapping[str, typing.Any]] = None,
    ) -> None:
        """
        Set the format and colors the ColoredFormatter will use.

        The ``fmt``, ``datefmt``, ``style``, and ``default`` args are passed on to the
        ``logging.Formatter`` constructor.

        The ``secondary_log_colors`` argument can be used to create additional
        ``log_color`` attributes. Each key in the dictionary will set
        ``{key}_log_color``, using the value to select from a different
        ``log_colors`` set.

        :Parameters:
        - fmt (str): The format string to use.
        - datefmt (str): A format string for the date.
        - log_colors (dict):
            A mapping of log level names to color names.
        - reset (bool):
            Implicitly append a color reset to all records unless False.
        - style ('%' or '{' or '$'):
            The format style to use.
        - secondary_log_colors (dict):
            Map secondary ``log_color`` attributes. (*New in version 2.6.*)
        - validate (bool)
            Validate the format string.
        - stream (typing.IO)
            The stream formatted messages will be printed to. Used to toggle colour
            on non-TTY outputs. Optional.
        - no_color (bool):
            Disable color output.
        - force_color (bool):
            Enable color output. Takes precedence over `no_color`.
        """

        # Select a default format if `fmt` is not provided.
        fmt = default_formats[style] if fmt is None else fmt

        if sys.version_info >= (3, 10):
            super().__init__(fmt, datefmt, style, validate, defaults=defaults)
        elif sys.version_info >= (3, 8):
            super().__init__(fmt, datefmt, style, validate)
        else:
            super().__init__(fmt, datefmt, style)

        self.log_colors = log_colors if log_colors is not None else default_log_colors
        self.secondary_log_colors = (
            secondary_log_colors if secondary_log_colors is not None else {}
        )
        self.reset = reset
        self.stream = stream
        self.no_color = no_color
        self.force_color = force_color

    def formatMessage(self, record: logging.LogRecord) -> str:
        """Format a message from a record object."""
        escapes = self._escape_code_map(record.levelname)
        wrapper = ColoredRecord(record, escapes)
        message = super().formatMessage(wrapper)  # type: ignore
        message = self._append_reset(message, escapes)
        return message

    def _escape_code_map(self, item: str) -> EscapeCodes:
        """
        Build a map of keys to escape codes for use in message formatting.

        If _color() returns False, all values will be an empty string.
        """
        codes = {**colorlog.escape_codes.escape_codes}
        codes.setdefault("log_color", self._get_escape_code(self.log_colors, item))
        for name, colors in self.secondary_log_colors.items():
            codes.setdefault("%s_log_color" % name, self._get_escape_code(colors, item))
        if not self._colorize():
            codes = {key: "" for key in codes.keys()}
        return codes

    def _colorize(self):
        """Return False if we should be prevented from printing escape codes."""
        if self.force_color or "FORCE_COLOR" in os.environ:
            return True

        if self.no_color or "NO_COLOR" in os.environ:
            return False

        if self.stream is not None and not self.stream.isatty():
            return False

        return True

    @staticmethod
    def _get_escape_code(log_colors: LogColors, item: str) -> str:
        """Extract a color sequence from a mapping, and return escape codes."""
        return colorlog.escape_codes.parse_colors(log_colors.get(item, ""))

    def _append_reset(self, message: str, escapes: EscapeCodes) -> str:
        """Add a reset code to the end of the message, if it's not already there."""
        reset_escape_code = escapes["reset"]

        if self.reset and not message.endswith(reset_escape_code):
            message += reset_escape_code

        return message

    if sys.version_info >= (3, 13):

        def formatException(self, ei) -> str:
            """
            Format and return the specified exception information as a string.

            This is a copy of logging.Formatter.formatException that passes in
            an appropriate value for colorize to print_exception.
            """
            kwargs = dict(colorize=self._colorize())

            sio = io.StringIO()
            tb = ei[2]
            traceback.print_exception(ei[0], ei[1], tb, limit=None, file=sio, **kwargs)
            s = sio.getvalue()
            sio.close()
            if s[-1:] == "\n":
                s = s[:-1]
            return s


class LevelFormatter:
    """An extension of ColoredFormatter that uses per-level format strings."""

    def __init__(self, fmt: typing.Mapping[str, str], **kwargs: typing.Any) -> None:
        """
        Configure a ColoredFormatter with its own format string for each log level.

        Supports fmt as a dict. All other args are passed on to the
        ``colorlog.ColoredFormatter`` constructor.

        :Parameters:
        - fmt (dict):
            A mapping of log levels (represented as strings, e.g. 'WARNING') to
            format strings. (*New in version 2.7.0)

            Levels that are not present in the mapping fall back to a default
            formatter, so records logged at custom or unlisted levels are
            formatted rather than raising a ``KeyError``. To customise the
            fallback, provide a format string under the special ``"DEFAULT"``
            key; otherwise the default format for the chosen ``style`` is used.
        (All other parameters are the same as in colorlog.ColoredFormatter)

        Example:

        formatter = colorlog.LevelFormatter(
            fmt={
                "DEBUG": "%(log_color)s%(message)s (%(module)s:%(lineno)d)",
                "INFO": "%(log_color)s%(message)s",
                "WARNING": "%(log_color)sWRN: %(message)s (%(module)s:%(lineno)d)",
                "ERROR": "%(log_color)sERR: %(message)s (%(module)s:%(lineno)d)",
                "CRITICAL": "%(log_color)sCRT: %(message)s (%(module)s:%(lineno)d)",
            }
        )
        """
        self.formatters = {
            level: ColoredFormatter(fmt=f, **kwargs) for level, f in fmt.items()
        }
        # Used for any level not present in ``fmt``. An explicit "DEFAULT" entry
        # takes precedence; otherwise fall back to the default format string.
        self.default_formatter = self.formatters.get(
            "DEFAULT", ColoredFormatter(**kwargs)
        )

    def format(self, record: logging.LogRecord) -> str:
        formatter = self.formatters.get(record.levelname, self.default_formatter)
        return formatter.format(record)


# Provided for backwards compatibility. The features provided by this subclass are now
# included directly in the `ColoredFormatter` class.
TTYColoredFormatter = ColoredFormatter
