# Copyright 2026 SigmaSRC, Inc. All Rights Reserved.

"""Logging configuration.

This module defines one function, setupLogging(), which should be
called by main() to initialize the standard Python logging module to
our taste.

*** NOTICE ***

For subtle initialization reasons, this module should be imported
before any other Sigma modules.  Otherwise some other Sigma
module, imported before this one, might create a global logger object
that is not an instance of MyLogger below.

"""

# Python standard library imports
import os
import sys
import time
import logging

# Sigma imports
from sigma.util import config
from sigma.util import xmltime
from sigma.util import timeskew

# Constants
DEFAULT_FORMAT = "%(asctime).19s:%(name)s:%(levelname)s:%(message)s"
DEFAULT_LEVEL = logging.INFO

def setupLogging(options=None):
    """Initialize logging.

    The options argument is an optional Options object specifying the
    configuration options.  By default config.Options() is
    instantiated without arguments.

    If this is called a second time, or after a root handler is
    already established, this has no effect.

    """
    # Return if the root logger already has a hander
    root = logging.getLogger()
    if root.handlers:
        return

    # Instantiate config.Options if not passed in
    if options is None:
        options = config.Options()

    # Create a logging formatter
    format = options.log_format or DEFAULT_FORMAT
    # Allow newlines in the log format (XXX this won't let you escape the \n)
    format = format.replace("\\n", "\n")
    formatter = logging.Formatter(format)

    # Log to a file
    handler = SharedFileHandler(options)
    handler.setFormatter(formatter)
    root.addHandler(handler)

    # Also log to stderr, if it is a tty. sys.stderr is None when running as a
    # Windows service (session 0, no console), so guard the isatty() call —
    # otherwise it raises AttributeError at import and the service dies before
    # it can connect to the SCM (Windows then reports a start timeout / 1053).
    if sys.stderr is not None and sys.stderr.isatty():
        handler = logging.StreamHandler(sys.stderr)
        handler.setFormatter(formatter)
        root.addHandler(handler)

    # Set the log level
    root.setLevel(DEFAULT_LEVEL)
    if options.log_level:
        level = levelnames.get(options.log_level.lower())
        if level is None:
            root.warn("Unrecognized logging level: %r", options.log_level)
        else:
            root.setLevel(level)

    # Ignore exceptions during logging -- handling them makes things worse
    logging.raiseExceptions = False

levelnames = {
    'critical': logging.CRITICAL,
    'fatal': logging.CRITICAL,
    'error': logging.ERROR,
    'warn': logging.WARNING,
    'warning': logging.WARNING,
    'info': logging.INFO,
    'debug': logging.DEBUG,
}

class SharedFileHandler(logging.Handler):

    """Similar to logging.FileHandler, but with a variable filename.

    The filename is created by appending the date to a base name (and
    appending a .txt suffix).  This way we get one log file per day
    without ever having to move log files.  A separate utility can
    dispose of old log files properly.

    Whenever we start a new log file, we begin by writing a record to
    it showing the time in UTC; this can be used to determine the
    difference between UTC and local time.

    We don't keep the log file open; this file we always use the
    correct log file.  XXX It is a bit slow, and an optimization is
    possible (assuming multiple processes can have the same file open
    simultaneously in 'append' mode without getting their output
    garbled).

    XXX We use local time.  This seems to make the most sense to me.

    """

    def __init__(self, options):
        self.options = options

        # Create the log directory if necessary
        logdir = options.agentlogs
        if not os.path.isdir(logdir):
            try:
                os.makedirs(logdir)
            except os.error as err:
                if not os.path.isdir(logdir):
                    raise

        # Compute the base filename
        self.baseFilename = os.path.join(logdir, options.log_prefix)

        # Initialize the base class
        super().__init__()

    def emit(self, record):
        try:
            y, m, d = time.localtime(record.created)[:3]
            filename = "%s.%04d-%02d-%02d.txt" % (self.baseFilename, y, m, d)
            new = not os.path.exists(filename)
            msg = self.format(record)
            f = open(filename, "a")
            try:
                if new:
                    self.logFirstLine(f)
                f.write(msg + "\n")
            finally:
                f.close()
        except Exception:
            self.handleError(record)

    def logFirstLine(self, f):
        """Write the current time in UTC and the hostname to the log file."""
        try:
            import socket
            hostname = socket.gethostname()
        except Exception:
            hostname = "?"
        skew = timeskew.get_skew(self.options)
        message = "UTC=%s skew=%s hostname=%s" % (xmltime.formatnow(),
                                                  skew,
                                                  hostname)
        record = logging.LogRecord("util.avalog",
                                   logging.INFO,
                                   "",
                                   0,
                                   message,
                                   (),
                                   None)
        msg = self.format(record)
        f.write(msg + "\n")

# Logger and LogRecord subclasses

class MyLogger(logging.Logger):

    """Logger subclass to use our own LogRecord subclass."""

    def __init__(self, name):
        super().__init__(name)

    def makeRecord(self, name, level, fn, lno, msg, args, exc_info,
                   func=None, extra=None, sinfo=None):
        rv = MyLogRecord(name, level, fn, lno, msg, args, exc_info,
                         func, sinfo)
        if extra is not None:
            for key in extra:
                if (key in ["message", "asctime"]) or (key in rv.__dict__):
                    raise KeyError("Attempt to overwrite %r in LogRecord" % key)
                rv.__dict__[key] = extra[key]
        return rv

class MyLogRecord(logging.LogRecord):

    """LogRecord subclass that handles formatting errors gracefully."""

    def getMessage(self):
        try:
            return super().getMessage()
        except Exception:
            return str(self)

# Enable Logger and LogRecord subclasses; this must be done as a side
# effect of importing this module (see module doc string)
logging.setLoggerClass(MyLogger)
