#!/usr/bin/env bash
# c4rgo-mqtt-status.sh — cruscotto rapido del canale MQTT scanner <-> cloud.
# Un solo comando, due lati (rileva da solo dove gira):
#   sul CLOUD/broker : elenca gli scanner e chi e' vivo adesso, piu' i check del broker
#   su uno SCANNER   : verifica che il bridge sia su e configurato bene
#
# Uso:
#   ./c4rgo-mqtt-status.sh                 # elenco scanner (lato broker)  [default]
#   ./c4rgo-mqtt-status.sh scanners        # idem, esplicito
#   ./c4rgo-mqtt-status.sh doctor          # check di configurazione (auto broker/scanner)
#   ./c4rgo-mqtt-status.sh watch <device>  # tail live di tutti i topic di un device
#
# Opzioni: -h <host> -p <port> -u <user> -P <pass>
#
# Credenziali (servono per leggere c4rgo/# => utente r_server):
#   -P <pass>  oppure  env C4RGO_MQTT_PASS  oppure  auto da <repo>/.env.local
#   (SCANNER_MQTT_PASSWORD). Sul lato scanner non servono: usa i topic $SYS locali.
#
# NB: niente `set -e`. Questo e' uno strumento diagnostico che chiama comandi che
# falliscono di proposito (mosquitto_sub -W va in timeout con exit 27): con set -e
# ogni assegnazione `x="$(comando)"` che torna non-zero ucciderebbe lo script in
# silenzio, proprio dove dovremmo REPORTARE il problema. Teniamo solo set -u.
set -u

# ── stile ────────────────────────────────────────────────────────────────────
if [[ -t 1 ]]; then B=$'\e[1m'; R=$'\e[31m'; G=$'\e[32m'; Y=$'\e[33m'; D=$'\e[2m'; Z=$'\e[0m'
else B=''; R=''; G=''; Y=''; D=''; Z=''; fi
ok()   { printf '  %s✓%s %s\n'  "$G" "$Z" "$*"; }
warn() { printf '  %s!%s %s\n'  "$Y" "$Z" "$*"; }
bad()  { printf '  %s✗%s %s\n'  "$R" "$Z" "$*"; }
hdr()  { printf '\n%s%s%s\n' "$B" "$*" "$Z"; }

# ── config / default ─────────────────────────────────────────────────────────
HOST=127.0.0.1
USER=r_server
PASS="${C4RGO_MQTT_PASS:-}"
PORT=""             # se vuoto: auto dal listener locale del broker, altrimenti 1883
HERE="$(cd "$(dirname "$0")" && pwd)"
REPO_ROOT="$(cd "$HERE/../.." && pwd)"   # deploy/mqtt-baremetal -> radice c4rgocloud

CMD=""
while [[ $# -gt 0 ]]; do
  case "$1" in
    -h) HOST="$2"; shift 2;;
    -p) PORT="$2"; shift 2;;
    -u) USER="$2"; shift 2;;
    -P) PASS="$2"; shift 2;;
    scanners|doctor|watch) CMD="$1"; shift;;
    *) if [[ -z "$CMD" ]]; then CMD="$1"; else ARG="$1"; fi; shift;;
  esac
done
CMD="${CMD:-scanners}"

# porta del listener locale in chiaro (il broker la mette in c4rgo-scanner.conf)
if [[ -z "$PORT" ]]; then
  PORT="$(grep -hoshE '^\s*listener\s+[0-9]+\s+127\.0\.0\.1' /etc/mosquitto/conf.d/c4rgo-scanner.conf 2>/dev/null \
          | grep -oE '[0-9]+' | head -1 || true)"
  PORT="${PORT:-1883}"
fi

# password: -P/env, altrimenti da .env.local
if [[ -z "$PASS" && -f "$REPO_ROOT/.env.local" ]]; then
  PASS="$(grep -hE '^\s*SCANNER_MQTT_PASSWORD=' "$REPO_ROOT/.env.local" 2>/dev/null \
          | tail -1 | cut -d= -f2- | sed 's/^["'\'']//; s/["'\'']$//' || true)"
fi

# sub helper: non deve far cadere lo script sul timeout -W (exit 27)
msub() { mosquitto_sub -h "$HOST" -p "$PORT" -u "$USER" -P "$PASS" "$@" 2>/dev/null || true; }
# sub sui soli topic $SYS locali (nessuna credenziale r_server necessaria lato device)
msys() { mosquitto_sub -h "$HOST" -p "$PORT" "$@" 2>/dev/null || true; }

need_pass() {
  if [[ -z "$PASS" ]]; then
    bad "Password di $USER non trovata (usa -P, env C4RGO_MQTT_PASS, o .env.local)."
    exit 2
  fi
}

# ── ruolo: broker o scanner? ─────────────────────────────────────────────────
role() {
  if   [[ -f /etc/mosquitto/conf.d/c4rgo-cloud-bridge.conf ]]; then echo scanner
  elif [[ -f /etc/mosquitto/conf.d/c4rgo-scanner.conf     ]]; then echo broker
  else echo broker; fi
}

# ── comando: scanners ────────────────────────────────────────────────────────
cmd_scanners() {
  need_pass
  printf '%sScanner sul broker %s:%s%s\n' "$B" "$HOST" "$PORT" "$Z"

  # 1) inventario dei topic. Uso -v (portabile anche su mosquitto-clients vecchi;
  #    -F non c'e' ovunque) e tengo solo le righe che iniziano per c4rgo/: cosi' le
  #    righe di continuazione dei payload JSON multiriga non creano device fantasma.
  local topics; topics="$(msub -t 'c4rgo/+/#' -v -W 2 | awk '/^c4rgo\//{print $1}')"
  if [[ -z "$topics" ]]; then
    # sonda vera: pubblica un messaggio nullo (ACL di r_server permette write c4rgo/#).
    # Se la connessione/auth va, non stampa nulla => davvero nessun retained.
    # Altrimenti stampa l'errore reale (auth, connessione, opzione non supportata).
    local err; err="$(mosquitto_pub -h "$HOST" -p "$PORT" -u "$USER" -P "$PASS" -t 'c4rgo/_probe' -n 2>&1)"
    if [[ -n "$err" ]]; then
      bad "Connessione/credenziali: $err"
    else
      warn "Connessione ok, ma nessun topic retained sotto c4rgo/#: nessuno scanner"
      warn "ha ancora pubblicato (o i retained sono stati cancellati)."
    fi
    return
  fi
  local devices; devices="$(echo "$topics" | awk -F/ 'NF>=2{print $2}' | sort -u)"

  # 2) presenza: topic retained del bridge (1=online, 0=offline via will). Autorevole,
  #    istantaneo e indipendente dal link (ethernet/wifi/modem): niente finestra d'ascolto.
  local pres; pres="$(msub -t 'c4rgo/+/status/bridge' -v -W 2)"

  # 3) tabella
  printf '\n  %-22s %-8s %-5s %s\n' "DEVICE" "STATO" "#TOP" "SERVIZI (retained)"
  printf '  %-22s %-8s %-5s %s\n'   "----------------------" "--------" "-----" "------------------"
  local d
  while IFS= read -r d; do
    [[ -z "$d" ]] && continue
    local n svc col word p
    n="$(echo "$topics" | awk -F/ -v k="$d" '$2==k' | wc -l)"
    svc="$(echo "$topics" | awk -F/ -v k="$d" '$2==k && NF>=3{print $3}' | sort -u | paste -sd, -)"
    p="$(echo "$pres" | awk -v t="c4rgo/$d/status/bridge" '$1==t{print $2; exit}')"
    case "$p" in
      1) col="$G"; word=online;;
      0) col="$R"; word=OFFLINE;;
      *) col="$D"; word='?';;
    esac
    # padding sulla parola in chiaro, colore fuori dal campo (gli escape non contano)
    printf '  %-22s %s%-8s%s %-5s %s\n' "$d" "$col" "$word" "$Z" "$n" "$svc"
  done <<< "$devices"

  printf '\n%s  online/OFFLINE = presenza del bridge sul cloud (retained, will su disconnessione),\n' "$D"
  printf '  indipendente dal link.  "?" = device senza topic di presenza (legacy o pre-1.x).%s\n' "$Z"
}

# ── comando: watch ───────────────────────────────────────────────────────────
cmd_watch() {
  need_pass
  local dev="${ARG:-}"
  [[ -z "$dev" ]] && { bad "Uso: $0 watch <device_id>"; exit 2; }
  printf '%sTail live di c4rgo/%s/#  (Ctrl-C per uscire)%s\n' "$B" "$dev" "$Z"
  mosquitto_sub -h "$HOST" -p "$PORT" -u "$USER" -P "$PASS" -t "c4rgo/$dev/#" -v
}

# ── comando: doctor (broker) ─────────────────────────────────────────────────
doctor_broker() {
  hdr "Broker  ($HOST:$PORT + TLS 8883)"
  systemctl is-active --quiet mosquitto && ok "mosquitto attivo" || bad "mosquitto NON attivo (systemctl status mosquitto)"

  ss -lntH 2>/dev/null | grep -q ':8883 ' && ok "listener TLS 8883 su" \
    || bad "nessun listener su 8883: i device non possono collegarsi (firewall? cert?)"
  ss -lntH 2>/dev/null | grep -qE "127\.0\.0\.1:$PORT " && ok "listener locale $PORT su (per il worker)" \
    || warn "nessun listener 127.0.0.1:$PORT: il worker non si collega (SCANNER_MQTT_PORT?)"

  # cert: presenza, SAN, scadenza
  local crt; crt="$(ls /etc/mosquitto/certs/*.crt 2>/dev/null | grep -v '/ca\.crt$' | head -1 || true)"
  if [[ -n "$crt" ]]; then
    local san days
    san="$(openssl x509 -in "$crt" -noout -ext subjectAltName 2>/dev/null | tail -1 | sed 's/^ *//')"
    ok "cert broker: $(basename "$crt")  SAN: ${san:-?}"
    if openssl x509 -in "$crt" -noout -checkend 2592000 >/dev/null 2>&1; then
      days="$(openssl x509 -in "$crt" -noout -enddate 2>/dev/null | cut -d= -f2)"
      ok "cert valido (scade $days)"
    else
      warn "cert in scadenza entro 30 giorni: rigeneralo (install-broker-baremetal.sh)"
    fi
  else
    bad "nessun cert broker in /etc/mosquitto/certs/"
  fi

  # passwd + acl
  local pw=/etc/mosquitto/c4rgo-scanner.passwd
  if [[ -f "$pw" ]]; then
    ok "passwd presente: $(grep -c ':' "$pw" 2>/dev/null || echo 0) utenti (r_server + un device ciascuno)"
    [[ "$(stat -c '%a' "$pw" 2>/dev/null)" == 600 ]] && ok "permessi passwd 600" || warn "permessi passwd non 600 ($pw)"
  else
    bad "manca $pw: nessuno puo' autenticarsi"
  fi
  [[ -f /etc/mosquitto/c4rgo-scanner.acl ]] && ok "acl per-device presente" \
    || warn "manca l'acl: gli scanner potrebbero vedere i topic altrui"

  # auth di r_server + clienti connessi (bridge + worker)
  hdr "Connettivita'"
  need_pass
  local ver; ver="$(msys -u "$USER" -P "$PASS" -t '$SYS/broker/version' -C 1 -W 3)"
  [[ -n "$ver" ]] && ok "login r_server ok ($ver)" || bad "login r_server fallito: password sbagliata o ACL"
  local cc; cc="$(msys -u "$USER" -P "$PASS" -t '$SYS/broker/clients/connected' -C 1 -W 3)"
  [[ -n "$cc" ]] && ok "client connessi ora: $cc  (= bridge dei device + worker)"

  # trappola convivenza: un secondo listener senza acl_file
  local nlist; nlist="$(grep -rhcE '^\s*listener\s' /etc/mosquitto/conf.d/ 2>/dev/null | paste -sd+ - | bc 2>/dev/null || echo '?')"
  [[ "$nlist" =~ ^[0-9]+$ && "$nlist" -gt 2 ]] && \
    warn "$nlist listener in conf.d/: controlla che nessuno esponga i topic senza acl (vedi TEST-LAB.md)"
}

# ── comando: doctor (scanner) ────────────────────────────────────────────────
doctor_scanner() {
  local cf=/etc/mosquitto/conf.d/c4rgo-cloud-bridge.conf
  hdr "Bridge dello scanner"
  systemctl is-active --quiet mosquitto && ok "mosquitto locale attivo" || bad "mosquitto locale NON attivo"

  [[ -f "$cf" ]] && ok "bridge conf presente" || { bad "manca $cf"; return; }
  [[ "$(stat -c '%a' "$cf" 2>/dev/null)" == 600 ]] && ok "permessi 600 (contiene la password)" \
    || warn "permessi non 600 su $cf (contiene remote_password!)"

  # il conf e' 600 root: senza sudo non lo leggiamo (e NON vogliamo falsi allarmi)
  local addr=""
  if [[ -r "$cf" ]]; then
    addr="$(grep -hE '^\s*address\s' "$cf" | awk '{print $2}' | head -1)"
    ok "punta a: ${addr:-?}"
    local ins; ins="$(grep -hE '^\s*bridge_insecure\s' "$cf" | awk '{print $2}' | head -1)"
    [[ "$ins" == false ]] && ok "bridge_insecure false (verifica il cert del broker)" \
      || warn "bridge_insecure=${ins:-assente}: TLS NON verificato, mettilo a false"
    local ca; ca="$(grep -hE '^\s*bridge_cafile\s' "$cf" | awk '{print $2}' | head -1)"
    [[ -n "$ca" && -f "$ca" ]] && ok "ca file presente ($ca)" || bad "bridge_cafile mancante: ${ca:-?}"
    local ids; ids="$(grep -hE '^\s*topic\s' "$cf" | grep -oE 'c4rgo/[^/]+/' | sort -u | wc -l)"
    [[ "$ids" -le 1 ]] && ok "topic tutti sullo stesso namespace del device" \
      || warn "righe topic con device_id diversi: probabile copia-incolla"
  else
    warn "conf non leggibile senza sudo: rilancia con  sudo $0 doctor  per i dettagli"
  fi

  # stato del bridge: connessione TCP uscente verso il cloud (cred-free, a livello
  # socket) => piu' affidabile del topic $SYS locale, che qui richiede auth.
  hdr "Stato live"
  local dst; dst="${addr#tls://}"; dst="${dst%% *}"       # host:porta dall'address
  # $NF = il peer (ultimo campo), robusto a prescindere dalle colonne di ss
  local est
  if [[ -n "$dst" ]]; then
    est="$(ss -tnH state established 2>/dev/null | awk -v d="$dst" '$NF==d' || true)"
  else
    # senza address dal conf: qualsiasi conn uscente su :8883 non-loopback
    est="$(ss -tnH state established 2>/dev/null | awk '$NF ~ /:8883$/ && $NF !~ /^127\./' || true)"
  fi
  if [[ -n "$est" ]]; then
    ok "bridge CONNESSO al cloud (${dst:-:8883}) — connessione TCP stabilita"
  else
    bad "bridge NON connesso a ${dst:-:8883}: credenziali? indirizzo? firewall 8883?"
    warn "diagnosi:  journalctl -u mosquitto -n 20 --no-pager | grep -iE 'bridge|refus|auth'"
  fi
}

# ── dispatch ─────────────────────────────────────────────────────────────────
case "$CMD" in
  scanners) cmd_scanners;;
  watch)    cmd_watch;;
  doctor)   case "$(role)" in scanner) doctor_scanner;; *) doctor_broker;; esac;;
  *) bad "comando sconosciuto: $CMD"; grep -E '^#( |$)' "$0" | sed 's/^# \?//' | head -18; exit 2;;
esac
