#!/usr/bin/env bash

# Tell somebody when a program crashes.
#
# On GNOME, ABRT says so. Under a hand-assembled Hyprland desktop nothing does,
# and applications die silently -- which is most of how "Linux is flaky" gets
# earned. Fedora ships systemd-coredump by default, so the information is
# already there; nobody is reading it.
#
# Follows the journal for systemd-coredump's own message id and reports each
# program once per session.
#
# ONCE PER SESSION IS THE WHOLE DESIGN. This machine's portal backend crashes
# between eleven and sixty times a day -- see the portal-stability check in
# panama-doctor -- and a notification per crash would be a notification every
# few minutes for something the user can do nothing about. The first one is
# news; the fortieth is why people turn notifications off. The health page
# carries the running count for anyone who wants it.
#
# When an agent has been chosen, the notification stops being a dead end. It
# carries the diagnosis command as data in a `panama-exec` hint, which the shell
# runs on click. Command-as-data rather than a libnotify action, because an
# action would tie the click to this process still being alive to hear it, and
# this process is a `journalctl -f` that outlives nothing in particular. The
# hint survives a shell restart and never blocks the watcher.

set -uo pipefail

PANAMA_PATH="${PANAMA_PATH:-$HOME/.local/share/Panama}"
SETTINGS="${PANAMA_AGENT_SETTINGS:-${XDG_CONFIG_HOME:-$HOME/.config}/panama/settings.json}"

# systemd-coredump's MESSAGE_ID. Matching on this rather than on text keeps
# working when the wording changes and never matches a program that merely
# mentions the word "crash" in its own logs.
readonly COREDUMP_MESSAGE_ID='fc2e22bc6ee647b6b90729ab34a250b1'

command -v journalctl >/dev/null 2>&1 || exit 0
command -v notify-send >/dev/null 2>&1 || exit 0

# The shell owns org.freedesktop.Notifications, and the crash most worth
# reporting is the one that took the shell with it. Waiting means that report
# arrives rather than vanishing into a bus nobody is serving.
for _ in $(seq 1 60); do
    busctl --user status org.freedesktop.Notifications >/dev/null 2>&1 && break
    sleep 1
done

# Read per crash rather than once at startup, so choosing an agent in Settings
# takes effect on the next crash instead of on the next login. This service runs
# for the life of the session; nothing restarts it when a preference changes.
read_setting() {
    local key="$1" fallback="$2"
    [[ -r "$SETTINGS" ]] || { printf '%s' "$fallback"; return; }
    command -v jq >/dev/null 2>&1 || { printf '%s' "$fallback"; return; }
    jq -r --arg k "$key" --arg d "$fallback" \
        'if has($k) and (.[$k] != null) then (.[$k] | tostring) else $d end' \
        "$SETTINGS" 2>/dev/null || printf '%s' "$fallback"
}

# What to call the agent in a sentence aimed at a person.
agent_label() {
    case "$1" in
        claude) printf 'Claude Code' ;;
        codex) printf 'Codex' ;;
        *) printf '%s' "$1" ;;
    esac
}

declare -A reported=()

# -f from now, not from the boot: a session that starts after a crash should
# not open with a notification about something the user has already lived
# through and cannot act on.
journalctl --user -f -n 0 --output=json MESSAGE_ID="$COREDUMP_MESSAGE_ID" 2>/dev/null \
    | while IFS= read -r line; do
        [[ -n "$line" ]] || continue

        # One jq per entry rather than one per field: the fields are read
        # together, and the click payload needs all of them.
        IFS=$'\t' read -r uid exe comm pid signal < <(
            jq -r '[(.COREDUMP_UID // ""),
                    (.COREDUMP_EXE // ""),
                    (.COREDUMP_COMM // ""),
                    (.COREDUMP_PID // ""),
                    (.COREDUMP_SIGNAL_NAME // "")] | @tsv' <<<"$line" 2>/dev/null
        )

        # Another user's crash is not this session's business, and reporting it
        # would leak what they are running.
        [[ "$uid" == "$(id -u)" ]] || continue
        [[ -n "$exe" || -n "$comm" ]] || continue

        # The executable name first: COREDUMP_COMM is the kernel's comm field
        # and is truncated to fifteen characters, so it reports
        # "panama-test-cra" for a program called panama-test-crasher.
        if [[ -n "$exe" ]]; then
            program="$(basename "$exe")"
        else
            program="$comm"
        fi

        # Never announce our own machinery. A crash watcher that notifies about
        # the crash watcher, or about the agent it just launched to investigate
        # the last crash, is a loop with a toast in it.
        [[ "$program" == panama-crash-* || "$program" == panama-agent* ]] && continue

        [[ -z "${reported[$program]:-}" ]] || continue
        reported[$program]=1

        # The toast can only offer a diagnosis if there is something to diagnose
        # with. No agent, or the offer switched off, and it stays exactly the
        # actionless notification it has always been.
        agent="$(read_setting preferredAgent none)"
        offer="$(read_setting crashDiagnoseOffer true)"

        if [[ -n "$agent" && "$agent" != "none" && "$offer" != "false" && "$pid" =~ ^[0-9]+$ ]]; then
            # By absolute path, not by name. The shell runs this hint, and the
            # shell is started by systemd -- whose environment does not carry
            # the repository's bin directory on PATH, so a bare name would
            # click into "command not found".
            exec_command="$(printf '%q %q %q %q %q' \
                "$PANAMA_PATH/bin/panama-agent-crash" \
                "$pid" "${comm:-$program}" "${exe:-unknown}" "${signal:-unknown}")"

            # The hint is data, not privilege. Any process on this session bus
            # could send one, and running it grants nothing a local process
            # could not already do for itself.
            # Same urgency as the plain report, deliberately. Making the
            # clickable one critical would let a crash break through Do Not
            # Disturb, which is a louder desktop than anybody asked for in
            # exchange for an offer that keeps until it is read anyway.
            notify-send --icon=dialog-error-symbolic --app-name=Panama \
                --hint="string:panama-exec:$exec_command" \
                "$program stopped unexpectedly" \
                "Click to diagnose with $(agent_label "$agent")." \
                2>/dev/null || true
        else
            notify-send --icon=dialog-error-symbolic --app-name=Panama \
                "$program stopped unexpectedly" \
                "It crashed and was not able to recover. System Health has the details." \
                2>/dev/null || true
        fi
    done
