"""Non-main-thread unhandled-exception hook.

Without this, an uncaught exception in a thread is logged to stderr by
the Python runtime and otherwise lost — no crash report, no remote sink,
nothing in the application's structured log.

Idempotent — second call is a no-op.

See ``docs/standards/ERROR_HANDLING.md`` §4.3.
"""

from __future__ import annotations

import logging
import threading

from mgf.common._identity import app_name
from mgf.common.observability.crash_reporter import report_now
from mgf.common.observability.excepthook import _safe_emit

_INSTALLED_MARKER = "_mgf_common_threading_excepthook_installed"


def _wire_threading_excepthook() -> None:
    """Install :func:`threading.excepthook` wrapper (rule EH-04).

    Internal — called by :func:`mgf.common.bootstrap`. The public
    ``install_threading_excepthook`` shim was removed pre-v1.0;
    consumers MUST use bootstrap.
    """
    prior = threading.excepthook
    if getattr(prior, _INSTALLED_MARKER, False):
        return

    log = logging.getLogger(f"{app_name()}.unhandled")

    def hook(args: threading.ExceptHookArgs) -> None:
        # SystemExit raised inside a thread is the canonical "thread
        # is asked to stop" signal — don't crash-report it.
        if args.exc_type is SystemExit:
            return

        thread_name = args.thread.name if args.thread is not None else "?"
        # ``args.exc_value`` is typed as ``BaseException | None`` by
        # the stdlib, but in practice ``threading.excepthook`` only
        # fires when something was actually raised — the value is
        # ``None`` only for synthetic cases that real threads cannot
        # produce. Guard it explicitly so we never feed ``None`` into
        # the logger or crash reporter, and so mypy is happy with the
        # ``exc_info`` triple type.
        if args.exc_value is None:
            return
        # Hardened against closed-stderr cascade during teardown
        # (PAPER-60); see excepthook._safe_emit.
        _safe_emit(
            log,
            "unhandled exception in thread %s",
            (thread_name,),
            (args.exc_type, args.exc_value, args.exc_traceback),
        )
        report_now(args.exc_value, source=f"thread:{thread_name}")
        prior(args)

    setattr(hook, _INSTALLED_MARKER, True)
    threading.excepthook = hook


__all__: list[str] = []
