#!/usr/bin/env bash
# configbakrestore.sh
#
# Back up and restore settings/preferences for FLDIGI, FLRIG, FLMSG, FLAMP,
# JS8Call, JS8Spotter, and SitRepMapper, so they don't have to be
# reconfigured by hand on a new machine or after a fresh OS install.
#
# Usage:
#   ./configbakrestore.sh backup  [--only=NAME[,NAME...]] [--outdir=DIR]
#   ./configbakrestore.sh restore <path-to-zip> [--only=NAME[,NAME...]]
#   ./configbakrestore.sh -h | --help

set -u

SCRIPT_VERSION="1.2.1"
SCRIPT_DATE="2026-09-06"

ALL_PROGRAMS=(fldigi flrig flmsg flamp js8call js8spotter sitrepmapper)

usage() {
    cat <<EOF
configbakrestore.sh v${SCRIPT_VERSION} (${SCRIPT_DATE})

Back up and restore settings for FLDIGI, FLRIG, FLMSG, FLAMP, JS8Call,
JS8Spotter, and SitRepMapper.

Usage:
  $0 backup  [--only=NAME[,NAME...]] [--outdir=DIR]
  $0 restore <path-to-zip> [--only=NAME[,NAME...]]
  $0 -h | --help

NAME is one of: ${ALL_PROGRAMS[*]}

backup:
  Captures each program's settings files (not logbook/message data) into a
  single timestamped zip. Default output directory is the current
  directory; override with --outdir=DIR.

restore:
  Extracts settings files from a zip made by 'backup' back into place.
  Any existing file at the destination is moved aside first (suffixed
  .bak-<timestamp>) rather than being overwritten outright.

--only=NAME[,NAME...]
  Limit the operation to the named program(s) instead of all of:
  ${ALL_PROGRAMS[*]}

Notes:
  - Settings only: logbook/message data (FLDIGI's ADIF logbook, JS8Call's
    ALL.TXT/DIRECTED.TXT/inbox.db3) is never captured. 'backup' prints
    their locations so you can copy them by hand if you want them too.
  - JS8Call stores its rig serial port as a plain /dev/ttyUSBn, which can
    point at the wrong device on a different machine. 'restore' prints a
    reminder to re-check the CAT/PTT port in JS8Call afterward.
  - JS8Spotter's settings db is only found at its default
    install-sitrepnet-suite.sh location (~/src/fldigi-suite/js8spotter/).
    If it was installed with a custom --build-dir=, it will be skipped as
    "not found" -- back it up by hand instead.
  - SitRepMapper's config.json stores 'last_db_path', an absolute path to
    the JS8Spotter database it last opened. Like JS8Call's serial port,
    this is restored as-is and may point at the wrong path on a different
    machine or install location.
EOF
}

err() { echo "ERROR: $*" >&2; }
warn() { echo "WARNING: $*" >&2; }
info() { echo "$*"; }

require_cmd() {
    if ! command -v "$1" >/dev/null 2>&1; then
        err "'$1' is required but not installed. Install it with: sudo apt install $2"
        exit 1
    fi
}

# ---- per-program path specs -------------------------------------------
#
# Each program maps to a list of paths, relative to $HOME. A path ending
# in "/" is a directory captured/restored recursively; anything else is a
# single file.
#
# Two variants:
#   backup_paths()  -- used when building a backup. FLRIG globs $HOME's
#                      *.prefs so it picks up whichever rig-specific prefs
#                      file is active, without parsing flrig.prefs to find
#                      out. This must run against the SOURCE machine.
#   restore_paths() -- used when filtering what a zip is allowed to
#                      restore. Must NOT depend on the destination
#                      machine's current state (it usually won't have
#                      these files yet -- that's the point of restoring),
#                      so FLRIG is expressed as a directory prefix instead
#                      of a glob: whatever *.prefs file(s) backup_paths
#                      captured on the source machine are restored as-is.

backup_paths() {
    case "$1" in
        fldigi)
            echo ".fldigi/fldigi.prefs"
            echo ".fldigi/fldigi_def.xml"
            echo ".fldigi/fldigi_UI_colors.colors"
            echo ".fldigi/frequencies2.txt"
            echo ".fldigi/macros/"
            ;;
        flrig)
            local f
            for f in "$HOME"/.flrig/*.prefs; do
                [ -e "$f" ] || continue
                echo ".flrig/$(basename "$f")"
            done
            ;;
        flmsg)
            echo ".nbems/FLMSG.prefs"
            ;;
        flamp)
            echo ".nbems/FLAMP/FLAMP.prefs"
            ;;
        js8call)
            echo ".config/JS8Call.ini"
            ;;
        js8spotter)
            echo "src/fldigi-suite/js8spotter/js8spotter.db"
            ;;
        sitrepmapper)
            echo ".config/sitrepmapper/"
            ;;
        *)
            err "unknown program: $1"
            exit 1
            ;;
    esac
}

restore_paths() {
    case "$1" in
        fldigi)
            echo ".fldigi/fldigi.prefs"
            echo ".fldigi/fldigi_def.xml"
            echo ".fldigi/fldigi_UI_colors.colors"
            echo ".fldigi/frequencies2.txt"
            echo ".fldigi/macros/"
            ;;
        flrig)
            echo ".flrig/"
            ;;
        flmsg)
            echo ".nbems/FLMSG.prefs"
            ;;
        flamp)
            echo ".nbems/FLAMP/FLAMP.prefs"
            ;;
        js8call)
            echo ".config/JS8Call.ini"
            ;;
        js8spotter)
            echo "src/fldigi-suite/js8spotter/js8spotter.db"
            ;;
        sitrepmapper)
            echo ".config/sitrepmapper/"
            ;;
        *)
            err "unknown program: $1"
            exit 1
            ;;
    esac
}

# Process name(s) to check with pgrep for each program (best-effort).
program_procname() {
    case "$1" in
        fldigi) echo "fldigi" ;;
        flrig) echo "flrig" ;;
        flmsg) echo "flmsg" ;;
        flamp) echo "flamp" ;;
        js8call) echo "js8call" ;;
        # These two run as "python3 <script>", not their own binary name,
        # so they need a pgrep -f (full command line) substring match
        # instead of -x (exact process name) like the programs above.
        js8spotter) echo "js8spotter.py" ;;
        sitrepmapper) echo "sitrepmapper/main.py" ;;
    esac
}

warn_if_running() {
    local prog running=()
    for prog in "$@"; do
        local proc
        proc="$(program_procname "$prog")"
        case "$prog" in
            js8spotter|sitrepmapper)
                pgrep -f -i "$proc" >/dev/null 2>&1 && running+=("$prog")
                ;;
            *)
                pgrep -x -i "$proc" >/dev/null 2>&1 && running+=("$prog")
                ;;
        esac
    done
    if [ "${#running[@]}" -gt 0 ]; then
        warn "these programs appear to be running: ${running[*]} -- settings may not be fully written to disk (backup) or may get overwritten again on exit (restore). Consider closing them first."
    fi
}

# Extra context appended to a "skipped" line for programs where "not
# found" doesn't necessarily mean "not installed" (best-effort, empty for
# everything else).
skip_hint() {
    case "$1" in
        js8spotter)
            echo " -- if installed with a custom --build-dir=, back it up by hand instead"
            ;;
    esac
}

data_files_message() {
    cat <<EOF

Data/log files NOT included in this backup (copy by hand if you want them):
  FLDIGI logbook:     ~/.fldigi/logs/logbook.adif
  FLDIGI contest data: ~/.fldigi/data/
  JS8Call message log: ~/.local/share/JS8Call/ALL.TXT
  JS8Call directed log: ~/.local/share/JS8Call/DIRECTED.TXT
  JS8Call inbox:        ~/.local/share/JS8Call/inbox.db3
EOF
}

parse_only() {
    local csv="$1" name
    IFS=',' read -ra names <<<"$csv"
    for name in "${names[@]}"; do
        local found=0
        local p
        for p in "${ALL_PROGRAMS[@]}"; do
            [ "$p" = "$name" ] && found=1 && break
        done
        if [ "$found" -eq 0 ]; then
            err "unknown program in --only: $name (expected one of: ${ALL_PROGRAMS[*]})"
            exit 1
        fi
    done
    echo "${names[@]}"
}

# ---- backup -------------------------------------------------------------

cmd_backup() {
    local outdir="." only_csv=""
    for arg in "$@"; do
        case "$arg" in
            --only=*) only_csv="${arg#--only=}" ;;
            --outdir=*) outdir="${arg#--outdir=}" ;;
            *) err "unknown option: $arg"; exit 1 ;;
        esac
    done

    require_cmd zip zip

    local programs=() parsed_only
    if [ -n "$only_csv" ]; then
        parsed_only="$(parse_only "$only_csv")" || exit 1
        read -ra programs <<<"$parsed_only"
    else
        programs=("${ALL_PROGRAMS[@]}")
    fi

    warn_if_running "${programs[@]}"

    mkdir -p "$outdir"
    outdir="$(cd "$outdir" && pwd)"
    local archive="${outdir}/configbakrestore_$(hostname)_$(date +%Y%m%d_%H%M%S).zip"

    local added=() skipped=()
    local prog relpath

    cd "$HOME" || { err "cannot cd to \$HOME"; exit 1; }

    local to_zip=()
    for prog in "${programs[@]}"; do
        while IFS= read -r relpath; do
            [ -n "$relpath" ] || continue
            local check="${relpath%/}"
            if [ -e "$check" ]; then
                to_zip+=("$relpath")
                added+=("$prog: $relpath")
            else
                skipped+=("$prog: $relpath (not found)$(skip_hint "$prog")")
            fi
        done < <(backup_paths "$prog")
    done

    if [ "${#to_zip[@]}" -eq 0 ]; then
        err "nothing found to back up for: ${programs[*]}"
        exit 1
    fi

    zip -rq "$archive" "${to_zip[@]}"

    info "Backup written to: $archive"
    info ""
    info "Included:"
    printf '  %s\n' "${added[@]}"
    if [ "${#skipped[@]}" -gt 0 ]; then
        info ""
        info "Skipped (not found on this machine):"
        printf '  %s\n' "${skipped[@]}"
    fi
    data_files_message
}

# ---- restore --------------------------------------------------------------

cmd_restore() {
    local zip_path="" only_csv=""
    local positional=()
    for arg in "$@"; do
        case "$arg" in
            --only=*) only_csv="${arg#--only=}" ;;
            --*) err "unknown option: $arg"; exit 1 ;;
            *) positional+=("$arg") ;;
        esac
    done

    if [ "${#positional[@]}" -ne 1 ]; then
        err "restore requires exactly one argument: path to the backup zip"
        exit 1
    fi
    zip_path="${positional[0]}"

    if [ ! -f "$zip_path" ]; then
        err "no such file: $zip_path"
        exit 1
    fi
    zip_path="$(cd "$(dirname "$zip_path")" && pwd)/$(basename "$zip_path")"

    require_cmd unzip unzip

    local programs=() parsed_only
    if [ -n "$only_csv" ]; then
        parsed_only="$(parse_only "$only_csv")" || exit 1
        read -ra programs <<<"$parsed_only"
    else
        programs=("${ALL_PROGRAMS[@]}")
    fi

    warn_if_running "${programs[@]}"

    STAGING_DIR="$(mktemp -d)"
    trap 'rm -rf "$STAGING_DIR"' EXIT
    local staging="$STAGING_DIR"

    unzip -q "$zip_path" -d "$staging"

    local ts restored=() js8call_restored=0
    ts="$(date +%Y%m%d_%H%M%S)"

    local prog relpath
    for prog in "${programs[@]}"; do
        while IFS= read -r relpath; do
            [ -n "$relpath" ] || continue
            local is_dir=0
            local check="${relpath%/}"
            [[ "$relpath" == */ ]] && is_dir=1

            if [ "$is_dir" -eq 1 ]; then
                local srcdir="$staging/$check"
                [ -d "$srcdir" ] || continue
                local f
                while IFS= read -r -d '' f; do
                    local sub="${f#"$staging"/}"
                    restore_one_file "$staging/$sub" "$HOME/$sub" "$ts"
                    restored+=("$prog: $sub")
                done < <(find "$srcdir" -type f -print0)
            else
                local src="$staging/$relpath"
                [ -f "$src" ] || continue
                restore_one_file "$src" "$HOME/$relpath" "$ts"
                restored+=("$prog: $relpath")
                [ "$prog" = "js8call" ] && js8call_restored=1
            fi
        done < <(restore_paths "$prog")
    done

    if [ "${#restored[@]}" -eq 0 ]; then
        err "nothing in $zip_path matched the requested program(s): ${programs[*]}"
        exit 1
    fi

    info "Restored:"
    printf '  %s\n' "${restored[@]}"

    if [ "$js8call_restored" -eq 1 ]; then
        warn "JS8Call's CAT/PTT serial port was restored as-is (e.g. /dev/ttyUSB0) and may not match this machine. Open JS8Call and re-check Settings > Radio before using it."
    fi
}

restore_one_file() {
    local src="$1" dest="$2" ts="$3"
    if [ -e "$dest" ]; then
        cp -a "$dest" "${dest}.bak-${ts}"
    fi
    mkdir -p "$(dirname "$dest")"
    cp -a "$src" "$dest"
}

# ---- main -------------------------------------------------------------

main() {
    if [ "$#" -eq 0 ]; then
        usage
        exit 1
    fi

    case "$1" in
        -h|--help)
            usage
            exit 0
            ;;
        backup)
            shift
            cmd_backup "$@"
            ;;
        restore)
            shift
            cmd_restore "$@"
            ;;
        *)
            err "unknown command: $1"
            usage
            exit 1
            ;;
    esac
}

main "$@"
