#!/bin/bash

# omarchy:summary=Watch for process crashes and offer an AI diagnosis
# omarchy:hidden=true

# systemd-coredump journals every core dump under a known MESSAGE_ID with
# structured COREDUMP_* fields, which carry more than the core filenames do.

set -uo pipefail

# See systemd.journal-fields(7).
readonly COREDUMP_MESSAGE_ID=fc2e22bc6ee647b6b90729ab34a250b1

# nf-md-robot_dead, escaped so this file reads without a Nerd Font.
readonly CRASH_GLYPH=$'\U000f16a1'

# Crash loops dump core repeatedly, so announce each program at most once a
# window.
readonly dedupe_seconds=${OMARCHY_CRASH_DEDUPE_SECONDS:-60}

# Extended regex of process names never worth announcing.
readonly ignore_pattern=${OMARCHY_CRASH_IGNORE:-}

declare -A last_notified

announce() {
  local comm=$1 pid=$2 exe=$3 signal=$4

  # The shell owns org.freedesktop.Notifications, so a shell crash takes the
  # notification server down with it and a toast sent into that gap is lost.
  # Wait for the restarted shell to claim the name again: the crash least
  # likely to be delivered is the one most worth reporting.
  omarchy-notification-wait || return 1

  # --exec rather than a libnotify action: the shell runs clicks from its own
  # hint and never emits ActionInvoked. Keeps the default "omarchy-action" app
  # name, the only one shouldBypassDnd() lets through. Crash details ride as
  # discrete argv words, so a hostile process name can't be reparsed as a
  # command. --exec consumes the rest of the line, so it comes last.
  omarchy-notification-send \
    --urgency critical \
    --glyph "$CRASH_GLYPH" \
    "Process crashed: $comm" \
    "Click to diagnose with AI" \
    --exec omarchy-agent-crash "$pid" "$comm" "$exe" "$signal"
}

# -n 0 so a restart does not re-announce crashes already dealt with.
journalctl -f -n 0 -o json "MESSAGE_ID=$COREDUMP_MESSAGE_ID" 2>/dev/null |
  while IFS= read -r entry; do
    # A dash for a field that is empty as well as one that is missing: tab is
    # IFS whitespace, so an empty field collapses into the next delimiter and
    # every field after it shifts along one. A process can set its own comm to
    # nothing, and that crash used to be read as somebody else's and dropped.
    IFS=$'\t' read -r uid comm pid exe signal < <(
      jq -r 'def field: if . == null or . == "" then "-" else . end;
             [(._UID | field),
              (.COREDUMP_COMM | field),
              (.COREDUMP_PID | field),
              (.COREDUMP_EXE | field),
              (.COREDUMP_SIGNAL_NAME | field)] | @tsv' <<<"$entry" 2>/dev/null
    )

    [[ $pid =~ ^[0-9]+$ ]] || continue

    # The toast only offers a diagnosis, so it has nothing to offer until an
    # agent is chosen. Checked per crash, not at startup, so picking one takes
    # effect without restarting this service.
    [[ -n $(omarchy-default-agent) ]] || continue

    # Only this user's crashes; a daemon dumping core is a sysadmin's problem.
    [[ $uid =~ ^[0-9]+$ ]] || continue
    ((uid == UID)) || continue

    # comm is truncated to 15 characters, so prefer the executable's basename.
    name=$comm
    [[ $exe == /* ]] && name=${exe##*/}

    # A process can set its own comm to anything prctl takes, slashes included,
    # and a crash with no recorded executable falls back to it. The mute below
    # turns this name into a path, so keep it one component: a crash must not
    # reach a flag outside crash-ignore/, nor have a diagnosis write one there.
    name=${name##*/}

    # What that leaves is not always a name. "/" leaves nothing, which is no
    # kind of array subscript and no kind of toast; a dot component names a
    # directory rather than a flag, so a mute on it would touch that directory
    # and then never match; and a dash is what the read above puts there when
    # the crash recorded no name at all.
    [[ -n $name && $name != "-" && $name != "." && $name != ".." ]] || name=unknown

    [[ -n $ignore_pattern && $name =~ $ignore_pattern ]] && continue

    # Never announce our own machinery, or it notifies about itself.
    [[ $name == omarchy-crash-* || $name == omarchy-agent-* ]] && continue

    # Muted at the end of a diagnosis, when the user was offered it and said
    # yes. A flag per program rather than one list, so omarchy-crash-mute can
    # lift one without reading, rewriting and re-parsing the rest.
    omarchy-toggle-enabled "crash-ignore/$name" && continue

    now=$EPOCHSECONDS
    (((now - ${last_notified[$name]:-0}) < dedupe_seconds)) && continue

    # Only a delivered toast starts the dedupe window. A failed send that
    # counted would suppress the rest of a crash loop for a minute, and
    # `journalctl -n 0` never replays what was missed.
    announce "$name" "$pid" "$exe" "$signal" && last_notified[$name]=$now
  done
