#!/bin/bash
set -euo pipefail

# Puts a notification on the classroom screen, from anywhere.
#
#   classroom-notify "Title" "Body text"
#   classroom-notify --critical "Parent Call" "Room 3 needs a parent."
#   classroom-notify --urgency=low "DVD" "Disc ejected."
#   sudo classroom-notify --dry-run
#
# Used by classroom-notify-test to send a sample, and by classroom-mqtt
# to put messages from the church broker on the screen.
#
# The classroom desktop runs as the classroom user, and the notification
# service is part of that user's Plasma session. An administrator over SSH
# sits in a different session where nothing owns
# org.freedesktop.Notifications, so a plain notify-send there asks D-Bus to
# start a service that has no display to open, and times out with NoReply.
# Find the running desktop session and deliver into it instead.

CLASSROOM_USER=classroom

urgency=normal
dry_run=false

while [[ $# -gt 0 ]]; do
    case "$1" in
        --critical)
            urgency=critical
            shift
            ;;
        --low)
            urgency=low
            shift
            ;;
        --urgency=*)
            urgency="${1#--urgency=}"
            case "$urgency" in
                low|normal|critical) ;;
                *) urgency=normal ;;
            esac
            shift
            ;;
        --dry-run)
            dry_run=true
            shift
            ;;
        --)
            shift
            break
            ;;
        -*)
            echo "classroom-notify: unknown option $1" >&2
            echo "usage: classroom-notify-test [--low|--critical] [--dry-run] [title] [body]" >&2
            exit 2
            ;;
        *)
            break
            ;;
    esac
done

title="${1:-DVD Ready}"
body="${2:-A disc is ready to play.}"

fail() {
    echo "classroom-notify: $1" >&2
    exit 1
}

# Another process's environment, or nothing. Reading this needs ptrace
# access, which root has on the endpoint but not, for instance, inside a
# container with CAP_SYS_PTRACE dropped — so every caller has to cope
# with getting nothing back.
read_environ() {
    { tr '\0' '\n' < "/proc/$1/environ"; } 2>/dev/null || true
}

environ_value() {
    printf '%s\n' "$1" | sed -n "s/^$2=//p" | head -n 1
}

# The compositor holds its own wayland-N.lock open, so the display name can be read
# back off the process instead of being guessed at.
display_from_lock() {
    local link target

    for link in "/proc/$1/fd"/*; do
        target="$(readlink "$link" 2>/dev/null || true)"

        case "$target" in
            */wayland-*.lock)
                target="${target%.lock}"
                echo "${target##*/}"
                return 0
                ;;
        esac
    done

    return 0
}

classroom_uid="$(id -u "$CLASSROOM_USER" 2>/dev/null || true)"

[[ -n "$classroom_uid" ]] ||
    fail "there is no $CLASSROOM_USER user on this machine."

current_uid="$(id -u)"

runner=()
session="this session"
source_note="inherited from this session"

if [[ "$current_uid" == "$classroom_uid" &&
      -n "${WAYLAND_DISPLAY:-}" &&
      -n "${DBUS_SESSION_BUS_ADDRESS:-}" ]]; then
    # Already inside the classroom desktop session.
    session_pid="n/a"
    runtime_dir="${XDG_RUNTIME_DIR:-}"
    bus="$DBUS_SESSION_BUS_ADDRESS"
    wayland_display="$WAYLAND_DISPLAY"
else
    session="the $CLASSROOM_USER desktop session"

    # The compositor is the session: it owns the Wayland display and the
    # environment every other part of the session inherited.
    session_pid="$(pgrep -u "$CLASSROOM_USER" -x kwin_wayland | head -n 1 || true)"

    [[ -n "$session_pid" ]] ||
        fail "no classroom desktop session is running: there is no kwin_wayland process for user $CLASSROOM_USER."

    # Checked before anything that needs to look inside the session, so
    # an administrator gets told what to do rather than a permission
    # error from somewhere further down.
    if [[ "$current_uid" != "$classroom_uid" && "$current_uid" != "0" ]]; then
        fail "run this as root (sudo classroom-notify) or as the $CLASSROOM_USER user."
    fi

    environment_block="$(read_environ "$session_pid")"

    if [[ -n "$environment_block" ]]; then
        source_note="read from the compositor (pid $session_pid)"
    else
        source_note="the compositor's environment was unreadable; using session defaults"
    fi

    runtime_dir="$(environ_value "$environment_block" XDG_RUNTIME_DIR)"
    runtime_dir="${runtime_dir:-/run/user/$classroom_uid}"

    [[ -d "$runtime_dir" ]] ||
        fail "the classroom session runtime directory $runtime_dir does not exist."

    bus="$(environ_value "$environment_block" DBUS_SESSION_BUS_ADDRESS)"
    bus="${bus:-unix:path=$runtime_dir/bus}"

    case "$bus" in
        unix:path=*)
            bus_socket="${bus#unix:path=}"
            bus_socket="${bus_socket%%,*}"

            [[ -S "$bus_socket" ]] ||
                fail "the classroom session bus socket $bus_socket does not exist. Is the desktop session fully up?"
            ;;
    esac

    wayland_display="$(environ_value "$environment_block" WAYLAND_DISPLAY)"

    if [[ -z "$wayland_display" ]]; then
        wayland_display="$(display_from_lock "$session_pid")"
    fi

    environment=(
        "XDG_RUNTIME_DIR=$runtime_dir"
        "DBUS_SESSION_BUS_ADDRESS=$bus"
    )

    # notify-send only needs the bus. The display is passed through for
    # the sake of anything else in the session, and its absence is not
    # worth refusing to send over.
    if [[ -n "$wayland_display" ]]; then
        environment+=("WAYLAND_DISPLAY=$wayland_display")
    fi

    if [[ "$current_uid" == "$classroom_uid" ]]; then
        runner=(env "${environment[@]}")
    else
        runner=(runuser -u "$CLASSROOM_USER" -- env "${environment[@]}")
    fi
fi

# Deliver to the notification service that is already running. Sending into
# a session without one would have D-Bus start something with no display to
# draw on, which is exactly the failure this script exists to avoid.
pgrep -u "$CLASSROOM_USER" -x plasmashell >/dev/null 2>&1 ||
    fail "the classroom shell is not running, and this will not start it. Check with: pgrep -u $CLASSROOM_USER -a plasmashell"

if [[ "$dry_run" == true ]]; then
    echo "Would notify $session:"
    echo "  compositor pid:           $session_pid"
    echo "  session environment:      $source_note"
    echo "  XDG_RUNTIME_DIR:          $runtime_dir"
    echo "  DBUS_SESSION_BUS_ADDRESS: $bus"
    echo "  WAYLAND_DISPLAY:          ${wayland_display:-(not resolved)}"
    echo "  urgency:                  $urgency"
    echo "  title:                    $title"
    echo "  body:                     $body"
    exit 0
fi

"${runner[@]}" notify-send \
    --app-name="Classroom" \
    --urgency="$urgency" \
    -- \
    "$title" "$body"

echo "Sent $urgency notification to $session: $title"
