#!/usr/bin/env bash

: <<'END'
dhupdate
DigiHub script updater - syncs installed scripts and dh.lib against
the PlotLost DigiHub repository

Version 0.1

Steve de Bode - W0FFS - July 2026

Input:  -y, --yes     assume yes for the update confirmation prompt
        -c, --check   check only; do not prompt or apply changes
        -h, --help    show this help text
Output: interactive by default

Exit codes:
 0 = up to date, applied successfully, help shown, or update declined
 1 = a prerequisite failed (git, network, manifest, or clone)
 2 = an unknown command line option was supplied
 3 = --check found changes available (not applied)
END

set -euo pipefail

UI_LIB="/usr/local/lib/digihub/dh.lib"
[ -r "$UI_LIB" ] && source "$UI_LIB" || {
  printf "\nERROR: Missing UI library: %s\n\n" "$UI_LIB" >&2
  exit 1
}

REPO_URL="https://www.plotlost.com/static/downloads/DigiHubHam.git"
MANIFEST="/usr/local/bin/.digihub.manifest"
LIBDIR="/usr/local/lib/digihub"
UpdateFlagFile="${HOME:-}/.digihub-update-available"
DhInfoPath="${HOME:-}/.dhinfo"
DhInfoBackup="${HOME:-}/.dhinfo.last"
DhInfoMissingFlag="${HOME:-}/.digihub-dhinfo-missing"

# Every long-running DigiHub daemon, keyed by its wrapper script's
# basename under /usr/local/bin. Used to restart whichever of these are
# currently active whenever their script (or dh.lib/dhweb, which they
# all draw from) changes -- oneshot units (dhhamdbupdate, dhupdatecheck)
# are deliberately excluded, systemctl is-active on those is meaningless
# between runs.
declare -A SERVICE_FOR_SCRIPT=(
  [dhgpsmonitor]=dhgpsmonitor.service
  [dhweb]=dhweb.service
  [dhrigctld]=dhrigctld.service
  [dhpatd]=dhpat.service
  [dhardopd]=dhardop.service
  [dhwebchatd]=dhwebchat.service
  [dhnoded]=dhnode.service
  [dhconsoled]=dhconsole.service
  [dhdirewolf]=dhdirewolf.service
  [dhwsjtxd]=dhwsjtx.service
  [dhjs8calld]=dhjs8call.service
  [dhqsstvd]=dhqsstv.service
)

ASSUME_YES=0
CHECK_ONLY=0
CloneDir=""
NewManifest=""
OldManifestFiltered=""

usage() {
  sed -n '/^: <<'\''END'\''/,/^END$/p' "$0" | sed '1d;$d'
}

WriteUpdateFlag() {
  [[ -n "$UpdateFlagFile" ]] || return 0
  {
    printf 'DigiHub update available (%d added, %d updated, %d removed)\n' \
      "${#Added[@]}" "${#Updated[@]}" "${#Removed[@]}"
    printf 'Checked: %s\n' "$(date '+%F %T')"
  } > "$UpdateFlagFile" 2>/dev/null || true
}

ClearUpdateFlag() {
  [[ -n "$UpdateFlagFile" ]] && rm -f -- "$UpdateFlagFile" 2>/dev/null || true
}

AddServiceToRestart() {
  local svc="$1" s
  for s in ${ServicesToRestart[@]+"${ServicesToRestart[@]}"}; do
    [[ "$s" == "$svc" ]] && return 0
  done
  ServicesToRestart+=("$svc")
}

# dh.lib changing affects every daemon that sources it, so restart all
# of them; a dhweb/* file affects only dhweb itself; anything else maps
# 1:1 through SERVICE_FOR_SCRIPT if it's a known daemon wrapper.
MarkServiceForDest() {
  local dest="$1" script svc
  case "$dest" in
    "$LIBDIR/dh.lib")
      for svc in "${SERVICE_FOR_SCRIPT[@]}"; do AddServiceToRestart "$svc"; done
      ;;
    "$LIBDIR"/dhweb/*)
      AddServiceToRestart "dhweb.service"
      ;;
    /usr/local/bin/*)
      script="$(basename "$dest")"
      [[ -n "${SERVICE_FOR_SCRIPT[$script]:-}" ]] && AddServiceToRestart "${SERVICE_FOR_SCRIPT[$script]}"
      ;;
  esac
}

# If .dhinfo is missing, restore it from .dhinfo.last (kept up to date by
# install.sh and dhedit whenever they save .dhinfo). If no backup exists
# either, leave a login reminder since there is nothing to recover from.
CheckDhInfo() {
  [[ -n "$DhInfoPath" ]] || return 0

  if [[ -f "$DhInfoPath" ]]; then
    [[ -n "$DhInfoMissingFlag" ]] && rm -f -- "$DhInfoMissingFlag" 2>/dev/null || true
    return 0
  fi

  section "DigiHub Configuration"

  if [[ -f "$DhInfoBackup" ]] && cp -f -- "$DhInfoBackup" "$DhInfoPath" 2>/dev/null; then
    kv "Notice" ".dhinfo was missing; restored from .dhinfo.last"
    [[ -n "$DhInfoMissingFlag" ]] && rm -f -- "$DhInfoMissingFlag" 2>/dev/null || true
  else
    err_kv "Warning" ".dhinfo is missing and no backup is available"
    err_kv "Action"  "Run install.sh or dhedit to recreate it"
    if [[ -n "$DhInfoMissingFlag" ]]; then
      {
        printf 'DigiHub configuration missing: .dhinfo not found.\n'
        printf 'Run install.sh or dhedit to recreate it.\n'
      } > "$DhInfoMissingFlag" 2>/dev/null || true
    fi
  fi

  printf "\n"
}

run_sudo() {
  if [[ "${EUID}" -eq 0 ]]; then
    "$@"
  else
    sudo "$@"
  fi
}

cleanup() {
  [[ -n "$CloneDir" ]] && rm -rf -- "$CloneDir" >/dev/null 2>&1
  [[ -n "$NewManifest" ]] && rm -f -- "$NewManifest" >/dev/null 2>&1
  [[ -n "$OldManifestFiltered" ]] && rm -f -- "$OldManifestFiltered" >/dev/null 2>&1
  return 0
}
trap cleanup EXIT

while (( $# > 0 )); do
  case "$1" in
    -y|--yes)
      ASSUME_YES=1
      ;;
    -c|--check)
      CHECK_ONLY=1
      ;;
    -h|--help)
      usage
      exit 0
      ;;
    *)
      printf 'Unknown option: %s\n' "$1" >&2
      usage >&2
      exit 2
      ;;
  esac
  shift
done

need_file "$MANIFEST" "Run install.sh to install DigiHub first."

CheckDhInfo

if ! command -v git >/dev/null 2>&1; then
  err_section "Missing Dependency"
  err_kv "Command" "git"
  err_kv "Action" "Install git and try again."
  printf "\n" >&2
  exit 1
fi

if ! ping -c 1 -W 1 1.1.1.1 >/dev/null 2>&1; then
  err_section "No Connectivity"
  err_kv "Message" "No internet connectivity detected; dhupdate requires network access."
  printf "\n" >&2
  exit 1
fi

section "DigiHub Update"
printf 'Fetching latest scripts from %s...\n' "$REPO_URL"

CloneDir="$(mktemp -d)"

if ! git clone --quiet "$REPO_URL" "$CloneDir" >/dev/null 2>&1; then
  err_section "Fetch Failed"
  err_kv "Message" "Could not clone $REPO_URL"
  printf "\n" >&2
  exit 1
fi

if [[ ! -d "$CloneDir/scripts" || ! -f "$CloneDir/lib/dh.lib" || ! -d "$CloneDir/systemd" || ! -d "$CloneDir/man" || ! -d "$CloneDir/dhweb" ]]; then
  err_section "Unexpected Repository Layout"
  err_kv "Message" "scripts/, lib/dh.lib, systemd/, man/, or dhweb/ not found in the cloned repository."
  printf "\n" >&2
  exit 1
fi

# Map an installed destination path back to its source in the clone.
RepoSrcFor() {
  local dest="$1" base
  case "$dest" in
    "$LIBDIR"/dhweb/*)
      printf '%s\n' "$CloneDir/dhweb/${dest#"$LIBDIR/dhweb/"}"
      return
      ;;
  esac
  base="$(basename "$dest")"
  case "$base" in
    dh.lib) printf '%s\n' "$CloneDir/lib/dh.lib" ;;
    *.service|*.timer) printf '%s\n' "$CloneDir/systemd/$base" ;;
    *.1) printf '%s\n' "$CloneDir/man/$base" ;;
    *) printf '%s\n' "$CloneDir/scripts/$base" ;;
  esac
}

# Build the new (repo) file list: /usr/local/bin/<name> for each top-level
# script, plus the shared UI library, every cached systemd unit template,
# every installed man page, and the dhweb application tree.
NewManifest="$(mktemp)"
find "$CloneDir/scripts" -maxdepth 1 -type f -printf '/usr/local/bin/%f\n' > "$NewManifest"
printf '%s\n' "$LIBDIR/dh.lib" >> "$NewManifest"
find "$CloneDir/systemd" -maxdepth 1 -type f -printf "$LIBDIR/%f\n" >> "$NewManifest"
find "$CloneDir/man" -maxdepth 1 -type f -printf '/usr/local/share/man/man1/%f\n' >> "$NewManifest"
find "$CloneDir/dhweb" -type f -printf "$LIBDIR/dhweb/%P\n" >> "$NewManifest"
LC_ALL=C sort -u "$NewManifest" -o "$NewManifest"

# Build the currently-installed (manifest) file list, restricted to the same
# hard safety gates used by dhremove/install.sh.
OldManifestFiltered="$(mktemp)"
if [[ -r "$MANIFEST" ]]; then
  while IFS= read -r p; do
    [[ -n "${p//[[:space:]]/}" ]] || continue
    case "$p" in
      /usr/local/bin/*|/usr/local/lib/digihub/*|/usr/local/share/man/man1/*) ;;
      *) continue ;;
    esac
    [[ "$p" != "/usr/local/bin" && "$p" != "/usr/local/lib/digihub" && "$p" != "/usr/local/share/man/man1" ]] || continue
    [[ "$p" != *"*"* && "$p" != *"?"* && "$p" != *"["* ]] || continue
    printf '%s\n' "$p"
  done < "$MANIFEST" | LC_ALL=C sort -u > "$OldManifestFiltered"
fi

Added=()
Updated=()
Removed=()

while IFS= read -r dest; do
  [[ -n "$dest" ]] || continue

  src="$(RepoSrcFor "$dest")"

  if [[ ! -e "$dest" ]]; then
    Added+=("$dest")
  elif ! cmp -s "$src" "$dest"; then
    Updated+=("$dest")
  fi
done < "$NewManifest"

while IFS= read -r dest; do
  [[ -n "$dest" ]] || continue
  Removed+=("$dest")
done < <(LC_ALL=C comm -23 "$OldManifestFiltered" "$NewManifest")

if (( ${#Added[@]} == 0 && ${#Updated[@]} == 0 && ${#Removed[@]} == 0 )); then
  ClearUpdateFlag
  printf '\nDigiHub is already up to date.\n\n'
  exit 0
fi

if (( ${#Added[@]} > 0 )); then
  section "New"
  for f in "${Added[@]}"; do kv "+" "$f"; done
fi

if (( ${#Updated[@]} > 0 )); then
  section "Changed"
  for f in "${Updated[@]}"; do kv "~" "$f"; done
fi

if (( ${#Removed[@]} > 0 )); then
  section "Removed"
  for f in "${Removed[@]}"; do kv "-" "$f"; done
fi

printf "\n"

if (( CHECK_ONLY == 1 )); then
  WriteUpdateFlag
  printf 'Run "dhupdate" to review and apply these changes.\n\n'
  exit 3
fi

if (( ASSUME_YES == 0 )); then
  response=""
  while true; do
    read -n1 -rp "Apply these changes (Y/n)? " response </dev/tty
    printf '\n'
    case "$response" in
      Y|y|'') break ;;
      N|n) WriteUpdateFlag; printf '\nUpdate cancelled.\n\n'; exit 0 ;;
      *) printf 'Please select Y/n.\n' ;;
    esac
  done
fi

ManPagesChanged=0
ServicesToRestart=()
SelfUpdated=0

if (( ${#Added[@]} > 0 || ${#Updated[@]} > 0 )); then
  run_sudo install -d -m 0755 /usr/local/bin
  run_sudo install -d -m 0755 "$LIBDIR"
  run_sudo install -d -m 0755 /usr/local/share/man/man1
fi

for dest in ${Added[@]+"${Added[@]}"} ${Updated[@]+"${Updated[@]}"}; do
  src="$(RepoSrcFor "$dest")"

  case "$dest" in
    "$LIBDIR"/*|/usr/local/share/man/man1/*)
      run_sudo install -D -m 0644 "$src" "$dest"
      ;;
    *)
      run_sudo install -D -m 0755 "$src" "$dest"
      ;;
  esac

  [[ "$dest" == "/usr/local/bin/dhupdate" ]] && SelfUpdated=1
  case "$dest" in
    /usr/local/share/man/man1/*) ManPagesChanged=1 ;;
    *) MarkServiceForDest "$dest" ;;
  esac
done

for dest in ${Removed[@]+"${Removed[@]}"}; do
  run_sudo rm -f -- "$dest" || true
  case "$dest" in
    /usr/local/share/man/man1/*) ManPagesChanged=1 ;;
    *) MarkServiceForDest "$dest" ;;
  esac
done

TmpManifest="$(mktemp)"
cp "$NewManifest" "$TmpManifest"
run_sudo install -d -m 0755 /usr/local/bin
run_sudo install -m 0644 "$TmpManifest" "$MANIFEST"
rm -f "$TmpManifest"

ClearUpdateFlag

printf '\nUpdate complete: %d added, %d updated, %d removed.\n' \
  "${#Added[@]}" "${#Updated[@]}" "${#Removed[@]}"

if (( ManPagesChanged == 1 )) && command -v mandb >/dev/null 2>&1; then
  run_sudo mandb -q >/dev/null 2>&1 || true
fi

if command -v systemctl >/dev/null 2>&1; then
  for svc in ${ServicesToRestart[@]+"${ServicesToRestart[@]}"}; do
    if systemctl is-active --quiet "$svc" 2>/dev/null; then
      printf 'Restarting %s to apply the update...\n' "$svc"
      run_sudo systemctl restart "$svc" || true
    fi
  done
fi

printf "\n"

# dhupdate just overwrote its own file on disk -- this process is still
# running the old copy from memory, and anything this update itself
# changed in dhupdate's own logic (a new restart mapping, a bug fix,
# etc.) needs a fresh process to take effect. Re-run it: the second
# pass finds nothing left to do and exits immediately, but it proves
# the newly-installed dhupdate actually runs end-to-end.
if (( SelfUpdated == 1 )); then
  printf 'dhupdate itself was updated -- re-running to apply it...\n\n'
  cleanup
  RelaunchArgs=()
  (( ASSUME_YES == 1 )) && RelaunchArgs+=(--yes)
  exec /usr/local/bin/dhupdate ${RelaunchArgs[@]+"${RelaunchArgs[@]}"}
fi
