#!/bin/sh
#
# reprise - Diagnose and repair a broken SliTaz system.
#
# Copyright (C) 2026 SliTaz GNU/Linux - BSD License
#
# Runs on the machine, offline first. Checks find what is broken,
# snapshots of the last known good state let it repair without knowing
# the bug, the fixes database published by the SliTaz developers on
# filature.slitaz.org adds what was learned from other machines. Every
# change is explained, asked [y/N], backed up and can be undone.
# Reports go through filature-report, the only SliTaz report sender.
#

VERSION="0.4.1"

# Share directory holding lib.sh, checks and repairs. Overridable so the
# tool runs straight from a source checkout: REPRISE_SHARE=. ./reprise
REPRISE_SHARE="${REPRISE_SHARE:-/usr/share/reprise}"

# Root of the system to inspect: "/" or a broken system mounted from a
# live CD (--rescue=DIR).
REPRISE_ROOT="/"

# Mode: diagnose (default), check (machine output), fix, report.
REPRISE_MODE="diagnose"
REPRISE_REPORT=""
REPRISE_QUICK=""
ACTION=""
UNDO_STAMP=""

# Checks run first, in this order: the network first (fixes may need to
# download), the arch next (package management depends on it), then
# packages and what changed since the last good state. Unlisted checks
# follow in alphabetical order.
CHECK_ORDER="network arch packages changes libs desktop xorg kernel"

# usage - print command line help.
usage() {
    cat << EOT
Usage: reprise [option]

Diagnose and repair a broken SliTaz system.

Options:
  --fix           Propose repairs for the problems found ([y/N] each)
  --check         Machine output (key=value), read-only, quick
  --full          With --check: full package audit (slower)
  --report        Plain-text report of the checks, safe to pipe
  --send          Send a bug report to the SliTaz developers
  --undo[=STAMP]  Undo the last repairs (or those of backup STAMP)
  --update-db     Fetch the latest fixes database from filature
  --prepare       Before an upgrade: fetch the database, take a snapshot
  --snapshot      Take a snapshot of the current state
  --mark-good     Mark the current state as known good
  --boot-entry    Add a "SliTaz rescue (reprise)" boot menu entry
  --rescue=DIR    Inspect and repair a SliTaz system mounted on DIR
  --version       Show version and exit
  --help          Show this help and exit
EOT
}

# Parse the command line.
for arg in "$@"; do
    case "$arg" in
        --fix) REPRISE_MODE="fix" ;;
        --check) REPRISE_MODE="check"; REPRISE_QUICK="yes" ;;
        --full) FULL="yes" ;;
        --report) REPRISE_MODE="report"; REPRISE_REPORT="yes" ;;
        --send) ACTION="send" ;;
        --undo) ACTION="undo" ;;
        --undo=*) ACTION="undo"; UNDO_STAMP="${arg#--undo=}" ;;
        --update-db) ACTION="update-db" ;;
        --prepare) ACTION="prepare" ;;
        --snapshot) ACTION="snapshot" ;;
        --mark-good) ACTION="mark-good" ;;
        --boot-begin) ACTION="boot-begin" ;;
        --boot) ACTION="boot" ;;
        --boot-entry) ACTION="boot-entry" ;;
        --rescue=*) REPRISE_ROOT="${arg#--rescue=}" ;;
        --version) echo "reprise $VERSION"; exit 0 ;;
        --help|-h) usage; exit 0 ;;
        *)
            echo "reprise: unknown option: $arg" >&2
            usage >&2
            exit 2 ;;
    esac
done
[ -n "$FULL" ] && REPRISE_QUICK=""

export REPRISE_ROOT REPRISE_MODE REPRISE_REPORT REPRISE_QUICK

# Keep the user's stdin on fd 3: ask reads its answers there only.
exec 3<&0

# Load the shared code. Exit 2: reprise itself is broken.
for lib in lib.sh fixes.sh repair.sh snapshot.sh; do
    if [ ! -f "$REPRISE_SHARE/$lib" ]; then
        echo "reprise: cannot find $REPRISE_SHARE/$lib" >&2
        exit 2
    fi
    . "$REPRISE_SHARE/$lib"
done

# Preflight: a rescue root must at least look like a SliTaz system.
if [ "$REPRISE_ROOT" != "/" ]; then
    if [ ! -d "$REPRISE_ROOT" ]; then
        log_err "$REPRISE_ROOT is not a directory"
        exit 2
    fi
    if [ ! -f "$(reprise_path /etc/slitaz-release)" ]; then
        log_err "$REPRISE_ROOT does not look like a SliTaz root"
        log_err "(no etc/slitaz-release found, is the partition mounted?)"
        exit 2
    fi
fi

# need_root ACTION - repairs and state changes need root.
need_root() {
    if [ "$(id -u)" != "0" ]; then
        log_err "$1 needs root: su -c 'reprise $*'"
        exit 1
    fi
}

# Machine records of this run.
REPRISE_OUT="${TMPDIR:-/tmp}/reprise-out.$$"
: > "$REPRISE_OUT" || exit 2
trap 'rm -f "$REPRISE_OUT" "$REPRISE_OUT".*' EXIT

# check_list - check names, CHECK_ORDER first.
check_list() {
    local name module
    for name in $CHECK_ORDER; do
        [ -f "$REPRISE_SHARE/checks/$name.sh" ] && echo "$name"
    done
    for module in "$REPRISE_SHARE"/checks/*.sh; do
        [ -f "$module" ] || continue
        name=$(basename "$module" .sh)
        case " $CHECK_ORDER " in
            *" $name "*) ;;
            *) echo "$name" ;;
        esac
    done
}

# run_check NAME - run one check, record ok|warn|fail with the first
# problem and its signature. Returns the check status.
run_check() {
    local name status sig text
    name="$1"
    . "$REPRISE_SHARE/checks/$name.sh"
    REPRISE_CHECK="$name"
    CHECK_INFO=""
    rm -f "$REPRISE_OUT.$name"
    log_info "Checking: $name"
    "check_$name"
    status=$?
    if [ "$status" -ne 0 ]; then
        rec "check.$name" fail
        log_err "$name: problems found"
    elif [ -n "$CHECK_INFO" ]; then
        rec "check.$name" warn
    else
        rec "check.$name" ok
        log_ok "$name: OK"
    fi
    if [ -n "$CHECK_INFO" ]; then
        rec "check.$name.info" "$CHECK_INFO"
        text=$(signature "$CHECK_INFO")
        sig=$(signature_hash "$text")
        rec "sig.$name" "$sig"
        rec "sig.$name.text" "$text"
    fi
    REPRISE_CHECK=""
    return $status
}

# run_checks - every check, returns the number of failed checks.
run_checks() {
    local name broken total
    broken=0
    total=0
    for name in $(check_list); do
        total=$((total + 1))
        run_check "$name" || broken=$((broken + 1))
    done
    log_info "Checks run: $total - problems: $broken"
    return $broken
}

# check_problems NAME - normalized problems of check NAME in this run,
# one per line, "-" for a failed check that noted none.
check_problems() {
    local line
    if [ -s "$REPRISE_OUT.$1" ]; then
        while IFS= read -r line; do
            signature "$line"
        done < "$REPRISE_OUT.$1"
    else
        case "$(rec_get "check.$1")" in
            fail|warn) echo "-" ;;
        esac
    fi
}

# checks_save DIR - run the quick checks, save their problems in
# DIR/checks.tsv: what was already broken before an upgrade.
checks_save() {
    local name sig
    REPRISE_MODE="check"
    REPRISE_QUICK="yes"
    run_checks > /dev/null 2>&1
    for name in $(check_list); do
        check_problems "$name" | while IFS= read -r sig; do
            printf '%s\t%s\n' "$name" "$sig"
        done
    done > "$1/checks.tsv"
}

# checks_since - compare the problems of this run with the newest
# --prepare snapshot: check.<name>.new counts the problems it did not
# have, check.<name>.new.info shows the first one.
checks_since() {
    local stamp dir name sig new first
    for stamp in $(snapshot_list | sort -r); do
        dir="$REPRISE_VAR/snapshots/$stamp"
        grep -qx 'reason=prepare' "$dir/INFO" 2>/dev/null &&
            [ -f "$dir/checks.tsv" ] && break
        dir=""
    done
    [ -n "$dir" ] || return 0
    rec prepare.since "$stamp"
    for name in $(check_list); do
        [ -n "$(rec_get "check.$name.info")" ] ||
            [ "$(rec_get "check.$name")" = "fail" ] || continue
        new=0
        first=""
        while IFS= read -r sig; do
            [ -n "$sig" ] || continue
            grep -qxF "$name	$sig" "$dir/checks.tsv" && continue
            new=$((new + 1))
            [ -n "$first" ] || first="$sig"
        done << EOT
$(check_problems "$name")
EOT
        rec "check.$name.new" "$new"
        [ -n "$first" ] && rec "check.$name.new.info" "$first"
    done
}

# match_fixes - match the fixes database, record and show the matches.
# Prints nothing, fills MATCHED (\037 separated lines).
MATCHED=""
match_fixes() {
    local id bug rep hint
    MATCHED=$(fixes_match)
    [ -n "$MATCHED" ] || return 0
    while IFS="$FS_US" read -r id bug rep hint; do
        [ -n "$id" ] || continue
        rec "issue.$id" matched
        [ -n "$hint" ] && rec "issue.$id.hint" "$hint"
        [ -n "$bug" ] && rec "issue.$id.bug" "$bug"
        [ -n "$rep" ] && rec "issue.$id.repair" "$rep"
        log_warn "Known problem: $id"
        [ -n "$hint" ] && say "    $hint"
        [ -n "$bug" ] && say "    more: https://filature.slitaz.org/bug/$bug"
    done << EOT
$MATCHED
EOT
}

# machine_output - print the records of this run in the order agreed
# with filature-report: what is wrong first, then what is fine, the
# verbose details last (a 16 KiB report keeps the important lines).
machine_output() {
    awk '
        { i = index($0, "="); k = substr($0, 1, i - 1); v = substr($0, i + 1) }
        k ~ /^check\.[^.]+$/ && (v == "fail" || v == "warn") { a[++na] = $0; next }
        k ~ /^issue\./ { b[++nb] = $0; next }
        k ~ /^verify\./ || k ~ /^repair\.[^.]+\.[0-9]+$/ || k ~ /^repair\.[^.]+$/ { r[++nr] = $0; next }
        k ~ /^sig\./ { c[++nc] = $0; next }
        k ~ /^upgraded\./ { d[++nd] = $0; next }
        k ~ /^check\.[^.]+$/ { e[++ne] = $0; next }
        { f[++nf] = $0 }
        END {
            for (i = 1; i <= na; i++) print a[i]
            for (i = 1; i <= nb; i++) print b[i]
            for (i = 1; i <= nr; i++) print r[i]
            for (i = 1; i <= nc; i++) print c[i]
            for (i = 1; i <= nd; i++) print d[i]
            for (i = 1; i <= ne; i++) print e[i]
            for (i = 1; i <= nf; i++) print f[i]
        }
    ' "$REPRISE_OUT"
}

# failed_checks - names of the checks that failed in this run.
failed_checks() {
    sed -n 's/^check\.\([^.=]*\)=fail$/\1/p' "$REPRISE_OUT"
}

# repair_all - database entries first (specific, learned), then the
# generic repairs of each failed check, then verify: run the failed
# checks again and record whether each repair really fixed something.
repair_all() {
    local id bug rep hint name before entry
    : > "$REPRISE_OUT.map"
    before=$(failed_checks)
    while IFS="$FS_US" read -r id bug rep hint; do
        [ -n "$id" ] && [ -n "$rep" ] || continue
        set -f
        set -- $rep
        set +f
        run_repair "$id" "$@"
    done << EOT
$MATCHED
EOT
    for name in $before; do
        type "fix_$name" >/dev/null 2>&1 || continue
        CURRENT_FIX="$name"
        "fix_$name"
        CURRENT_FIX=""
    done
    grep -q '=applied$' "$REPRISE_OUT" || return 0
    log_info "Checking again after the repairs..."
    for name in $before; do
        sort "$REPRISE_OUT.$name" > "$REPRISE_OUT.$name.before" 2>/dev/null
        ( REPRISE_MODE=check; REPRISE_OUT="$REPRISE_OUT.verify"; run_check "$name" )
    done
    # A database entry still matching after its repair did not fix it.
    entry=$(REPRISE_OUT="$REPRISE_OUT.verify" fixes_match | cut -d"$FS_US" -f1)
    sed -n 's/^repair\.\([^=]*\)=applied$/\1/p' "$REPRISE_OUT" | while read -r id; do
        # A check repair is verified by the check that proposed it: ok
        # when some of its problems are gone and none appeared.
        name=$(awk -v i="$id" '$1 == i {print $2; exit}' "$REPRISE_OUT.map" 2>/dev/null)
        if [ -n "$name" ]; then
            if verify_check "$name"; then
                feedback "verify.$id" ok
            else
                feedback "verify.$id" fail
            fi
        else
            case " $(echo $entry) " in
                *" $id "*) feedback "verify.$id" fail ;;
                *) feedback "verify.$id" ok ;;
            esac
        fi
    done
    if grep -q '=fail$' "$REPRISE_OUT.verify" 2>/dev/null; then
        log_warn "Some problems remain: $(sed -n 's/^check\.\([^.=]*\)=fail$/\1/p' "$REPRISE_OUT.verify" | tr '\n' ' ')"
    else
        log_ok "All repaired checks pass now"
    fi
    rm -f "$REPRISE_OUT.verify" "$REPRISE_OUT.verify".* "$REPRISE_OUT.map"
}

# verify_check NAME - after repairs: return 0 when the check passes or
# lost problems without gaining any.
verify_check() {
    local before after
    case "$(sed -n "s/^check\.$1=//p" "$REPRISE_OUT.verify")" in
        ok|warn) return 0 ;;
    esac
    before="$REPRISE_OUT.$1.before"
    after="$REPRISE_OUT.verify.$1"
    [ -f "$before" ] && [ -f "$after" ] || return 1
    # Nothing new, and fewer lines than before.
    [ -z "$(sort "$after" | comm -13 "$before" - 2>/dev/null)" ] || return 1
    [ "$(wc -l < "$after")" -lt "$(wc -l < "$before")" ]
}

# summary_line - one line: N checks, M problems: names.
summary_line() {
    local total fails
    total=$(grep -c '^check\.[^.=]*=' "$REPRISE_OUT")
    fails=$(failed_checks | tr '\n' ' ')
    if [ -n "$fails" ]; then
        echo "$total checks, $(failed_checks | wc -l) problems: ${fails% }"
    else
        echo "$total checks, no problem"
    fi
}

#
# Reports, through filature-report
#

# report_supports OPTION - return 0 when filature-report knows OPTION
# (-x, -k): older versions take an unknown option for the title.
report_supports() {
    filature-report help 2>/dev/null | grep -q -- " $1 "
}

# report_lines FILE - machine lines of this run for a report.
report_lines() {
    machine_output > "$1"
}

# send_bug - send a bug report: the user describes the problem, the
# checks and known problems are joined as extra lines.
send_bug() {
    local tmp out title rel arch
    if ! have_cmd filature-report; then
        log_err "filature-report is not installed: tazpkg get-install filature-report"
        log_info "Or paste the output of 'reprise --report' on https://filature.slitaz.org/report"
        return 1
    fi
    run_checks
    match_fixes
    tmp="${TMPDIR:-/tmp}/reprise-extra.$$"
    out="${TMPDIR:-/tmp}/reprise-report.$$"
    report_lines "$tmp"
    rel=$(system_release)
    arch=$(system_arch)
    title="reprise: $rel $arch, $(summary_line)"
    [ "$REPRISE_ROOT" != "/" ] && export FILATURE_REPORT_ROOT="$REPRISE_ROOT"
    echo ""
    echo "Describe the problem in a few lines (what you did, what broke),"
    echo "end with an empty line:"
    desc=""
    while IFS= read -r line && [ -n "$line" ]; do
        desc="$desc$line
"
    done
    [ -n "$desc" ] || desc="$(summary_line)"
    if report_supports -x; then
        printf '%s' "$desc" | filature-report make -t -x "$tmp" "$title" > "$out" || {
            rm -f "$tmp" "$out"; return 1; }
    else
        # Older filature-report: append our lines, the server validates
        # every line anyway.
        printf '%s' "$desc" | filature-report make -t "$title" > "$out" || {
            rm -f "$tmp" "$out"; return 1; }
        cat "$tmp" >> "$out"
    fi
    rm -f "$tmp"
    echo ""
    cat "$out"
    echo ""
    if ask "Send this report to the SliTaz developers?"; then
        if filature-report send "$out"; then
            rm -f "$out"
        else
            log_err "Not accepted, report kept in $out"
            log_info "Send it later with: filature-report send $out"
        fi
    else
        log_info "Not sent, report kept in $out"
    fi
}

# send_feedback - offer to send the results of the repairs: this is how
# the fixes database learns what works.
send_feedback() {
    local tmp out
    [ -s "$REPRISE_VAR/feedback" ] || return 0
    have_cmd filature-report && report_supports -k || return 0
    tmp="${TMPDIR:-/tmp}/reprise-feedback.$$"
    out="${TMPDIR:-/tmp}/reprise-feedback-report.$$"
    # Results first (verify, repair), then this run's diagnosis.
    { grep -E '^(verify|repair)\.' "$REPRISE_VAR/feedback"
      machine_output | grep -vE '^(verify|repair)\.'; } > "$tmp"
    [ "$REPRISE_ROOT" != "/" ] && export FILATURE_REPORT_ROOT="$REPRISE_ROOT"
    if ! filature-report make -k reprise -x "$tmp" < /dev/null > "$out"; then
        rm -f "$tmp" "$out"
        return 0
    fi
    rm -f "$tmp"
    echo ""
    cat "$out"
    echo ""
    if ask "Send these results anonymously to help repair other machines?"; then
        # Cleared only on "OK <id>": refused results are offered again
        # after the next --fix.
        if filature-report send "$out"; then
            : > "$REPRISE_VAR/feedback"
        else
            log_info "Not accepted, the results are kept for next time"
        fi
    fi
    rm -f "$out"
}

# report - plain-text report of the checks, no color, no prompt.
report() {
    local status
    echo "=== Reprise $VERSION report - $(date '+%Y-%m-%d %H:%M') ==="
    echo ""
    echo "SliTaz release : $(system_release)"
    echo "Arch           : $(system_arch)"
    [ "$REPRISE_ROOT" != "/" ] && echo "Rescue root    : $REPRISE_ROOT"
    echo ""
    run_checks
    status=$?
    match_fixes
    echo ""
    echo "=== $(summary_line) ==="
    return $status
}

#
# Undo
#

# undo - restore the files and packages saved by a reprise run.
undo() {
    local dir kind id path ver file list
    need_root --undo
    if [ -n "$UNDO_STAMP" ]; then
        dir="$REPRISE_VAR/backups/$UNDO_STAMP"
    else
        dir=$(ls -d "$REPRISE_VAR"/backups/*/ 2>/dev/null | tail -1)
        dir="${dir%/}"
    fi
    if [ ! -s "$dir/MANIFEST" ]; then
        log_err "No reprise backup to undo${UNDO_STAMP:+ named $UNDO_STAMP}"
        log_info "Backups: $(ls "$REPRISE_VAR/backups" 2>/dev/null | tr '\n' ' ')"
        return 1
    fi
    list=$(awk '
        $1 == "file" { print "restore " $3 }
        $1 == "absent" { print "remove  " $3 }
        $1 == "pkg" && $4 == "none" { print "remove package " $3 }
        $1 == "pkg" && $4 != "none" { print "reinstall " $3 " " $4 }
    ' "$dir/MANIFEST")
    explain "Undo the repairs of $(basename "$dir")" "$list" nobackup
    ask "Undo these changes?" || return 1
    while read -r kind id path ver file; do
        case "$kind" in
            file)
                cp -a "$dir/files$path" "$(reprise_path "$path")" ;;
            absent)
                rm -rf "$(reprise_path "$path")" ;;
            pkg)
                if [ "$ver" = "none" ]; then
                    pkg_remove "$path"
                elif [ -n "$file" ] && [ -f "$file" ]; then
                    pkg_add_file "$file"
                else
                    pkg_get "$path"
                fi ;;
        esac
    done < "$dir/MANIFEST"
    for id in $(awk '{print $2}' "$dir/MANIFEST" | sort -u); do
        case "$id" in
            users) ;;
            *) feedback "repair.$id" undone ;;
        esac
    done
    journal "undo $(basename "$dir")"
    mv -f "$dir/MANIFEST" "$dir/MANIFEST.undone"
    log_ok "Changes of $(basename "$dir") undone"
}

#
# Boot integration
#

# boot_entry - add the "SliTaz rescue (reprise)" entry next to the main
# one: same kernel and arguments, plus "reprise".
boot_entry() {
    local cfg menu entry
    need_root --boot-entry
    cfg=$(reprise_path /boot/grub/grub.cfg)
    menu=$(reprise_path /boot/grub/menu.lst)
    if [ -f "$cfg" ]; then
        grep -q 'SliTaz rescue (reprise)' "$cfg" && { log_ok "Entry already present"; return 0; }
        entry=$(awk '/^[[:space:]]*linux[[:space:]]/ && !done {
            sub(/^[[:space:]]*/, ""); print "\t" $0 " reprise"; done = 1 }' "$cfg")
        [ -n "$entry" ] || { log_err "No linux line in grub.cfg"; return 1; }
        explain "No rescue entry in the boot menu" \
"append to /boot/grub/grub.cfg:
menuentry \"SliTaz rescue (reprise)\" {
$entry
}"
        ask "Add the rescue entry?" || return 1
        backup_file boot-entry /boot/grub/grub.cfg
        printf '\nmenuentry "SliTaz rescue (reprise)" {\n%s\n}\n' "$entry" >> "$cfg"
    elif [ -f "$menu" ]; then
        grep -q 'SliTaz rescue (reprise)' "$menu" && { log_ok "Entry already present"; return 0; }
        entry=$(awk '/^[[:space:]]*kernel[[:space:]]/ && !done {
            print $0 " reprise"; done = 1 }' "$menu")
        [ -n "$entry" ] || { log_err "No kernel line in menu.lst"; return 1; }
        explain "No rescue entry in the boot menu" \
"append to /boot/grub/menu.lst:
title SliTaz rescue (reprise)
$entry"
        ask "Add the rescue entry?" || return 1
        backup_file boot-entry /boot/grub/menu.lst
        printf '\ntitle SliTaz rescue (reprise)\n%s\n' "$entry" >> "$menu"
    else
        log_err "No grub configuration in /boot/grub"
        log_info "EFI install: mount the EFI partition on /boot/efi and edit its grub.cfg,"
        log_info "or add \"reprise\" to the linux line with the e key in the GRUB menu."
        return 1
    fi
    journal "applied boot-entry"
    log_ok "Rescue entry added to the boot menu"
}

# boot_rescue - console session started by rcS when the kernel command
# line has "reprise": diagnose, repair, reboot.
# offer_update_db - with the network up, offer to fetch the newest fixes
# database first: machines that never run spk-up would otherwise keep
# the copy of their package. Never without a yes (offline first).
offer_update_db() {
    [ "$REPRISE_ROOT" = "/" ] || return 0
    ip route 2>/dev/null | grep -q '^default' || return 0
    log_info "Fixes database: version $(fixes_version "$(fixes_file)")"
    ask "Fetch the newest fixes database from filature.slitaz.org?" || return 0
    update_db
}

boot_rescue() {
    REPRISE_MODE="fix"
    echo ""
    echo "${C_BOLD}SliTaz rescue - reprise $VERSION${C_OFF}"
    echo "The graphical session is not started. Problems found will be"
    echo "explained one by one, nothing changes without your yes."
    echo ""
    good=$(snapshot_good)
    [ -n "$good" ] && echo "Last known good state: $good"
    # The boot does not wait for DHCP: give it a few seconds, then get
    # the newest fixes database while the network is there.
    n=0
    while [ "$n" -lt 15 ] && ! ip route 2>/dev/null | grep -q '^default'; do
        [ "$n" -eq 0 ] && echo "Waiting for the network (15 s at most)..."
        sleep 1
        n=$((n + 1))
    done
    ip route 2>/dev/null | grep -q '^default' && update_db
    run_checks
    match_fixes
    repair_all
    # Repairs were offered: the next boot gets its chance before the
    # boot prompt comes back.
    echo 0 > "$REPRISE_VAR/boot-pending"
    send_feedback
    echo ""
    if ask "Reboot now?"; then
        reboot
    fi
    echo "Boot continues without the graphical session. Type 'reboot' when done."
}

#
# Main
#

case "$ACTION" in
    update-db)
        need_root --update-db
        update_db
        exit $? ;;
    prepare)
        # Called by spk-up before an upgrade: never blocks it.
        [ "$(id -u)" = "0" ] || exit 0
        update_db > /dev/null 2>&1
        stamp=$(snapshot_take prepare) &&
            { snapshot_prune; checks_save "$REPRISE_VAR/snapshots/$stamp"; }
        exit 0 ;;
    snapshot)
        need_root --snapshot
        stamp=$(snapshot_take manual) && log_ok "Snapshot $stamp taken"
        exit $? ;;
    mark-good)
        [ "$(id -u)" = "0" ] || exit 0
        mark_good
        exit 0 ;;
    boot-begin)
        boot_begin
        exit 0 ;;
    boot)
        need_root --boot
        boot_rescue
        exit 0 ;;
    boot-entry)
        boot_entry
        exit $? ;;
    undo)
        undo
        exit $? ;;
    send)
        send_bug
        exit $? ;;
esac

case "$REPRISE_MODE" in
    check)
        # Some checks print detail lines: stdout is for records only.
        run_checks > /dev/null 2>&1
        match_fixes > /dev/null 2>&1
        checks_since
        machine_output
        failed_checks | grep -q . && exit 1
        grep -q '^issue\.' "$REPRISE_OUT" && exit 1
        exit 0 ;;
    report)
        report
        exit $? ;;
    fix)
        need_root --fix
        offer_update_db
        run_checks
        match_fixes
        repair_all
        send_feedback
        failed_checks | grep -q . && exit 1
        exit 0 ;;
    *)
        run_checks
        status=$?
        match_fixes
        echo ""
        log_info "$(summary_line)"
        if [ "$status" -ne 0 ] || [ -n "$MATCHED" ]; then
            log_info "To repair: su -c 'reprise --fix'   To report: reprise --send"
            exit 1
        fi
        exit 0 ;;
esac
