#!/usr/bin/env bash
#
#  Solvento Linux Agent - installer
#  Solvento V.O.F. - solvento.nl
#
#  Interactief installeren:
#    wget -qO- https://agent.solvento.nl/install.sh | sudo bash
#  of
#    wget https://agent.solvento.nl/install.sh && sudo bash install.sh
#
#  Zonder vragen (bulk uitrol):
#    wget -qO- https://agent.solvento.nl/install.sh | sudo bash -s -- --unattended \
#      --token "123456:ABC..." --chat-id "-100123456" --customer "Klant BV" \
#      --day zo --time 20:00 --reboot smart
#
#  Alle opties:
#    --token, --chat-id, --thread-id, --customer, --label,
#    --day (ma|di|wo|do|vr|za|zo|dagelijks), --time HH:MM, --random-delay MIN,
#    --reboot (smart|always|never), --services "unifi mongod",
#    --health "https://127.0.0.1:8443", --timezone Europe/Amsterdam,
#    --no-test, --unattended, --uninstall
#
#  Opnieuw draaien = agent bijwerken (config blijft behouden).
#
set -uo pipefail

readonly AGENT_NAME="Solvento Linux Agent"
readonly AGENT_VERSION="1.1.1"
readonly BIN=/usr/local/sbin/solvento-agent
readonly CONF_DIR=/etc/solvento
readonly CONF=$CONF_DIR/agent.conf
readonly UNIT=solvento-agent

# ---------------------------------------------------------------------------
# Opmaak
# ---------------------------------------------------------------------------
if [[ -t 1 ]]; then
  B=$'\e[1m'; D=$'\e[2m'; R=$'\e[0m'
  C=$'\e[38;5;39m'; G=$'\e[32m'; Y=$'\e[33m'; E=$'\e[31m'
else
  B=""; D=""; R=""; C=""; G=""; Y=""; E=""
fi

banner() {
  printf '\n%s%s' "$C" "$B"
  cat <<'EOF'
   ____        _                 _
  / ___|  ___ | |_   _____ _ __ | |_ ___
  \___ \ / _ \| \ \ / / _ \ '_ \| __/ _ \
   ___) | (_) | |\ V /  __/ | | | || (_) |
  |____/ \___/|_| \_/ \___|_| |_|\__\___/
  
EOF
  printf '%s' "$R"
  printf '  %sLinux Agent v%s%s   %sautomatische updates met Telegram-rapportage%s\n' \
    "$B" "$AGENT_VERSION" "$R" "$D" "$R"
  printf '  %ssolvento.nl%s\n\n' "$D" "$R"
}

section() { printf '\n%s%s▸ %s%s\n' "$C" "$B" "$1" "$R"; }
ok()      { printf '  %s✔%s %s\n' "$G" "$R" "$1"; }
warn()    { printf '  %s!%s %s\n' "$Y" "$R" "$1"; }
fail()    { printf '\n  %s✖ %s%s\n\n' "$E" "$1" "$R"; exit 1; }
note()    { printf '  %s%s%s\n' "$D" "$1" "$R"; }

# ---------------------------------------------------------------------------
# Invoer
# ---------------------------------------------------------------------------
UNATTENDED=0
NO_TEST=0
DO_UNINSTALL=0
MODE=""   # new | upgrade | reconfigure

# Waarden (gevuld vanuit bestaande config en/of opties)
TELEGRAM_BOT_TOKEN=""
TELEGRAM_CHAT_ID=""
TELEGRAM_THREAD_ID=""
CUSTOMER=""
HOST_LABEL=""
SCHEDULE="Sun *-*-* 20:00:00"
RANDOM_DELAY_MIN=0
REBOOT="smart"
WATCH_SERVICES=""
HEALTHCHECK_URLS=""
DAY=""
TIME=""

declare -A OPT=()

usage() {
  cat <<'EOF'
Solvento Linux Agent - installer

  Interactief:   wget -qO- https://agent.solvento.nl/install.sh | sudo bash
  Zonder vragen: ... | sudo bash -s -- --unattended --token X --chat-id Y [opties]

  --token, --chat-id, --thread-id, --customer, --label
  --day (ma|di|wo|do|vr|za|zo|dagelijks), --time HH:MM, --random-delay MIN
  --reboot (smart|always|never), --services "unifi mongod"
  --health "https://127.0.0.1:8443", --timezone Europe/Amsterdam
  --no-test, --unattended, --uninstall
EOF
  exit 0
}

parse_args() {
  while (( $# )); do
    case $1 in
      --token)        OPT[token]=${2:-};    shift ;;
      --chat-id)      OPT[chat]=${2:-};     shift ;;
      --thread-id)    OPT[thread]=${2:-};   shift ;;
      --customer)     OPT[customer]=${2:-}; shift ;;
      --label)        OPT[label]=${2:-};    shift ;;
      --day)          OPT[day]=${2:-};      shift ;;
      --time)         OPT[time]=${2:-};     shift ;;
      --random-delay) OPT[delay]=${2:-};    shift ;;
      --reboot)       OPT[reboot]=${2:-};   shift ;;
      --services)     OPT[services]=${2:-}; shift ;;
      --health)       OPT[health]=${2:-};   shift ;;
      --timezone)     OPT[timezone]=${2:-}; shift ;;
      --unattended|-y) UNATTENDED=1 ;;
      --no-test)      NO_TEST=1 ;;
      --uninstall)    DO_UNINSTALL=1 ;;
      -h|--help)      usage ;;
      *) fail "Onbekende optie: $1 (zie --help)" ;;
    esac
    shift
  done
}

# ask VAR "vraag" "standaard"
ask() {
  local __var=$1 q=$2 def=${3:-} ans=""
  if (( UNATTENDED )); then
    printf -v "$__var" '%s' "$def"
    return
  fi
  if [[ -n $def ]]; then
    read -r -p "  ${q} ${D}[${def}]${R}: " ans </dev/tty
  else
    read -r -p "  ${q}: " ans </dev/tty
  fi
  ans=${ans#"${ans%%[![:space:]]*}"}
  ans=${ans%"${ans##*[![:space:]]}"}
  printf -v "$__var" '%s' "${ans:-$def}"
}

# ask_yn "vraag" j|n  -> return 0 bij ja
ask_yn() {
  local q=$1 def=${2:-j} ans hint
  (( UNATTENDED )) && { [[ $def == j ]]; return; }
  [[ $def == j ]] && hint="J/n" || hint="j/N"
  read -r -p "  ${q} ${D}[${hint}]${R}: " ans </dev/tty
  ans=${ans:-$def}
  [[ ${ans,,} == j* || ${ans,,} == y* ]]
}

# choose VAR "vraag" standaard-nummer "optie1" "optie2" ...
choose() {
  local __var=$1 q=$2 def=$3 ans i
  shift 3
  local opts=("$@")
  if (( UNATTENDED )); then
    printf -v "$__var" '%s' "$def"
    return
  fi
  printf '  %s\n' "$q"
  for i in "${!opts[@]}"; do
    printf '    %s%d)%s %s\n' "$B" $(( i + 1 )) "$R" "${opts[$i]}"
  done
  while :; do
    read -r -p "  Keuze ${D}[${def}]${R}: " ans </dev/tty
    ans=${ans:-$def}
    if [[ $ans =~ ^[0-9]+$ ]] && (( ans >= 1 && ans <= ${#opts[@]} )); then
      printf -v "$__var" '%s' "$ans"
      return
    fi
    warn "Kies een nummer tussen 1 en ${#opts[@]}"
  done
}

# Tekst mag geen tekens bevatten die de config kunnen breken
safe_text() { [[ $1 != *[\"\$\`\\]* ]]; }

invalid() {
  (( UNATTENDED )) && fail "$1"
  warn "$1"
}

# ---------------------------------------------------------------------------
# Telegram controles
# ---------------------------------------------------------------------------
tg_api() {
  # tg_api <methode> [key=value ...]  -> JSON op stdout
  local method=$1 kv
  shift
  local args=()
  for kv in "$@"; do args+=(--data-urlencode "$kv"); done
  curl -s --max-time 15 "${args[@]}" -K - \
    <<<"url = \"https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/${method}\"" 2>/dev/null
}

json_str() {
  # json_str <key> : eerste "key":"waarde" uit stdin
  grep -o "\"$1\":\"[^\"]*\"" | head -n 1 | cut -d'"' -f4
}

check_token() {
  local res name
  res=$(tg_api getMe)
  if [[ $res == *'"ok":true'* ]]; then
    name=$(json_str username <<<"$res")
    ok "Bot gevonden: @${name}"
    return 0
  fi
  return 1
}

check_chat() {
  local res title
  res=$(tg_api getChat "chat_id=${TELEGRAM_CHAT_ID}")
  if [[ $res == *'"ok":true'* ]]; then
    title=$(json_str title <<<"$res")
    [[ -z $title ]] && title=$(json_str first_name <<<"$res")
    ok "Chat gevonden: ${title:-$TELEGRAM_CHAT_ID}"
    return 0
  fi
  return 1
}

# ---------------------------------------------------------------------------
# Schema omrekenen
# ---------------------------------------------------------------------------
day_to_cal() {
  case ${1,,} in
    ma|maandag) echo Mon ;;   di|dinsdag) echo Tue ;;  wo|woensdag) echo Wed ;;
    "do"|donderdag) echo Thu ;; vr|vrijdag) echo Fri ;;  za|zaterdag) echo Sat ;;
    zo|zondag) echo Sun ;;    dagelijks|elke-dag) echo "*" ;;
    *) echo "" ;;
  esac
}

cal_to_day() {
  case $1 in
    Mon) echo ma ;; Tue) echo di ;; Wed) echo wo ;; Thu) echo "do" ;;
    Fri) echo vr ;; Sat) echo za ;; Sun) echo zo ;; *) echo dagelijks ;;
  esac
}

day_long() {
  case $1 in
    ma) echo maandag ;; di) echo dinsdag ;; wo) echo woensdag ;; "do") echo donderdag ;;
    vr) echo vrijdag ;; za) echo zaterdag ;; zo) echo zondag ;; *) echo "elke dag" ;;
  esac
}

parse_schedule() {
  # Zet DAY en TIME vanuit een bestaande SCHEDULE (alleen het standaardformaat)
  local re='^((Mon|Tue|Wed|Thu|Fri|Sat|Sun) )?\*-\*-\* ([0-9]{2}:[0-9]{2})'
  if [[ $SCHEDULE =~ $re ]]; then
    DAY=$(cal_to_day "${BASH_REMATCH[2]:-*}")
    TIME=${BASH_REMATCH[3]}
  else
    DAY=zo; TIME=20:00
  fi
}

# ---------------------------------------------------------------------------
# Slimme detectie van services op deze VM
# ---------------------------------------------------------------------------
detect_services() {
  local s found=()
  local candidates=(unifi mongod nginx apache2 mysql mariadb postgresql docker \
                    redis-server haproxy caddy tomcat9 tomcat10 grafana-server \
                    zabbix-server zabbix-agent zabbix-agent2 librenms)
  for s in "${candidates[@]}"; do
    systemctl is-enabled --quiet "$s" 2>/dev/null && found+=("$s")
  done
  while read -r s; do
    [[ -n $s ]] && systemctl is-enabled --quiet "$s" 2>/dev/null && found+=("${s%.service}")
  done < <(systemctl list-unit-files --type=service --no-legend 2>/dev/null \
             | awk '{print $1}' | grep -E '^php[0-9.]+-fpm\.service$')
  printf '%s' "${found[*]}"
}

detect_health() {
  local urls=()
  systemctl is-enabled --quiet unifi 2>/dev/null && urls+=("https://127.0.0.1:8443")
  printf '%s' "${urls[*]}"
}

# ---------------------------------------------------------------------------
# Stappen
# ---------------------------------------------------------------------------
preflight() {
  section "Systeem controleren"
  (( EUID == 0 )) || fail "Draai de installer als root, bijvoorbeeld: wget -qO- <url> | sudo bash"
  command -v apt-get >/dev/null || fail "Geen apt-get gevonden. Deze agent is voor Ubuntu/Debian."
  command -v systemctl >/dev/null || fail "Geen systemd gevonden."

  local os
  os=$( . /etc/os-release 2>/dev/null && echo "${PRETTY_NAME:-onbekend}" )
  ok "Besturingssysteem: ${os}"
  [[ $os == Ubuntu* ]] || warn "Getest op Ubuntu; ${os} zou moeten werken maar is niet getest."
  ok "Hostname: $(hostname -f 2>/dev/null || hostname)"
  ok "Tijdzone: $(timedatectl show -p Timezone --value 2>/dev/null || echo onbekend)"

  if (( ! UNATTENDED )); then
    { : </dev/tty; } 2>/dev/null || fail "Geen terminal beschikbaar voor vragen. Gebruik --unattended met opties."
  fi

  if ! command -v curl >/dev/null; then
    note "curl installeren..."
    DEBIAN_FRONTEND=noninteractive apt-get -y -q install curl </dev/null >/dev/null 2>&1 \
      || fail "Kon curl niet installeren."
    ok "curl geinstalleerd"
  fi
}

# ---------------------------------------------------------------------------
# Tijdzone en klok
# ---------------------------------------------------------------------------
is_tz() { [[ -n $1 && $1 != *..* && -f /usr/share/zoneinfo/$1 ]]; }

set_tz() {
  local tz=$1
  timedatectl set-timezone "$tz" 2>/dev/null && return 0
  ln -sf "/usr/share/zoneinfo/$tz" /etc/localtime && echo "$tz" >/etc/timezone
}

current_tz() {
  local tz
  tz=$(timedatectl show -p Timezone --value 2>/dev/null)
  [[ -z $tz && -f /etc/timezone ]] && tz=$(</etc/timezone)
  printf '%s' "$tz"
}

check_timezone() {
  local tz new ntp
  tz=$(current_tz)

  if [[ -n ${OPT[timezone]:-} ]]; then
    new=${OPT[timezone]}
    is_tz "$new" || fail "Onbekende tijdzone: $new (gebruik bijvoorbeeld Europe/Amsterdam)"
    if [[ $new != "$tz" ]]; then
      section "Tijdzone"
      set_tz "$new" && ok "Tijdzone ingesteld: $new (nu $(date '+%H:%M'))"                     || fail "Kon de tijdzone niet instellen op $new"
    fi
  elif [[ -z $tz || $tz =~ ^(Etc/)?(UTC|UCT|Universal|Zulu|GMT|GMT0|Greenwich)$ ]]; then
    section "Tijdzone"
    warn "Deze server staat op ${tz:-een onbekende tijdzone}. De planning volgt de tijd van de server,"
    warn "dus 20:00 zou nu 20:00 ${tz:-UTC} worden in plaats van 20:00 Nederlandse tijd."
    if (( UNATTENDED )); then
      warn "Tijdzone niet aangepast. Gebruik --timezone Europe/Amsterdam om dit mee te nemen."
    elif ask_yn "Tijdzone aanpassen?" j; then
      note "Voorbeelden: Europe/Amsterdam (Nederland), Europe/Brussels (Belgie), Europe/Berlin (Duitsland)"
      ask_valid new "Nieuwe tijdzone" "Europe/Amsterdam" is_tz \
        "Onbekende tijdzone. Gebruik het formaat Regio/Stad, bijvoorbeeld Europe/Amsterdam."
      if set_tz "$new"; then
        ok "Tijdzone ingesteld: $new (nu $(date '+%H:%M'))"
      else
        warn "Kon de tijdzone niet instellen. Doe het later met: sudo timedatectl set-timezone $new"
      fi
    else
      note "Tijdzone blijft ${tz:-ongewijzigd}."
    fi
  fi

  ntp=$(timedatectl show -p NTPSynchronized --value 2>/dev/null)
  if [[ $ntp == no ]]; then
    warn "De systeemklok wordt niet gesynchroniseerd via NTP."
    if (( UNATTENDED )) || ask_yn "NTP-synchronisatie aanzetten?" j; then
      timedatectl set-ntp true 2>/dev/null && ok "NTP aangezet" \
        || warn "Kon NTP niet aanzetten. Controleer systemd-timesyncd of chrony."
    fi
  fi
}

load_existing() {
  if [[ -f $CONF ]]; then
    # shellcheck disable=SC1090
    . "$CONF"
    return 0
  fi
  return 1
}

apply_options() {
  [[ -n ${OPT[token]:-} ]]    && TELEGRAM_BOT_TOKEN=${OPT[token]}
  [[ -n ${OPT[chat]:-} ]]     && TELEGRAM_CHAT_ID=${OPT[chat]}
  [[ -n ${OPT[thread]:-} ]]   && TELEGRAM_THREAD_ID=${OPT[thread]}
  [[ -n ${OPT[customer]:-} ]] && CUSTOMER=${OPT[customer]}
  [[ -n ${OPT[label]:-} ]]    && HOST_LABEL=${OPT[label]}
  [[ -n ${OPT[delay]:-} ]]    && RANDOM_DELAY_MIN=${OPT[delay]}
  [[ -n ${OPT[reboot]:-} ]]   && REBOOT=${OPT[reboot]}
  [[ -n ${OPT[services]:-} ]] && WATCH_SERVICES=${OPT[services]}
  [[ -n ${OPT[health]:-} ]]   && HEALTHCHECK_URLS=${OPT[health]}
  parse_schedule
  [[ -n ${OPT[day]:-} ]]      && DAY=${OPT[day]}
  [[ -n ${OPT[time]:-} ]]     && TIME=${OPT[time]}
  return 0
}

choose_mode() {
  local c
  if ! load_existing; then
    MODE=new
    return
  fi
  apply_options
  section "Bestaande installatie gevonden"
  ok "$("$BIN" version 2>/dev/null || echo "$AGENT_NAME") met config ${CONF}"
  if (( UNATTENDED )); then
    if (( ${#OPT[@]} )); then MODE=reconfigure; else MODE=upgrade; fi
    return
  fi
  choose c "Wat wil je doen?" 1 \
    "Agent bijwerken naar v${AGENT_VERSION}, config behouden" \
    "Opnieuw configureren" \
    "Verwijderen" \
    "Annuleren"
  case $c in
    1) MODE=upgrade ;;
    2) MODE=reconfigure ;;
    3) do_uninstall ;;
    4) printf '\n  Geannuleerd.\n\n'; exit 0 ;;
  esac
}

# Validators
is_token()  { [[ $1 =~ ^[0-9]+:[A-Za-z0-9_-]{30,}$ ]]; }
is_chat()   { [[ $1 =~ ^-?[0-9]+$ ]]; }
is_thread() { [[ -z $1 || $1 =~ ^[0-9]+$ ]]; }
is_day()    { [[ -n $(day_to_cal "$1") ]]; }
is_time()   { [[ $1 =~ ^([01][0-9]|2[0-3]):[0-5][0-9]$ ]]; }
is_num()    { [[ $1 =~ ^[0-9]+$ ]]; }

# ask_valid VAR "vraag" "standaard" validator "foutmelding"
# Bij ongeldige invoer blijft de oorspronkelijke standaard staan.
ask_valid() {
  local __var=$1 q=$2 def=$3 check=$4 msg=$5 __val
  while :; do
    ask __val "$q" "$def"
    if "$check" "$__val"; then
      printf -v "$__var" '%s' "$__val"
      return 0
    fi
    invalid "$msg"
  done
}

configure() {
  local c tok_ok=0 det_svc det_url cal
  local bad_text='Gebruik geen " $ ` of \ in deze tekst.'

  [[ $MODE == new ]] && apply_options

  # --- Telegram ---
  section "Telegram"
  (( UNATTENDED )) || note "Maak een bot via @BotFather en voeg hem toe aan je groep."
  while :; do
    ask_valid TELEGRAM_BOT_TOKEN "Bot token" "$TELEGRAM_BOT_TOKEN" is_token \
      "Dat lijkt geen geldige bot token (formaat 123456:ABC...)."
    if check_token; then tok_ok=1; break; fi
    if (( UNATTENDED )); then
      warn "Token kon niet gecontroleerd worden (geen verbinding met Telegram?), ga toch door."
      break
    fi
    warn "Telegram kent deze token niet, of er is geen verbinding met Telegram."
    ask_yn "Toch deze token gebruiken?" n && break
  done

  while :; do
    ask_valid TELEGRAM_CHAT_ID "Chat ID (groepen beginnen met -)" "$TELEGRAM_CHAT_ID" is_chat \
      "Een chat ID bestaat alleen uit cijfers, eventueel met een - ervoor."
    (( tok_ok )) || break
    check_chat && break
    if (( UNATTENDED )); then
      warn "Chat niet gevonden. Zit de bot in de groep?"
      break
    fi
    warn "De bot kan deze chat niet zien. Zit de bot in de groep?"
    ask_yn "Toch dit chat ID gebruiken?" n && break
  done

  ask_valid TELEGRAM_THREAD_ID "Topic ID, alleen bij een groep met Topics (leeg = geen)" \
    "$TELEGRAM_THREAD_ID" is_thread "Een topic ID bestaat alleen uit cijfers."

  # --- Identificatie ---
  section "Identificatie"
  ask_valid CUSTOMER "Klantnaam in de rapportage (leeg = niet tonen)" "$CUSTOMER" safe_text "$bad_text"
  ask_valid HOST_LABEL "Naam van deze server in Telegram" \
    "${HOST_LABEL:-$(hostname -f 2>/dev/null || hostname)}" safe_text "$bad_text"

  # --- Planning ---
  section "Planning"
  ask_valid DAY "Dag (ma, di, wo, do, vr, za, zo of dagelijks)" "${DAY:-zo}" is_day \
    "Onbekende dag. Kies ma, di, wo, do, vr, za, zo of dagelijks."
  cal=$(day_to_cal "$DAY")
  DAY=$(cal_to_day "$cal")
  ask_valid TIME "Tijd (HH:MM)" "${TIME:-20:00}" is_time "Gebruik het formaat HH:MM, bijvoorbeeld 20:00."
  if [[ $cal == "*" ]]; then
    SCHEDULE="*-*-* ${TIME}:00"
  else
    SCHEDULE="${cal} *-*-* ${TIME}:00"
  fi
  ask_valid RANDOM_DELAY_MIN "Willekeurige spreiding in minuten (0 = precies op tijd)" \
    "${RANDOM_DELAY_MIN:-0}" is_num "Vul een aantal minuten in."

  # --- Reboot ---
  section "Reboot na updates"
  case $REBOOT in always) c=2 ;; never) c=3 ;; *) c=1 ;; esac
  if (( UNATTENDED )); then
    [[ $REBOOT =~ ^(smart|always|never)$ ]] || fail "--reboot moet smart, always of never zijn."
    note "Reboot: $REBOOT"
  else
    choose c "Wanneer mag de server herstarten?" "$c" \
      "Slim: alleen bij nieuwe kernel, reboot-required of lange uptime (aanbevolen)" \
      "Altijd na de updates" \
      "Nooit (wel melden als het nodig is)"
    case $c in 1) REBOOT=smart ;; 2) REBOOT=always ;; 3) REBOOT=never ;; esac
  fi

  # --- Controle na reboot ---
  section "Controle na reboot"
  if [[ $MODE == new && -z $WATCH_SERVICES ]]; then
    det_svc=$(detect_services)
    [[ -n $det_svc ]] && note "Gevonden op deze server: ${det_svc}"
    WATCH_SERVICES=$det_svc
  fi
  if [[ $MODE == new && -z $HEALTHCHECK_URLS ]]; then
    det_url=$(detect_health)
    [[ -n $det_url ]] && note "UniFi gevonden, health check voorgesteld: ${det_url}"
    HEALTHCHECK_URLS=$det_url
  fi
  ask_valid WATCH_SERVICES "Services die na reboot moeten draaien (spatie-gescheiden)" \
    "$WATCH_SERVICES" safe_text "$bad_text"
  ask_valid HEALTHCHECK_URLS "URL's die na reboot moeten antwoorden (spatie-gescheiden)" \
    "$HEALTHCHECK_URLS" safe_text "$bad_text"
}

summary() {
  local reboot_txt
  case $REBOOT in
    smart)  reboot_txt="slim (alleen als nodig)" ;;
    always) reboot_txt="altijd" ;;
    never)  reboot_txt="nooit" ;;
  esac
  section "Samenvatting"
  printf '  %-16s %s\n' "Server:"       "$HOST_LABEL"
  printf '  %-16s %s\n' "Klant:"        "${CUSTOMER:--}"
  printf '  %-16s %s\n' "Telegram:"     "chat ${TELEGRAM_CHAT_ID}${TELEGRAM_THREAD_ID:+, topic $TELEGRAM_THREAD_ID}"
  printf '  %-16s %s\n' "Planning:"     "$(day_long "$DAY") om ${TIME}$( (( RANDOM_DELAY_MIN > 0 )) && echo " (+ max ${RANDOM_DELAY_MIN} min spreiding)")"
  printf '  %-16s %s\n' "Reboot:"       "$reboot_txt"
  printf '  %-16s %s\n' "Services:"     "${WATCH_SERVICES:--}"
  printf '  %-16s %s\n' "Health checks:" "${HEALTHCHECK_URLS:--}"
  echo
  if (( ! UNATTENDED )); then
    ask_yn "Installeren met deze instellingen?" j || { printf '\n  Geannuleerd.\n\n'; exit 0; }
  fi
}

write_agent() {
  local tmp
  tmp=$(mktemp)
  cat >"$tmp" <<'SOLVENTO_AGENT_EOF'
#!/usr/bin/env bash
#
#  Solvento Linux Agent
#  Unattended updates voor Ubuntu VM's met slimme reboot en Telegram-rapportage.
#  Solvento V.O.F. - solvento.nl
#
#  Installeren gaat via de installer:
#    wget -qO- https://agent.solvento.nl/install.sh | sudo bash
#
#  Commando's:
#    sudo solvento-agent run [--no-reboot]    updates nu uitvoeren
#    sudo solvento-agent check                laten zien wat er zou gebeuren
#    sudo solvento-agent test                 Telegram testbericht
#    sudo solvento-agent status               planning en laatste run
#    sudo solvento-agent install              (her)installeren, schema bijwerken
#    sudo solvento-agent uninstall            verwijderen (config en logs blijven)
#
set -uo pipefail

readonly AGENT_NAME="Solvento Linux Agent"
readonly AGENT_VERSION="1.1.1"
readonly BIN=/usr/local/sbin/solvento-agent
readonly CONF_DIR=/etc/solvento
readonly CONF=$CONF_DIR/agent.conf
readonly LOG_DIR=/var/log/solvento-agent
readonly STATE_DIR=/var/lib/solvento-agent
readonly LOCK_FILE=/run/lock/solvento-agent.lock
readonly UNIT_DIR=/etc/systemd/system
readonly UNIT=solvento-agent

# ---------------------------------------------------------------------------
# Standaardwaarden (overschrijfbaar in /etc/solvento/agent.conf)
# ---------------------------------------------------------------------------
TELEGRAM_BOT_TOKEN=""
TELEGRAM_CHAT_ID=""
TELEGRAM_THREAD_ID=""
CUSTOMER=""
HOST_LABEL=""
SCHEDULE="Sun *-*-* 20:00:00"
RANDOM_DELAY_MIN=0
UPGRADE_MODE="upgrade"
REBOOT="smart"
MAX_UPTIME_DAYS=60
RESTART_SERVICES="yes"
REBOOT_ON_ERROR="no"
REBOOT_DELAY_MIN=1
SNAP_REFRESH="yes"
WATCH_SERVICES=""
HEALTHCHECK_URLS=""
NOTIFY_NO_UPDATES="yes"
MIN_FREE_ROOT_MB=1000
MIN_FREE_BOOT_MB=150
KEEP_LOG_DAYS=90

# shellcheck disable=SC1090
[[ -r $CONF ]] && . "$CONF"

SCRIPT_DIR=$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")

set_derived() {
  HOST="${HOST_LABEL:-$(hostname -f 2>/dev/null || hostname)}"
  if [[ $UPGRADE_MODE == full-upgrade ]]; then
    UPG_ARGS=(full-upgrade)
  else
    UPG_ARGS=(upgrade --with-new-pkgs)
  fi
}
set_derived

# ---------------------------------------------------------------------------
# Alles non-interactive: geen prompts, geen pagers, geen menu's
# ---------------------------------------------------------------------------
export DEBIAN_FRONTEND=noninteractive
export DEBCONF_NONINTERACTIVE_SEEN=true
export APT_LISTCHANGES_FRONTEND=none
export APT_LISTBUGS_FRONTEND=none
export NEEDRESTART_MODE=a
export NEEDRESTART_SUSPEND=1
export UCF_FORCE_CONFFOLD=1
export PAGER=cat SYSTEMD_PAGER=cat MANPAGER=cat
export TERM=dumb
export LC_ALL=C.UTF-8 LANG=C.UTF-8
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

APT_OPTS=(
  -y -q
  -o Dpkg::Options::=--force-confdef
  -o Dpkg::Options::=--force-confold
  -o Dpkg::Lock::Timeout=1800
  -o DPkg::Use-Pty=0
  -o Acquire::Retries=5
  -o Acquire::http::Timeout=60
  -o Acquire::https::Timeout=60
)

LOG=""
START=$SECONDS
FINISHED=0
REBOOT_PLAN=0
SEC_COUNT=0
SNAP_CHANGED=""
KERNEL_BEFORE=$(uname -r)
ERRORS=()
WARNINGS=()
INFO=()
PKGS=()
REASONS=()
RESTARTED=()

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
log() {
  local m
  m="[$(date '+%F %T')] $*"
  echo "$m"
  if [[ -n $LOG ]]; then echo "$m" >>"$LOG"; fi
}

require_root() {
  (( EUID == 0 )) || { echo "Draai dit als root (sudo)."; exit 1; }
}

join_by() {
  local d=$1 out="" x
  shift
  for x in "$@"; do out+="${out:+$d}$x"; done
  printf '%s' "$out"
}

esc() {
  local s=$1
  s=${s//&/&amp;}; s=${s//</&lt;}; s=${s//>/&gt;}
  printf '%s' "$s"
}

strip_html() {
  sed -e 's/<[^>]*>//g' -e 's/&lt;/</g' -e 's/&gt;/>/g' -e 's/&amp;/\&/g'
}

fmt_dur() {
  local s=$1
  if (( s >= 3600 )); then
    printf '%du %02dm' $(( s / 3600 )) $(( s % 3600 / 60 ))
  else
    printf '%dm %02ds' $(( s / 60 )) $(( s % 60 ))
  fi
}

os_name() {
  ( . /etc/os-release 2>/dev/null && echo "${PRETTY_NAME:-Linux}" ) || echo Linux
}

disk_free() {
  df -h --output=avail "$1" 2>/dev/null | tail -n 1 | tr -d ' '
}

free_mb() {
  df -Pm "$1" 2>/dev/null | awk 'NR==2{print $4+0}'
}

latest_kernel() {
  local f k=()
  for f in /boot/vmlinuz-*; do
    [[ -e $f ]] && k+=("${f#/boot/vmlinuz-}")
  done
  (( ${#k[@]} )) && printf '%s\n' "${k[@]}" | sort -V | tail -n 1
}

# Telegram-bericht (HTML). Token gaat via stdin naar curl, niet zichtbaar in ps.
# Valt terug op platte tekst als de opmaak geweigerd wordt of het bericht te lang is.
tg_send() {
  local text=$1 tries=${TG_TRIES:-6} mode=HTML i
  local args=()
  if [[ -z $TELEGRAM_BOT_TOKEN || -z $TELEGRAM_CHAT_ID ]]; then
    log "Telegram niet geconfigureerd in $CONF, bericht niet verstuurd"
    return 1
  fi
  if (( ${#text} > 4000 )); then
    text=$(strip_html <<<"$text"); mode=""
    (( ${#text} > 4000 )) && text="${text:0:3900}"$'\n\n[ingekort, zie log op de server]'
  fi
  for (( i = 1; i <= tries; i++ )); do
    args=(--data-urlencode "chat_id=${TELEGRAM_CHAT_ID}"
          --data-urlencode "text=${text}"
          --data-urlencode "disable_web_page_preview=true")
    [[ -n $mode ]] && args+=(--data-urlencode "parse_mode=${mode}")
    [[ -n $TELEGRAM_THREAD_ID ]] && args+=(--data-urlencode "message_thread_id=${TELEGRAM_THREAD_ID}")
    if curl -sS -f --max-time 30 "${args[@]}" -K - >/dev/null 2>>"${LOG:-/dev/stderr}" \
         <<<"url = \"https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage\""; then
      log "Telegram-bericht verstuurd"
      return 0
    fi
    if [[ -n $mode ]]; then text=$(strip_html <<<"$text"); mode=""; fi
    if (( i < tries )); then
      log "Telegram poging $i/$tries mislukt, opnieuw over $(( i * 15 ))s"
      sleep $(( i * 15 ))
    fi
  done
  return 1
}

# step <omschrijving> <timeout> <commando...>
# Draait zonder stdin (kan dus nooit op input wachten), logt alles,
# verzamelt fouten en waarschuwingen.
step() {
  local desc=$1 tmo=$2 tmp rc l detail why
  shift 2
  tmp=$(mktemp)
  log "==> $desc"
  timeout --kill-after=2m "$tmo" "$@" </dev/null 2>&1 | tee -a "$LOG" >"$tmp"
  rc=${PIPESTATUS[0]}

  while IFS= read -r l; do
    [[ -n $l ]] && WARNINGS+=("$l")
  done < <(grep -E '^W: ' "$tmp" | head -n 10)

  if (( rc != 0 )); then
    why="exit $rc"
    (( rc == 124 || rc == 137 )) && why="timeout na $tmo"
    ERRORS+=("$desc mislukt ($why)")
    detail=$(grep -E '^(E: |dpkg: error|error:|Errors were encountered)' "$tmp" | head -n 8)
    [[ -z $detail ]] && detail=$(tail -n 5 "$tmp")
    while IFS= read -r l; do
      [[ -n $l ]] && ERRORS+=("  $l")
    done <<<"$detail"
    rm -f "$tmp"
    return 1
  fi
  rm -f "$tmp"
  return 0
}

# Als step, maar een fout telt als waarschuwing (blokkeert de reboot niet).
step_soft() {
  local n=${#ERRORS[@]} e
  step "$@" && return 0
  for e in "${ERRORS[@]:n}"; do WARNINGS+=("$e"); done
  ERRORS=("${ERRORS[@]:0:n}")
  return 1
}

simulate() {
  apt-get -s "${APT_OPTS[@]}" "${UPG_ARGS[@]}" </dev/null 2>/dev/null | grep '^Inst '
}

snap_revs() {
  snap list 2>/dev/null | awk 'NR>1{print $1" "$3}' | sort
}

has_snaps() {
  command -v snap >/dev/null && [[ -n $(snap_revs) ]]
}

outdated_services() {
  command -v needrestart >/dev/null || return 0
  env -u NEEDRESTART_SUSPEND needrestart -b -l </dev/null 2>/dev/null \
    | awk -F': ' '/^NEEDRESTART-SVC:/{print $2}'
}

eval_reboot() {
  local cur new up p=""
  REASONS=()
  if [[ -f /var/run/reboot-required ]]; then
    [[ -f /var/run/reboot-required.pkgs ]] && p=$(sort -u /var/run/reboot-required.pkgs | paste -sd ' ' -)
    REASONS+=("reboot vereist${p:+ door $p}")
  fi
  cur=$(uname -r)
  new=$(latest_kernel)
  if [[ -n $new && $new != "$cur" ]]; then
    REASONS+=("nieuwe kernel $new")
  fi
  if (( MAX_UPTIME_DAYS > 0 )); then
    up=$(( $(cut -d. -f1 /proc/uptime) / 86400 ))
    (( up >= MAX_UPTIME_DAYS )) && REASONS+=("uptime $up dagen (max $MAX_UPTIME_DAYS)")
  fi
}

restart_services() {
  local before=() after=() s
  mapfile -t before < <(outdated_services)
  (( ${#before[@]} )) || return 0
  step_soft "Services met verouderde libraries herstarten" 15m \
    env -u NEEDRESTART_SUSPEND needrestart -r a -l
  mapfile -t after < <(outdated_services)
  for s in "${before[@]}"; do
    [[ " ${after[*]} " == *" $s "* ]] || RESTARTED+=("$s")
  done
  (( ${#after[@]} )) && INFO+=("Nog herstart nodig: ${after[*]}")
}

check_space() {
  local r b fail=0
  r=$(free_mb /);     r=${r:-999999}
  b=$(free_mb /boot); b=${b:-999999}

  if (( r < MIN_FREE_ROOT_MB )); then
    step_soft "Pakketcache legen (weinig ruimte op /)" 10m apt-get clean
    r=$(free_mb /); r=${r:-999999}
  fi
  if (( b < MIN_FREE_BOOT_MB )); then
    step_soft "Oude kernels opruimen (weinig ruimte op /boot)" 30m \
      apt-get "${APT_OPTS[@]}" autoremove --purge
    b=$(free_mb /boot); b=${b:-999999}
  fi

  if (( r < MIN_FREE_ROOT_MB )); then
    ERRORS+=("Te weinig ruimte op /: ${r}MB vrij (minimaal ${MIN_FREE_ROOT_MB}MB). Update overgeslagen.")
    fail=1
  fi
  if (( b < MIN_FREE_BOOT_MB )); then
    ERRORS+=("Te weinig ruimte op /boot: ${b}MB vrij (minimaal ${MIN_FREE_BOOT_MB}MB). Update overgeslagen.")
    fail=1
  fi
  return $fail
}

on_exit() {
  local rc=$?
  if (( ! FINISHED )); then
    TG_TRIES=3 tg_send "❌ <b>$(esc "$HOST")</b>
${AGENT_NAME} is onverwacht gestopt (exit ${rc}). Geen reboot uitgevoerd.

<b>Laatste logregels:</b>
<pre>$(esc "$(tail -n 15 "$LOG" 2>/dev/null)")</pre>"
  fi
}

# ---------------------------------------------------------------------------
# Update-flow
# ---------------------------------------------------------------------------
do_update() {
  local i n ok=0 audit l sim before after
  local left=() held=()

  check_space || return 1

  step "Onderbroken installaties herstellen" 30m \
    dpkg --force-confdef --force-confold --configure -a
  step "Afhankelijkheden herstellen" 30m \
    apt-get "${APT_OPTS[@]}" -f install

  n=${#ERRORS[@]}
  for i in 1 2 3; do
    if step "Pakketlijsten ophalen (poging $i/3)" 15m apt-get "${APT_OPTS[@]}" update; then
      ERRORS=("${ERRORS[@]:0:n}")
      ok=1
      break
    fi
    if (( i < 3 )); then log "Opnieuw proberen over 60s"; sleep 60; fi
  done
  (( ok )) || return 1

  sim=$(simulate)
  mapfile -t PKGS < <(awk 'NF{print $2}' <<<"$sim")
  SEC_COUNT=$(grep -c -- '-security' <<<"$sim")
  log "Te updaten: ${#PKGS[@]} pakket(ten), waarvan ${SEC_COUNT} security"

  if (( ${#PKGS[@]} > 0 )); then
    step "Pakketten upgraden (${UPG_ARGS[*]})" 2h \
      apt-get "${APT_OPTS[@]}" "${UPG_ARGS[@]}" || return 1
  fi

  if [[ $SNAP_REFRESH == yes ]] && has_snaps; then
    before=$(snap_revs)
    step_soft "Snaps bijwerken" 30m snap refresh
    after=$(snap_revs)
    SNAP_CHANGED=$(comm -13 <(echo "$before") <(echo "$after") | awk '{print $1}' | paste -sd ' ' -)
  fi

  step_soft "Overbodige pakketten opruimen" 30m apt-get "${APT_OPTS[@]}" autoremove --purge
  step_soft "Pakketcache opschonen" 10m apt-get "${APT_OPTS[@]}" autoclean

  audit=$(dpkg --audit 2>&1)
  if [[ -n $audit ]]; then
    ERRORS+=("dpkg meldt half geinstalleerde pakketten:")
    while IFS= read -r l; do
      [[ -n $l ]] && ERRORS+=("  $l")
    done < <(head -n 5 <<<"$audit")
  fi

  mapfile -t left < <(simulate | awk '{print $2}')
  (( ${#left[@]} )) && INFO+=("Tegengehouden (kept back): ${left[*]}")
  mapfile -t held < <(apt-mark showhold 2>/dev/null)
  (( ${#held[@]} )) && INFO+=("Vastgezet (hold): ${held[*]}")

  eval_reboot
  case $REBOOT in
    always) REBOOT_PLAN=1 ;;
    smart)  (( ${#REASONS[@]} )) && REBOOT_PLAN=1 ;;
  esac
  if (( ! REBOOT_PLAN )) && [[ $RESTART_SERVICES == yes ]]; then
    restart_services
  fi
  return 0
}

finish() {
  local no_reboot=$1 do_reboot=$REBOOT_PLAN reasons reboot_txt
  local icon status result msg list kern_new
  local warns=()

  reasons=$(join_by "; " "${REASONS[@]}")

  if (( no_reboot )); then
    do_reboot=0
    reboot_txt="overgeslagen (--no-reboot)${reasons:+, wel nodig: $reasons}"
  elif (( ${#ERRORS[@]} )) && [[ $REBOOT_ON_ERROR != yes ]]; then
    do_reboot=0
    reboot_txt="NIET uitgevoerd vanwege fouten${reasons:+ (wel nodig: $reasons)}"
  elif (( do_reboot )); then
    if [[ $REBOOT == always ]]; then
      reboot_txt="over ${REBOOT_DELAY_MIN} min (altijd ingesteld)"
    else
      reboot_txt="over ${REBOOT_DELAY_MIN} min: ${reasons}"
    fi
  elif [[ $REBOOT == never ]]; then
    reboot_txt="uitgeschakeld${reasons:+, wel nodig: $reasons}"
  else
    reboot_txt="niet nodig"
  fi

  if (( ${#WARNINGS[@]} )); then
    mapfile -t warns < <(printf '%s\n' "${WARNINGS[@]}" | awk '!seen[$0]++' | head -n 10)
  fi

  if (( ${#ERRORS[@]} )); then
    icon="❌"; status="Update MISLUKT."; result="fout"
  elif (( ${#warns[@]} )); then
    icon="⚠️"; status="Updates uitgevoerd, met waarschuwingen."; result="waarschuwing"
  elif (( ${#PKGS[@]} == 0 )) && [[ -z $SNAP_CHANGED ]]; then
    icon="✅"; status="Geen updates nodig, systeem is up-to-date."; result="geen updates"
  else
    icon="✅"; status="Is zojuist geüpdatet. Alles ging goed."; result="ok"
  fi

  msg="${icon} <b>$(esc "$HOST")</b>"$'\n'"$(esc "$status")"$'\n'
  [[ -n $CUSTOMER ]] && msg+=$'\n'"<b>Klant:</b> $(esc "$CUSTOMER")"
  msg+=$'\n'"<b>Systeem:</b> $(esc "$(os_name)")"
  kern_new=$(latest_kernel)
  if [[ -n $kern_new && $kern_new != "$KERNEL_BEFORE" ]]; then
    msg+=$'\n'"<b>Kernel:</b> $(esc "$KERNEL_BEFORE") → $(esc "$kern_new")"
  else
    msg+=$'\n'"<b>Kernel:</b> $(esc "$KERNEL_BEFORE")"
  fi
  msg+=$'\n'"<b>Pakketten:</b> ${#PKGS[@]} (${SEC_COUNT} security)"
  if (( ${#PKGS[@]} )); then
    list=$(join_by ", " "${PKGS[@]:0:40}")
    (( ${#PKGS[@]} > 40 )) && list+=" (+$(( ${#PKGS[@]} - 40 )) meer)"
    msg+=$'\n'"<code>$(esc "$list")</code>"
  fi
  [[ -n $SNAP_CHANGED ]] && msg+=$'\n'"<b>Snaps:</b> $(esc "$SNAP_CHANGED")"
  (( ${#RESTARTED[@]} )) && msg+=$'\n'"<b>Services herstart:</b> $(esc "${RESTARTED[*]}")"
  msg+=$'\n'"<b>Reboot:</b> $(esc "$reboot_txt")"
  msg+=$'\n'"<b>Vrij:</b> / $(disk_free /), /boot $(disk_free /boot)"
  msg+=$'\n'"<b>Duur:</b> $(fmt_dur $(( SECONDS - START )))"

  if (( ${#ERRORS[@]} )); then
    msg+=$'\n\n'"<b>Fouten:</b>"$'\n'"<pre>$(esc "$(printf '%s\n' "${ERRORS[@]}" | head -n 25)")</pre>"
  fi
  if (( ${#warns[@]} )); then
    msg+=$'\n\n'"<b>Waarschuwingen:</b>"$'\n'"<pre>$(esc "$(printf '%s\n' "${warns[@]}")")</pre>"
  fi
  if (( ${#INFO[@]} )); then
    msg+=$'\n\n'"<b>Info:</b>"$'\n'"$(esc "$(printf '%s\n' "${INFO[@]}")")"
  fi
  msg+=$'\n\n'"<i>${AGENT_NAME} v${AGENT_VERSION} · $(esc "$LOG")</i>"

  printf '%s|%s|%s|%s\n' "$(date '+%F %T')" "$result" "${#PKGS[@]}" "$LOG" >"$STATE_DIR/last-run"
  FINISHED=1

  if [[ $result == "geen updates" && $NOTIFY_NO_UPDATES != yes ]] && (( ! do_reboot )); then
    log "Geen updates, melding overgeslagen (NOTIFY_NO_UPDATES=no)"
  else
    tg_send "$msg" || log "Telegram-melding kon niet verstuurd worden"
  fi

  if (( do_reboot )); then
    echo "$(date +%s) $KERNEL_BEFORE" >"$STATE_DIR/pending-reboot"
    log "Reboot gepland over ${REBOOT_DELAY_MIN} min ($reboot_txt)"
    if (( REBOOT_DELAY_MIN > 0 )); then
      shutdown -r +"$REBOOT_DELAY_MIN" "${AGENT_NAME}: reboot na updates"
    else
      shutdown -r now "${AGENT_NAME}: reboot na updates"
    fi || { log "shutdown mislukt, probeer systemctl reboot"; systemctl reboot; }
  fi
}

# ---------------------------------------------------------------------------
# Commando's
# ---------------------------------------------------------------------------
cmd_run() {
  local no_reboot=0
  [[ ${1:-} == --no-reboot ]] && no_reboot=1
  require_root
  mkdir -p "$LOG_DIR" "$STATE_DIR"
  chmod 700 "$LOG_DIR"

  exec 9>"$LOCK_FILE"
  flock -n 9 || { echo "${AGENT_NAME} draait al, stoppen."; exit 0; }

  LOG="$LOG_DIR/$(date +%F_%H%M%S).log"
  : >"$LOG"
  START=$SECONDS
  trap on_exit EXIT
  trap 'log "Signaal ontvangen, stoppen"; exit 143' TERM INT HUP

  log "${AGENT_NAME} v${AGENT_VERSION} gestart op ${HOST} (modus: ${UPG_ARGS[*]}, reboot: ${REBOOT})"
  if ! do_update; then
    log "Update afgebroken"
    (( ${#ERRORS[@]} )) || ERRORS+=("Update afgebroken, zie log")
    eval_reboot
  fi

  find "$LOG_DIR" -type f -name '*.log' ! -name postboot.log -mtime +"$KEEP_LOG_DAYS" -delete 2>/dev/null
  finish "$no_reboot"
}

cmd_post_boot() {
  local ts=0 old_k="" new_k latest failed msg icon="✅" s url code i ok
  local bad_svc=() bad_url=() notes=()
  [[ -f $STATE_DIR/pending-reboot ]] || exit 0
  read -r ts old_k <"$STATE_DIR/pending-reboot"
  rm -f "$STATE_DIR/pending-reboot"
  LOG="$LOG_DIR/postboot.log"
  log "Post-boot controle gestart"
  sleep 30

  # Gemonitorde services: max 5 minuten wachten tot ze actief zijn
  for (( i = 0; i < 20; i++ )); do
    bad_svc=()
    for s in $WATCH_SERVICES; do
      systemctl is-active --quiet "$s" || bad_svc+=("$s")
    done
    (( ${#bad_svc[@]} == 0 )) && break
    sleep 15
  done

  # Health checks: elke HTTP-response onder 500 telt als "draait"
  for url in $HEALTHCHECK_URLS; do
    ok=0
    for (( i = 0; i < 20; i++ )); do
      code=$(curl -k -s -o /dev/null -w '%{http_code}' --noproxy '*' --max-time 10 "$url")
      if [[ $code =~ ^[1-4][0-9][0-9]$ ]]; then ok=1; break; fi
      sleep 15
    done
    (( ok )) || bad_url+=("$url (HTTP $code)")
  done

  failed=$(systemctl --failed --no-legend --plain 2>/dev/null | awk '{print $1}' | paste -sd ' ' -)
  new_k=$(uname -r)
  latest=$(latest_kernel)

  (( ${#bad_svc[@]} )) && { icon="⚠️"; notes+=("Niet actief: ${bad_svc[*]}"); }
  (( ${#bad_url[@]} )) && { icon="⚠️"; notes+=("Health check mislukt: ${bad_url[*]}"); }
  [[ -n $failed ]] && { icon="⚠️"; notes+=("Mislukte units: $failed"); }
  if [[ -n $latest && $latest != "$new_k" ]]; then
    icon="⚠️"; notes+=("Draait niet op de nieuwste kernel ($latest), controleer GRUB")
  fi

  msg="${icon} <b>$(esc "$HOST")</b> is weer online"
  (( ts > 0 )) && msg+=" na $(fmt_dur $(( $(date +%s) - ts )))"
  msg+=$'\n'
  [[ -n $CUSTOMER ]] && msg+=$'\n'"<b>Klant:</b> $(esc "$CUSTOMER")"
  if [[ -n $old_k && $old_k != "$new_k" ]]; then
    msg+=$'\n'"<b>Kernel:</b> $(esc "$old_k") → $(esc "$new_k")"
  else
    msg+=$'\n'"<b>Kernel:</b> $(esc "$new_k")"
  fi
  [[ -n $WATCH_SERVICES ]] && (( ${#bad_svc[@]} == 0 )) && msg+=$'\n'"<b>Services:</b> $(esc "$WATCH_SERVICES") actief"
  [[ -n $HEALTHCHECK_URLS ]] && (( ${#bad_url[@]} == 0 )) && msg+=$'\n'"<b>Health checks:</b> OK"
  if (( ${#notes[@]} )); then
    msg+=$'\n\n'"<b>Aandacht nodig:</b>"$'\n'"$(esc "$(printf '%s\n' "${notes[@]}")")"
  else
    msg+=$'\n'"Alles draait."
  fi
  msg+=$'\n\n'"<i>${AGENT_NAME} v${AGENT_VERSION}</i>"
  tg_send "$msg"
}

cmd_check() {
  local sim sec r svc held
  local pk=()
  require_root
  echo "Pakketlijsten ophalen..."
  apt-get "${APT_OPTS[@]}" update </dev/null >/dev/null 2>&1 || echo "Let op: apt-get update gaf een fout"
  sim=$(simulate)
  mapfile -t pk < <(awk 'NF{print $2}' <<<"$sim")
  sec=$(grep -c -- '-security' <<<"$sim")
  echo
  echo "Host:          $HOST ($(os_name))"
  echo "Kernel:        $(uname -r) (nieuwste geinstalleerd: $(latest_kernel))"
  echo "Updates:       ${#pk[@]} (${sec} security)"
  (( ${#pk[@]} )) && printf '               %s\n' "${pk[@]}"
  eval_reboot
  r=$(join_by "; " "${REASONS[@]}")
  echo "Reboot nu nodig: ${r:-nee}"
  svc=$(outdated_services | paste -sd ' ' -)
  echo "Services met oude libraries: ${svc:-geen}"
  held=$(apt-mark showhold 2>/dev/null | paste -sd ' ' -)
  echo "Vastgezet (hold): ${held:-geen}"
  echo
  systemctl list-timers "$UNIT.timer" --no-pager 2>/dev/null
}

cmd_test() {
  local n
  require_root
  n=$(simulate | wc -l)
  if TG_TRIES=1 tg_send "🧪 <b>$(esc "$HOST")</b>
Testbericht van de ${AGENT_NAME}. Telegram werkt.
${CUSTOMER:+<b>Klant:</b> $(esc "$CUSTOMER")
}<b>Updates klaar:</b> ${n} (volgens de huidige pakketlijst)
<b>Schema:</b> $(esc "$SCHEDULE")

<i>${AGENT_NAME} v${AGENT_VERSION}</i>"; then
    echo "OK: testbericht verstuurd."
  else
    echo "FOUT: bericht niet verstuurd. 401 = verkeerde token, 400 = verkeerde chat id of thread id."
    exit 1
  fi
}

cmd_status() {
  local when result count logf
  echo "${AGENT_NAME} v${AGENT_VERSION} op ${HOST}"
  echo
  systemctl list-timers "$UNIT.timer" --no-pager
  echo
  if [[ -f $STATE_DIR/last-run ]]; then
    IFS='|' read -r when result count logf <"$STATE_DIR/last-run"
    echo "Laatste run: $when, resultaat: $result, pakketten: $count"
    echo "Log: $logf"
    echo
    tail -n 20 "$logf" 2>/dev/null
  else
    echo "Nog niet gedraaid."
  fi
}

default_conf() {
  cat <<'EOF'
# ===========================================================================
#  Solvento Linux Agent - configuratie
#  Na wijzigen van SCHEDULE of RANDOM_DELAY_MIN: sudo solvento-agent install
# ===========================================================================

# --- Telegram ---------------------------------------------------------------
TELEGRAM_BOT_TOKEN=""
TELEGRAM_CHAT_ID=""
# Alleen invullen bij een groep met Topics (forum), anders leeg laten
TELEGRAM_THREAD_ID=""

# --- Identificatie ----------------------------------------------------------
# Klantnaam in de rapportage (leeg = niet tonen)
CUSTOMER=""
# Naam in Telegram (leeg = hostname -f)
HOST_LABEL=""

# --- Planning ---------------------------------------------------------------
# systemd OnCalendar formaat, tijd in de tijdzone van de server
SCHEDULE="Sun *-*-* 20:00:00"
# Willekeurige vertraging in minuten (spreidt VM's met hetzelfde schema)
RANDOM_DELAY_MIN=0

# --- Updates ----------------------------------------------------------------
# upgrade      = zoals 'apt upgrade', verwijdert nooit pakketten (aanbevolen)
# full-upgrade = mag pakketten verwijderen om upgrades door te voeren
UPGRADE_MODE="upgrade"
SNAP_REFRESH="yes"

# --- Reboot -----------------------------------------------------------------
# smart  = alleen als het nodig is: nieuwe kernel, reboot-required, of uptime te lang
# always = na elke run rebooten
# never  = nooit (wel melden als het nodig is)
REBOOT="smart"
# smart: forceer reboot als de VM langer dan zoveel dagen draait (0 = uit)
MAX_UPTIME_DAYS=60
# Services met verouderde libraries herstarten als er geen reboot komt
RESTART_SERVICES="yes"
# yes = ook rebooten als er fouten waren (afgeraden)
REBOOT_ON_ERROR="no"
REBOOT_DELAY_MIN=1

# --- Controle na reboot -----------------------------------------------------
# Services die na de reboot actief moeten zijn, spatie-gescheiden
# Voorbeeld UniFi: WATCH_SERVICES="unifi mongod"
WATCH_SERVICES=""
# URL's die na de reboot moeten antwoorden (elke HTTP-code onder 500 = OK)
# Voorbeeld UniFi: HEALTHCHECK_URLS="https://127.0.0.1:8443"
HEALTHCHECK_URLS=""

# --- Overig -----------------------------------------------------------------
# Ook een melding sturen als er niets te updaten was
NOTIFY_NO_UPDATES="yes"
MIN_FREE_ROOT_MB=1000
MIN_FREE_BOOT_MB=150
KEEP_LOG_DAYS=90
EOF
}

cmd_install() {
  local src
  require_root

  if ! command -v curl >/dev/null; then
    apt-get "${APT_OPTS[@]}" install curl </dev/null
  fi

  src=$(readlink -f "${BASH_SOURCE[0]}")
  [[ $src != "$BIN" ]] && install -m 0755 "$src" "$BIN"
  mkdir -p "$CONF_DIR" "$LOG_DIR" "$STATE_DIR"
  chmod 700 "$LOG_DIR"

  if [[ ! -f $CONF ]]; then
    if [[ -f $SCRIPT_DIR/agent.conf ]]; then
      install -m 0600 "$SCRIPT_DIR/agent.conf" "$CONF"
      echo "Config overgenomen uit $SCRIPT_DIR/agent.conf"
    else
      default_conf >"$CONF"
      echo "Standaard config aangemaakt: $CONF (token en chat id nog invullen)"
    fi
  else
    echo "Bestaande config behouden: $CONF"
  fi
  chmod 600 "$CONF"
  # shellcheck disable=SC1090
  . "$CONF"
  set_derived

  if ! systemd-analyze calendar "$SCHEDULE" >/dev/null 2>&1; then
    echo "FOUT: SCHEDULE \"$SCHEDULE\" is geen geldig OnCalendar-formaat."
    exit 1
  fi

  cat >"$UNIT_DIR/$UNIT.service" <<EOF
[Unit]
Description=${AGENT_NAME}
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
ExecStart=$BIN run
StandardInput=null
TimeoutStartSec=4h
EOF

  cat >"$UNIT_DIR/$UNIT.timer" <<EOF
[Unit]
Description=${AGENT_NAME} schema

[Timer]
OnCalendar=${SCHEDULE}
RandomizedDelaySec=${RANDOM_DELAY_MIN}min
AccuracySec=1min
Persistent=false

[Install]
WantedBy=timers.target
EOF

  cat >"$UNIT_DIR/$UNIT-postboot.service" <<EOF
[Unit]
Description=${AGENT_NAME} controle na reboot
After=network-online.target
Wants=network-online.target
ConditionPathExists=$STATE_DIR/pending-reboot

[Service]
Type=oneshot
ExecStart=$BIN post-boot
StandardInput=null
TimeoutStartSec=20min

[Install]
WantedBy=multi-user.target
EOF

  systemctl daemon-reload
  systemctl enable "$UNIT.timer" >/dev/null 2>&1
  systemctl restart "$UNIT.timer"
  systemctl enable "$UNIT-postboot.service" >/dev/null 2>&1

  echo
  echo "${AGENT_NAME} v${AGENT_VERSION} geinstalleerd op ${HOST}"
  echo "Tijdzone: $(timedatectl show -p Timezone --value 2>/dev/null)   Schema: ${SCHEDULE}"
  systemctl list-timers "$UNIT.timer" --no-pager
  echo
  echo "Test nu Telegram met: sudo solvento-agent test"
}

cmd_uninstall() {
  require_root
  systemctl disable --now "$UNIT.timer" 2>/dev/null
  systemctl disable "$UNIT-postboot.service" 2>/dev/null
  rm -f "$UNIT_DIR/$UNIT.service" "$UNIT_DIR/$UNIT.timer" "$UNIT_DIR/$UNIT-postboot.service" "$BIN"
  systemctl daemon-reload
  echo "Verwijderd. Config ($CONF) en logs ($LOG_DIR) zijn bewaard."
}

usage() {
  sed -n '2,17p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
}

case ${1:-} in
  install)   cmd_install ;;
  run)       shift; cmd_run "$@" ;;
  check)     cmd_check ;;
  post-boot) cmd_post_boot ;;
  test)      cmd_test ;;
  status)    cmd_status ;;
  uninstall) cmd_uninstall ;;
  version)   echo "${AGENT_NAME} v${AGENT_VERSION}" ;;
  default-conf) default_conf ;;
  *)         usage; exit 1 ;;
esac
SOLVENTO_AGENT_EOF
  bash -n "$tmp" || { rm -f "$tmp"; fail "Agent-script is beschadigd, installatie afgebroken."; }
  install -m 0755 "$tmp" "$BIN"
  rm -f "$tmp"
  ok "Agent geinstalleerd: ${BIN}"
}

# set_kv <bestand> <sleutel> <waarde>  (vervangt of voegt toe)
set_kv() {
  local f=$1
  K=$2 V=$3 awk '
    BEGIN { k = ENVIRON["K"]; v = ENVIRON["V"]; done = 0 }
    index($0, k "=") == 1 { print k "=\"" v "\""; done = 1; next }
    { print }
    END { if (!done) print k "=\"" v "\"" }
  ' "$f" >"$f.tmp" && mv "$f.tmp" "$f"
}

write_conf() {
  local tmp key
  mkdir -p "$CONF_DIR"
  tmp=$(mktemp)
  if [[ -f $CONF ]]; then
    cp "$CONF" "$tmp"
    cp "$CONF" "$CONF.bak"
  else
    "$BIN" default-conf >"$tmp"
  fi
  for key in TELEGRAM_BOT_TOKEN TELEGRAM_CHAT_ID TELEGRAM_THREAD_ID CUSTOMER HOST_LABEL \
             SCHEDULE RANDOM_DELAY_MIN REBOOT WATCH_SERVICES HEALTHCHECK_URLS; do
    set_kv "$tmp" "$key" "${!key}"
  done
  install -m 0600 "$tmp" "$CONF"
  rm -f "$tmp"
  ok "Configuratie opgeslagen: ${CONF}"
}

migrate_old() {
  # Eerdere testversie (solvento-update) opruimen
  if [[ -f /etc/systemd/system/solvento-update.timer || -f /usr/local/sbin/solvento-update ]]; then
    systemctl disable --now solvento-update.timer >/dev/null 2>&1
    systemctl disable solvento-update-postboot.service >/dev/null 2>&1
    rm -f /etc/systemd/system/solvento-update.{service,timer} \
          /etc/systemd/system/solvento-update-postboot.service /usr/local/sbin/solvento-update
    systemctl daemon-reload
    ok "Oude Solvento Update Agent verwijderd"
  fi
}

do_install() {
  local out next
  section "Installeren"
  migrate_old
  write_agent
  [[ $MODE != upgrade ]] && write_conf
  out=$(mktemp)
  if "$BIN" install >"$out" 2>&1; then
    ok "Planning actief (systemd timer ${UNIT}.timer)"
  else
    cat "$out"
    rm -f "$out"
    fail "Activeren van de planning is mislukt."
  fi
  rm -f "$out"

  next=$(systemctl show "${UNIT}.timer" -p NextElapseUSecRealtime --value 2>/dev/null)
  [[ -n $next && $next != "n/a" ]] && ok "Eerstvolgende run: ${next}"

  if (( ! NO_TEST )); then
    if ask_yn "Testbericht naar Telegram sturen?" j; then
      if "$BIN" test >/dev/null 2>&1; then
        ok "Testbericht verstuurd, kijk in je Telegram-groep"
      else
        warn "Testbericht mislukt. Controleer token en chat ID in ${CONF}"
      fi
    fi
  fi
}

do_uninstall() {
  section "Verwijderen"
  if [[ -x $BIN ]]; then
    "$BIN" uninstall >/dev/null 2>&1
    ok "Agent en planning verwijderd"
  else
    warn "Agent was niet geinstalleerd"
  fi
  if ask_yn "Ook configuratie en logs verwijderen?" n; then
    rm -rf "$CONF_DIR/agent.conf" "$CONF_DIR/agent.conf.bak" /var/log/solvento-agent /var/lib/solvento-agent
    rmdir "$CONF_DIR" 2>/dev/null
    ok "Configuratie en logs verwijderd"
  fi
  printf '\n'
  exit 0
}

done_screen() {
  printf '\n%s%s  ✔ %s v%s is actief op %s%s\n\n' "$G" "$B" "$AGENT_NAME" "$AGENT_VERSION" "$HOST_LABEL" "$R"
  printf "  %sHandige commando's:%s\n" "$B" "$R"
  printf '    sudo solvento-agent status            %splanning en laatste run%s\n' "$D" "$R"
  printf '    sudo solvento-agent check             %swat zou er nu gebeuren%s\n' "$D" "$R"
  printf '    sudo solvento-agent run               %snu updaten%s\n' "$D" "$R"
  printf '    sudo solvento-agent test              %sTelegram testbericht%s\n' "$D" "$R"
  printf '    sudo nano %s   %sinstellingen aanpassen%s\n' "$CONF" "$D" "$R"
  printf '\n  %sInstaller opnieuw draaien = agent bijwerken of herconfigureren.%s\n\n' "$D" "$R"
}

# ---------------------------------------------------------------------------
main() {
  parse_args "$@"
  # Verwijderen zonder terminal: geen vragen, config en logs blijven staan
  if (( DO_UNINSTALL )) && ! { : </dev/tty; } 2>/dev/null; then UNATTENDED=1; fi
  banner
  preflight
  if (( DO_UNINSTALL )); then
    do_uninstall
  fi
  check_timezone
  choose_mode
  if [[ $MODE != upgrade ]]; then
    configure
    summary
  fi
  HOST_LABEL=${HOST_LABEL:-$(hostname -f 2>/dev/null || hostname)}
  do_install
  done_screen
}

main "$@"