"""Layer 3 — Full top-level handler wiring + report shape + redaction.

Production-shaped crash reporting. Demonstrates the patterns from
the ERROR_HANDLING standard §6 and SECURITY_STANDARD §11
(mgf-common standards ERROR_HANDLING / mgf-common standards SECURITY_STANDARD):

  • Both excepthooks installed at the entry point, idempotently
    (rule EH-04). Hooks chain to the prior hook so other
    excepthook-installers (pytest, ipython, IDEs) keep working.

  • The crash reporter NEVER raises (rule EH-05). Failures during
    build / write / ship are caught and logged at WARNING.

  • Local report under <state>/crashes/<timestamp>-<short-id>.json,
    written atomically (mode 0o600 — rule CF-04 + SC-04).

  • Cause chain preserved (rule EH-03). Exceptions raised with
    ``raise Y(...) from x`` carry the original cause; the crash
    report includes both.

  • Redactor scrubs the payload before write (rule SC-09 + SC-11).
    Sensitive fields in the exception's args don't leak into the
    on-disk report.

  • Optional remote sink (rule §6.1 of ERROR_HANDLING.md). Setting
    the ``<APP>_CRASH_SINK`` env var routes the report to Sentry,
    OTLP, or an HTTPS webhook in addition to the local file. We
    don't enable a real sink here (no network); the comment shows
    the env var.

The "report_now never raises" invariant is the load-bearing
property — the crash reporter is the last line of defence; if it
itself raised, every other top-level handler in the application
would amplify the problem instead of containing it.

Run with::

    python examples/05_crash_reporting/03_advanced.py
"""

from __future__ import annotations

import json
import logging
import os
import sys
import tempfile
import threading
from pathlib import Path

import mgf.common
from mgf.common.exceptions import AppError, GuestUnreachableError
from mgf.common.observability import report_now
from mgf.common.settings import LoggingSettings, MgfSettings


def _show_report(path: Path, label: str) -> None:
    """Pretty-print the crash report at ``path``."""
    print(f"\n─── {label} ───────────────────────────────")
    print(f"  path: {path}")
    print(f"  mode: {oct(path.stat().st_mode)[-3:]}  (rule CF-04 / SC-04: 0o600)")
    r = json.loads(path.read_text())
    print(f"  timestamp: {r['timestamp']}")
    print(f"  source: {r['source']!r}")
    print(f"  app: {r['app']!r}  version: {r['version']!r}")
    print(f"  report_id: {r['report_id'][:8]}…")
    print(f"  exception.type: {r['exception']['type']}")
    print(f"  exception.message: {r['exception']['message']!r}")
    if r.get("cause"):
        print(f"  cause.type: {r['cause']['type']}")
        print(f"  cause.message: {r['cause']['message']!r}")
    tb_lines = r.get("traceback", [])
    if tb_lines:
        print(f"  traceback: {len(tb_lines)} lines (first: {tb_lines[0].rstrip()!r})")


def main() -> None:
    with tempfile.TemporaryDirectory() as tmp:
        # State dir isolation — crash reports land under a tmp dir,
        # never the user's actual ~/.local/state.
        os.environ["XDG_STATE_HOME"] = tmp
        os.environ["HOME"] = tmp

        # bootstrap() wires identity + logging + OTel + excepthooks
        # in one call. Crash reports' `app` field reads from identity.
        # Logging lands the reporter's own warnings (if any) and the
        # unhandled-exception ERROR records; crash reports themselves
        # are JSON files written by the local sink.
        ctx = mgf.common.bootstrap(
            app_name="example-advanced-crash",
            app_version="0.1.0",
            settings=MgfSettings(
                logging=LoggingSettings(
                    level="DEBUG", format="json", file=Path(tmp) / "myapp.log",
                ),
            ),
        )

        LOG = logging.getLogger(__name__)

        # ── 1. bootstrap() installed the hooks (rule EH-04) ──
        #
        # The bootstrap call above already wired the sys + threading
        # excepthooks transactionally. The hooks are idempotent — the
        # marker on the installed wrapper makes a second install a
        # no-op. We swap sys.excepthook with a no-op below ONLY to
        # stop pytest's prior hook from capturing our synthetic
        # exception (same trick our test suite uses — see
        # tests/unit/observability/test_excepthooks.py).
        prior_excepthook = sys.excepthook
        try:
            LOG.info("excepthooks installed by bootstrap (idempotent)")
            sys.excepthook = lambda *_args: None

            # ── 2. Direct in-band report ────────────────────
            try:
                raise GuestUnreachableError("hv-east-3 timed out after 30s")
            except AppError as e:
                path1 = report_now(e, source="cli")
            _show_report(path1, "1. Direct report_now in CLI catch-all")

            # ── 3. Cause chain preserved (rule EH-03) ───────
            try:
                try:
                    raise OSError("ECONNREFUSED")
                except OSError as raw:
                    raise GuestUnreachableError("upstream gateway down") from raw
            except AppError as e:
                path2 = report_now(e, source="cli")
            _show_report(path2, "2. Cause chain")

            # ── 4. Worker-thread crash routed by hook ───────
            def _bad_worker() -> None:
                raise RuntimeError("worker tripped on a None")

            t = threading.Thread(target=_bad_worker, name="demo-worker")
            t.start()
            t.join()
            files = sorted(
                (Path(tmp) / mgf.common.app_name() / "crashes").glob("*.json"),
                key=lambda p: p.stat().st_mtime,
            )
            _show_report(files[-1], "3. Worker-thread crash (auto-routed)")

            # ── 5. Simulated main-thread uncaught exception ─
            # We invoke the installed sys.excepthook directly so the
            # example doesn't exit non-zero. The hook chain logs at
            # ERROR + writes a crash report + delegates to the
            # next hook in the chain.
            try:
                raise ValueError("simulated unhandled exception")
            except ValueError as e:
                sys.excepthook(type(e), e, e.__traceback__)
            files = sorted(
                (Path(tmp) / mgf.common.app_name() / "crashes").glob("*.json"),
                key=lambda p: p.stat().st_mtime,
            )
            _show_report(files[-1], "4. Main-thread uncaught (via sys.excepthook)")

            # ── 6. Reporter NEVER raises (rule EH-05) ───────
            # Even in degenerate cases (synthetic BaseException) the
            # reporter returns a Path (real or sentinel) without
            # propagating. Demonstrates the last-line-of-defence
            # invariant.
            try:
                report_now(BaseException("synthetic"), source="degenerate")
                print("\n✓ report_now never raised (rule EH-05 invariant)")
            except Exception as e:  # noqa: BLE001 — would be a defect
                print(f"\n✗ report_now raised — defect! {e}")

            # ── 7. Aggregate count ──────────────────────────
            crash_dir = Path(tmp) / mgf.common.app_name() / "crashes"
            all_reports = sorted(crash_dir.glob("*.json"))
            print(f"\n─── Summary ────────────────────────────────")
            print(f"  total crash reports written: {len(all_reports)}")
            print(f"  directory: {crash_dir}")
            print(
                "  (in production: set <APP>_CRASH_SINK=sentry|otlp|https://... "
                "to forward each report to a remote sink as well)"
            )

        finally:
            sys.excepthook = prior_excepthook
            ctx.shutdown()


if __name__ == "__main__":
    main()
