# elog/__init__.py - elog core functions
# Copyright 2006-2025 Gentoo Authors
# Distributed under the terms of the GNU General Public License v2

import os

from portage.const import EBUILD_PHASES
from portage.elog.filtering import filter_loglevels
from portage.elog.messages import collect_ebuild_messages, collect_messages
from portage.exception import AlarmSignal, PortageException
from portage.localization import _
from portage.process import atexit_register


def _merge_logentries(a, b):
    rValue = {}
    phases = set(a)
    phases.update(b)
    for p in phases:
        merged_msgs = []
        rValue[p] = merged_msgs
        for d in a, b:
            msgs = d.get(p)
            if msgs:
                merged_msgs.extend(msgs)
    return rValue


def _combine_logentries(logentries):
    # generate a single string with all log messages
    rValue = []
    for phase in EBUILD_PHASES:
        if phase not in logentries:
            continue
        previous_type = None
        for msgtype, msgcontent in logentries[phase]:
            if previous_type != msgtype:
                previous_type = msgtype
                rValue.append(f"{msgtype}: {phase}")
            if isinstance(msgcontent, str):
                rValue.append(msgcontent.rstrip("\n"))
            else:
                for line in msgcontent:
                    rValue.append(line.rstrip("\n"))
    if rValue:
        rValue.append("")
    return "\n".join(rValue)


_elog_mod_imports = {}


def _load_mod(name):
    global _elog_mod_imports
    m = _elog_mod_imports.get(name)
    if m is None:
        m = __import__(name)
        for comp in name.split(".")[1:]:
            m = getattr(m, comp)
        _elog_mod_imports[name] = m
    return m


_elog_listeners = []


def add_listener(listener):
    """
    Listeners should accept four arguments: settings, key, logentries and logtext
    """
    _elog_listeners.append(listener)


def remove_listener(listener):
    """
    Remove previously added listener
    """
    _elog_listeners.remove(listener)


_elog_atexit_handlers = []


def elog_process(cpv, mysettings, phasefilter=None):
    from portage.util import writemsg

    global _elog_atexit_handlers

    logsystems = mysettings.get("PORTAGE_ELOG_SYSTEM", "").split()
    for s in logsystems:
        # allow per module overrides of PORTAGE_ELOG_CLASSES
        if ":" in s:
            s, levels = s.split(":", 1)
            levels = levels.split(",")
        # - is nicer than _ for module names, so allow people to use it.
        s = s.replace("-", "_")
        try:
            _load_mod("portage.elog.mod_" + s)
        except ImportError:
            pass

    if "T" in mysettings:
        ebuild_logentries = collect_ebuild_messages(
            os.path.join(mysettings["T"], "logging")
        )
    else:
        # A build dir isn't necessarily required since the messages.e*
        # functions allow messages to be generated in-memory.
        ebuild_logentries = {}
    all_logentries = collect_messages(key=cpv, phasefilter=phasefilter)
    if cpv in all_logentries:
        # Messages generated by the python elog implementation are assumed
        # to come first. For example, this ensures correct order for einfo
        # messages that are generated prior to the setup phase.
        all_logentries[cpv] = _merge_logentries(all_logentries[cpv], ebuild_logentries)
    else:
        all_logentries[cpv] = ebuild_logentries

    my_elog_classes = set(mysettings.get("PORTAGE_ELOG_CLASSES", "").split())
    logsystems = {}
    for token in mysettings.get("PORTAGE_ELOG_SYSTEM", "").split():
        if ":" in token:
            s, levels = token.split(":", 1)
            levels = levels.split(",")
        else:
            s = token
            levels = ()
        levels_set = logsystems.get(s)
        if levels_set is None:
            levels_set = set()
            logsystems[s] = levels_set
        levels_set.update(levels)

    for key in all_logentries:
        default_logentries = filter_loglevels(all_logentries[key], my_elog_classes)

        # in case the filters matched all messages and no module overrides exist
        if len(default_logentries) == 0 and (
            ":" not in mysettings.get("PORTAGE_ELOG_SYSTEM", "")
        ):
            continue

        default_fulllog = _combine_logentries(default_logentries)

        # call listeners
        for listener in _elog_listeners:
            listener(mysettings, str(key), default_logentries, default_fulllog)

        # pass the processing to the individual modules
        for s, levels in logsystems.items():
            # allow per module overrides of PORTAGE_ELOG_CLASSES
            if levels:
                mod_logentries = filter_loglevels(all_logentries[key], levels)
                mod_fulllog = _combine_logentries(mod_logentries)
            else:
                mod_logentries = default_logentries
                mod_fulllog = default_fulllog
            if len(mod_logentries) == 0:
                continue
            # - is nicer than _ for module names, so allow people to use it.
            s = s.replace("-", "_")
            try:
                m = _load_mod("portage.elog.mod_" + s)
                # Timeout after one minute (in case something like the mail
                # module gets hung).
                try:
                    AlarmSignal.register(60)
                    m.process(mysettings, str(key), mod_logentries, mod_fulllog)
                finally:
                    AlarmSignal.unregister()
                if hasattr(m, "finalize") and m.finalize not in _elog_atexit_handlers:
                    _elog_atexit_handlers.append(m.finalize)
                    atexit_register(m.finalize)
            except (ImportError, AttributeError) as e:
                writemsg(
                    _(
                        "!!! Error while importing logging modules "
                        'while loading "mod_%s":\n'
                    )
                    % str(s)
                )
                writemsg(f"{e!s}\n", noiselevel=-1)
            except AlarmSignal:
                writemsg(f"Timeout in elog_process for system '{s}'\n", noiselevel=-1)
            except PortageException as e:
                writemsg(f"{e!s}\n", noiselevel=-1)
