#!/bin/sh
# streamchecker — instalador de agente (una línea).
#
#   Instalar (agente nuevo, con token del dashboard):
#     curl -fsSL https://stream-get.sentrica.cl/install.sh | sudo sh -s -- --enroll <TOKEN> [opciones]
#   Actualizar el binario de un agente YA enrolado (sin crear una sonda nueva ni
#   re-pasar el token — reusa el token/licencia/puerto/nombre existentes):
#     curl -fsSL https://stream-get.sentrica.cl/install.sh | sudo sh -s -- --update
#
# El token lo da el central al "Agregar agente" (dashboard). La URL del central
# va horneada abajo (CENTRAL) para el SaaS de Sentrica; en on-prem se pasa con
# --central. El script:
#   - detecta SO (Linux/macOS) y arquitectura (amd64/arm64) y baja el binario,
#   - crea estado + secretos y instala el servicio que reporta al central
#     (dial-home saliente): systemd en Linux, launchd (LaunchDaemon) en macOS,
#   - la vista en vivo del dashboard es automática con ffmpeg (streamchecker la
#     sirve con su propio ffmpeg; no hay sidecar go2rtc),
#   - es idempotente: re-correrlo (o --update) actualiza el binario y conserva
#     el enrolamiento, el puerto del dashboard y el nombre en la flota.
#
# Los secretos (token, clave del dashboard) viajan por el ENTORNO del servicio,
# no por la línea de comandos — así no quedan visibles en `ps`.
#
# Opciones:
#   --enroll TOKEN   token del agente (requerido en la primera instalación)
#   --name NAME      nombre en la flota (default: hostname)
#   --central URL    central al que reportar (default: el SaaS de abajo)
#   --live           (deprecado, no-op: la vista en vivo es automática con ffmpeg)
#   --no-web         sin dashboard local (por defecto sí, en :8080)
#   --web-port N     puerto del dashboard local (default 8080)
#   --metrics-port N puerto de métricas Prometheus (default 9090; "off" desactiva)
#   --hwenc MODE     aceleración HW del transcode de la vista en vivo (default auto):
#                    auto autodetecta y CAE A SOFTWARE si no hay HW/permiso; off
#                    fuerza software; o un encoder puntual (rkmpp|vaapi|v4l2m2m|
#                    nvenc|qsv|amf|videotoolbox). Nunca rompe la vista
#   --streams "..."  streams a monitorear al arranque (URLs separadas por espacio)
#   --version V      versión a instalar (default: latest)
#   --update         actualiza un agente ya enrolado (reusa token/puerto/nombre;
#                    falla si no hay agente instalado)
#   --uninstall      quita el servicio y el binario (conserva estado)
set -eu

# ---- valores por defecto (AJUSTAR a tu infra) -------------------------------
CENTRAL="${SC_CENTRAL:-https://stream-hub.sentrica.cl}"      # central SaaS (override: --central o $SC_CENTRAL)
RELEASES="${SC_RELEASES:-https://stream-get.sentrica.cl/bin}" # binarios (override: $SC_RELEASES, ej. staging/on-prem)
CHANNEL="stable"
VERSION="latest"

log()  { printf '\033[1;34m›\033[0m %s\n' "$*"; }
warn() { printf '\033[1;33m!\033[0m %s\n' "$*" >&2; }
die()  { printf '\033[1;31m✗\033[0m %s\n' "$*" >&2; exit 1; }

# ---- estado de las opciones --------------------------------------------------
TOKEN=""; NAME=""; STREAMS=""
WEB=1; WEBPORT=""; METRICSPORT=9090; HWENC=""; UNINSTALL=0; UPDATE=0

# ---- parseo de argumentos ----------------------------------------------------
while [ $# -gt 0 ]; do
  case "$1" in
    --enroll)    TOKEN="${2:-}"; shift 2 ;;
    --enroll=*)  TOKEN="${1#*=}"; shift ;;
    --name)      NAME="${2:-}"; shift 2 ;;
    --name=*)    NAME="${1#*=}"; shift ;;
    --central)   CENTRAL="${2:-}"; shift 2 ;;
    --central=*) CENTRAL="${1#*=}"; shift ;;
    --version)   VERSION="${2:-}"; shift 2 ;;
    --version=*) VERSION="${1#*=}"; shift ;;
    --streams)   STREAMS="${2:-}"; shift 2 ;;
    --streams=*) STREAMS="${1#*=}"; shift ;;
    --web-port)  WEBPORT="${2:-}"; shift 2 ;;
    --web-port=*) WEBPORT="${1#*=}"; shift ;;
    --metrics-port)   METRICSPORT="${2:-}"; shift 2 ;;
    --metrics-port=*) METRICSPORT="${1#*=}"; shift ;;
    --hwenc)     HWENC="${2:-}"; shift 2 ;;
    --hwenc=*)   HWENC="${1#*=}"; shift ;;
    --live)      shift ;;   # deprecado, no-op: la vista en vivo es automática con ffmpeg
    --no-web)    WEB=0; shift ;;
    --update)    UPDATE=1; shift ;;
    --uninstall) UNINSTALL=1; shift ;;
    -h|--help)   sed -n '2,28p' "$0" 2>/dev/null || true; exit 0 ;;
    *) die "opción desconocida: $1 (usá --help)" ;;
  esac
done

# ---- SO + arquitectura -------------------------------------------------------
[ "$(id -u)" = 0 ] || die "corré como root: … | sudo sh -s -- --enroll <TOKEN>"
case "$(uname -s)" in
  Linux)  OS=linux ;;
  Darwin) OS=darwin ;;
  *) die "SO no soportado: $(uname -s) (Linux o macOS)" ;;
esac
case "$(uname -m)" in
  x86_64|amd64)  ARCH=amd64 ;;
  aarch64|arm64) ARCH=arm64 ;;
  *) die "arquitectura no soportada: $(uname -m) (amd64/arm64)" ;;
esac
if [ "$OS" = linux ]; then
  command -v systemctl >/dev/null 2>&1 || die "requiere systemd."
else
  command -v launchctl >/dev/null 2>&1 || die "requiere launchd (macOS)."
fi

# ---- rutas (por SO) ----------------------------------------------------------
BIN=/usr/local/bin/streamchecker
if [ "$OS" = linux ]; then
  SC_HOME=/var/lib/streamchecker
  ETC=/etc/streamchecker
  UNIT=/etc/systemd/system/streamchecker.service
  # go2rtc quedó atrás (la vista en vivo la sirve ffmpeg): se quita si sobró de
  # una instalación vieja (ver "cleanup legacy" en el uninstall).
  GO2RTC_UNIT=/etc/systemd/system/go2rtc.service
else
  SC_HOME=/usr/local/var/streamchecker
  ETC=/usr/local/etc/streamchecker
  LABEL=cl.sentrica.streamchecker
  GO2RTC_LABEL=cl.sentrica.go2rtc
  UNIT="/Library/LaunchDaemons/$LABEL.plist"
  GO2RTC_UNIT="/Library/LaunchDaemons/$GO2RTC_LABEL.plist"
fi
SC_USER=streamchecker
ENVF="$ETC/hub.env"

# ---- helpers -----------------------------------------------------------------
mktmp() { mktemp "${TMPDIR:-/tmp}/streamchecker.XXXXXX"; }

fetch() { # fetch URL OUT
  if command -v curl >/dev/null 2>&1; then curl -fSL "$1" -o "$2"
  elif command -v wget >/dev/null 2>&1; then wget -qO "$2" "$1"
  else die "necesito curl o wget."; fi
}

sha256() { # sha256 FILE -> imprime solo el hash (Linux o macOS)
  if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" | cut -d' ' -f1
  else shasum -a 256 "$1" | cut -d' ' -f1; fi
}

verify() { # verify FILE URL — valida checksum si el central publica .sha256
  local sums; sums=$(mktmp)
  if fetch "$2.sha256" "$sums" 2>/dev/null; then
    local want have
    want=$(cut -d' ' -f1 "$sums"); have=$(sha256 "$1")
    [ "$want" = "$have" ] || { rm -f "$sums"; die "checksum no coincide"; }
    log "checksum OK"
  else
    warn "sin checksum publicado; salteando verificación"
  fi
  rm -f "$sums"
}

# svc_flag: valor que sigue a un flag en el servicio YA instalado (systemd o
# plist), o vacío si no está. Se usa en un re-run/update para preservar el
# puerto/nombre del agente sin re-pasarlos (y no resetearlos a los defaults).
svc_flag() { # svc_flag FLAG   (ej: -web, -agent-name)
  [ -f "$UNIT" ] || return 0
  if [ "$OS" = linux ]; then
    # ExecStart en una sola línea: FLAG "valor con espacios" | FLAG valor
    sed -n "s/.* $1 \"\([^\"]*\)\".*/\1/p; s/.* $1 \([^ ]*\).*/\1/p" "$UNIT" | head -1
  else
    # plist: <string>FLAG</string> y en la línea siguiente <string>VALOR</string>
    grep -A1 -- ">$1</string>" "$UNIT" 2>/dev/null |
      sed -n 's@.*<string>\(.*\)</string>.*@\1@p' | tail -1
  fi
}

# ensure_user: solo en Linux. En macOS el LaunchDaemon corre como root (crear un
# usuario de sistema en macOS es dscl y queda para el hardening).
ensure_user() { # ensure_user USER HOME
  [ "$OS" = linux ] || return 0
  id "$1" >/dev/null 2>&1 && return 0
  if command -v useradd >/dev/null 2>&1; then
    useradd --system --home-dir "$2" --create-home --shell /usr/sbin/nologin "$1"
  elif command -v adduser >/dev/null 2>&1; then
    adduser -S -H -h "$2" "$1" >/dev/null 2>&1 || adduser --system --no-create-home --home "$2" "$1"
  else
    die "ni useradd ni adduser disponibles."
  fi
}

# svc_owner: dueño de los archivos de estado/servicio. Linux: usuario de sistema.
# macOS: root (0:0 = root:wheel).
svc_owner() { [ "$OS" = linux ] && printf '%s:%s' "$SC_USER" "$SC_USER" || printf '0:0'; }

gen_pass() { head -c 16 /dev/urandom | od -An -tx1 | tr -d ' \n'; }

xmlesc() { # escapa & < > para incrustar en el plist (URLs traen &, <, >)
  printf '%s' "$1" | sed -e 's/&/\&amp;/g' -e 's/</\&lt;/g' -e 's/>/\&gt;/g'
}

# ---- gestión del servicio (por SO) ------------------------------------------
svc_restart() { # svc_restart UNIT LABEL — (re)carga y arranca; idempotente
  if [ "$OS" = linux ]; then
    systemctl daemon-reload
    systemctl enable "$(basename "$1")" >/dev/null 2>&1 || true
    systemctl restart "$(basename "$1")"
  else
    launchctl bootout system "$1" >/dev/null 2>&1 || true
    launchctl bootstrap system "$1"
    launchctl enable "system/$2" >/dev/null 2>&1 || true
  fi
}

svc_stop() { # svc_stop UNIT LABEL
  if [ "$OS" = linux ]; then
    systemctl disable --now "$(basename "$1")" 2>/dev/null || true
  else
    launchctl bootout system "$1" >/dev/null 2>&1 || true
  fi
}

# ---- desinstalación ----------------------------------------------------------
if [ "$UNINSTALL" = 1 ]; then
  log "Desinstalando streamchecker…"
  # También quita el unit de go2rtc si sobró de una instalación vieja (cleanup
  # legacy: ya no se instala; la vista en vivo la sirve ffmpeg).
  [ "$OS" = linux ] && { svc_stop "$UNIT"; svc_stop "$GO2RTC_UNIT"; } \
                    || { svc_stop "$UNIT" "$LABEL"; svc_stop "$GO2RTC_UNIT" "$GO2RTC_LABEL"; }
  rm -f "$UNIT" "$GO2RTC_UNIT" "$BIN"
  [ "$OS" = linux ] && { systemctl daemon-reload 2>/dev/null || true; }
  warn "conservo el estado ($SC_HOME) y los secretos ($ETC); borralos a mano si querés."
  log "Listo."
  exit 0
fi

# ---- enrolamiento: token nuevo o reuso del previo (re-run = update) -----------
# Un re-run SIN --enroll reusa el token del env (no crea una sonda nueva ni
# consume otra licencia): es el update. --update lo hace explícito y falla si no
# hay un agente ya instalado.
mkdir -p "$ETC"; chmod 700 "$ETC"
OLD_TOKEN=""; OLD_AUTH=""
[ -f "$ENVF" ] && OLD_TOKEN=$(sed -n 's/^HUB_TOKEN=//p' "$ENVF")
[ -f "$ENVF" ] && OLD_AUTH=$(sed -n 's/^WEB_AUTH=//p' "$ENVF")
[ -z "$TOKEN" ] && TOKEN="$OLD_TOKEN"
if [ "$UPDATE" = 1 ] && [ -z "$OLD_TOKEN" ]; then
  die "--update: no hay un agente instalado acá. Para uno nuevo usá --enroll <TOKEN>."
fi
[ -n "$TOKEN" ] || die "falta --enroll <TOKEN> (lo da el dashboard al 'Agregar agente')."
[ -n "$OLD_TOKEN" ] && log "agente ya enrolado — actualizo binario/servicio (reuso token y licencia)."

# Preservar puerto/nombre del servicio existente si no vienen por flag: así un
# update no los resetea a 8080/hostname (bug que puede tumbar al agente).
# svc_flag puede devolver el flag entero (":8080") o con host ("0.0.0.0:8080",
# como en los units armados a mano de la flota): quedarnos SOLO con el puerto
# (lo que sigue al último ":") — si no, " -web :$WEBPORT" da ":0.0.0.0:8080"
# (doble ":") y el listener no bindea (medido en la .106 tras un --update).
if [ -z "$WEBPORT" ]; then
  WEBPORT=$(svc_flag -web); WEBPORT="${WEBPORT##*:}"
  [ -n "$WEBPORT" ] || WEBPORT=8080
fi
if [ -z "$NAME" ]; then
  NAME=$(svc_flag -agent-name); [ -n "$NAME" ] || NAME=$(svc_flag -probe-name)
  [ -n "$NAME" ] || NAME=$(hostname)
fi
# HW accel del transcode de la vista en vivo: default "auto" (el binario
# autodetecta y CAE A SOFTWARE si no hay HW/permiso — nunca rompe la vista). Se
# preserva a través del update como el puerto/nombre, así un --update no lo apaga
# ni pierde el encoder elegido (bug que ya pasó en la .106: el update revertía a
# software y encima cambiaba al usuario fuera del grupo del device).
if [ -z "$HWENC" ]; then
  HWENC=$(svc_flag -hwenc); [ -n "$HWENC" ] || HWENC=auto
fi

# ---- binario -----------------------------------------------------------------
base="$RELEASES/$CHANNEL"; [ "$VERSION" = latest ] || base="$base/$VERSION"
url="$base/streamchecker-$OS-$ARCH"
tmp=$(mktmp)
log "Bajando streamchecker ($OS/$ARCH, $CHANNEL/$VERSION)…"
fetch "$url" "$tmp" || die "no pude bajar $url"
verify "$tmp" "$url"
install -m 0755 "$tmp" "$BIN"; rm -f "$tmp"
[ "$OS" = darwin ] && xattr -dr com.apple.quarantine "$BIN" 2>/dev/null || true

# ---- usuario + estado --------------------------------------------------------
ensure_user "$SC_USER" "$SC_HOME"
# chown -R (no solo el dir): si un update cambia el User del servicio, los
# archivos de estado (streams.json/events.db) deben quedar del nuevo dueño o la
# persistencia falla en silencio (el runner no puede escribir su propio estado).
mkdir -p "$SC_HOME"; chown -R "$(svc_owner)" "$SC_HOME"

# ---- secretos + flags web ----------------------------------------------------
WEB_AUTH_VALUE=""; WEB_FLAGS=""; STREAM_ARGS=""
if [ "$WEB" = 1 ]; then
  if [ -n "$OLD_AUTH" ]; then WEB_AUTH_VALUE="$OLD_AUTH"; else WEB_AUTH_VALUE="admin:$(gen_pass)"; fi
  WEB_FLAGS=" -web :$WEBPORT"   # la clave (WEB_AUTH) va por entorno, no en argv
fi
METRICS_FLAGS=""
[ "$METRICSPORT" = off ] || METRICS_FLAGS=" -metrics 127.0.0.1:$METRICSPORT"
[ -n "$STREAMS" ] && STREAM_ARGS=" $STREAMS"

# HW accel: -hwenc va solo con la vista en vivo (que es del dashboard). Con -web
# activo lo emitimos siempre (default auto) — el binario decide y cae a software
# si el HW no está. Se preserva en el update por venir en el ExecStart (svc_flag).
HWENC_FLAG=""
[ "$WEB" = 1 ] && HWENC_FLAG=" -hwenc $HWENC"

# Grupos del device para el transcode por HW (solo Linux): el usuario del
# servicio necesita render (vaapi/qsv) y/o video (rkmpp/v4l2) para abrir el
# VPU/GPU; sin esto -hwenc auto cae a software AUNQUE haya HW. Solo se agregan los
# grupos que EXISTEN — un SupplementaryGroups con un grupo inexistente impide
# arrancar la unit (por eso se chequean, no se ponen a ciegas).
HWGROUPS=""; HWGROUPS_LINE=""
if [ "$OS" = linux ] && [ "$WEB" = 1 ] && [ "$HWENC" != off ]; then
  for g in render video; do
    if getent group "$g" >/dev/null 2>&1 || grep -q "^$g:" /etc/group 2>/dev/null; then
      HWGROUPS="${HWGROUPS:+$HWGROUPS }$g"
    fi
  done
  [ -n "$HWGROUPS" ] && HWGROUPS_LINE="SupplementaryGroups=$HWGROUPS"
fi

# La vista en vivo la sirve streamchecker con su propio ffmpeg (sin sidecar):
# se activa sola con -web + ffmpeg. Nada que instalar/cablear acá.

# ---- secretos fuera de argv: env file (Linux) / se copian al plist (macOS) ---
umask 077
{
  printf 'HUB_TOKEN=%s\n' "$TOKEN"
  [ -n "$WEB_AUTH_VALUE" ] && printf 'WEB_AUTH=%s\n' "$WEB_AUTH_VALUE"
} > "$ENVF"
chmod 600 "$ENVF"; chown 0:0 "$ENVF" 2>/dev/null || true

# ---- limpieza legacy: go2rtc ------------------------------------------------
# La vista en vivo la sirve el propio streamchecker (ffmpeg); si quedó un
# go2rtc de una instalación vieja, se detiene y se quita también en el update
# (antes solo lo hacía --uninstall y el sidecar huérfano seguía corriendo).
if [ -f "$GO2RTC_UNIT" ]; then
  log "quitando go2rtc legacy (la vista en vivo ya no lo usa)…"
  [ "$OS" = linux ] && svc_stop "$GO2RTC_UNIT" || svc_stop "$GO2RTC_UNIT" "$GO2RTC_LABEL"
  rm -f "$GO2RTC_UNIT"
  [ "$OS" = linux ] && { systemctl daemon-reload 2>/dev/null || true; }
fi

# ---- servicio del agente ----------------------------------------------------
if [ "$OS" = linux ]; then
  cat > "$UNIT" <<EOF
[Unit]
Description=streamchecker — agente ($NAME → $CENTRAL)
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=$SC_USER
${HWGROUPS_LINE}
EnvironmentFile=$ENVF
ExecStart=$BIN -o none$WEB_FLAGS$HWENC_FLAG -state $SC_HOME/streams.json -events $SC_HOME/events.db$METRICS_FLAGS -hub $CENTRAL -agent-name "$NAME"$STREAM_ARGS
Restart=always
RestartSec=3
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=$SC_HOME
PrivateTmp=true

[Install]
WantedBy=multi-user.target
EOF
  svc_restart "$UNIT"
else
  # LaunchDaemon: los flags van en ProgramArguments; los SECRETOS en
  # EnvironmentVariables (no en argv → no salen en `ps`). El binario los lee por
  # entorno (HUB_TOKEN / WEB_AUTH).
  PROG=""
  add_arg() { PROG="${PROG}    <string>$(xmlesc "$1")</string>
"; }
  add_arg "$BIN"; add_arg "-o"; add_arg "none"
  [ "$WEB" = 1 ] && { add_arg "-web"; add_arg ":$WEBPORT"; add_arg "-hwenc"; add_arg "$HWENC"; }
  add_arg "-state"; add_arg "$SC_HOME/streams.json"
  add_arg "-events"; add_arg "$SC_HOME/events.db"
  [ "$METRICSPORT" = off ] || { add_arg "-metrics"; add_arg "127.0.0.1:$METRICSPORT"; }
  add_arg "-hub"; add_arg "$CENTRAL"
  add_arg "-agent-name"; add_arg "$NAME"
  for s in $STREAMS; do add_arg "$s"; done

  ENVXML="    <key>HUB_TOKEN</key><string>$(xmlesc "$TOKEN")</string>"
  [ -n "$WEB_AUTH_VALUE" ] && ENVXML="$ENVXML
    <key>WEB_AUTH</key><string>$(xmlesc "$WEB_AUTH_VALUE")</string>"

  cat > "$UNIT" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key><string>$LABEL</string>
  <key>ProgramArguments</key>
  <array>
${PROG}  </array>
  <key>EnvironmentVariables</key>
  <dict>
$ENVXML
  </dict>
  <key>RunAtLoad</key><true/>
  <key>KeepAlive</key><true/>
  <key>ProcessType</key><string>Background</string>
  <key>StandardOutPath</key><string>$SC_HOME/streamchecker.log</string>
  <key>StandardErrorPath</key><string>$SC_HOME/streamchecker.log</string>
</dict>
</plist>
EOF
  # 600 root: el plist lleva los secretos en EnvironmentVariables.
  chmod 600 "$UNIT"; chown 0:0 "$UNIT" 2>/dev/null || true
  plutil -lint "$UNIT" >/dev/null 2>&1 || die "plist inválido ($UNIT) — abortando."
  svc_restart "$UNIT" "$LABEL"
fi

# ---- resumen -----------------------------------------------------------------
printf '\n\033[1;32m✓ Sonda instalada.\033[0m\n'
printf '  agente   : %s (%s/%s)\n' "$NAME" "$OS" "$ARCH"
printf '  central  : %s  (aparece en la flota en ~10 s)\n' "$CENTRAL"
[ "$WEB" = 1 ] && printf '  local    : http://<IP-de-este-equipo>:%s  (%s)\n' "$WEBPORT" "$WEB_AUTH_VALUE"
[ "$WEB" = 0 ] && [ -z "$STREAMS" ] && warn "sin --no-web ni --streams: el agente no monitorea nada hasta cargarle streams."
# ffmpeg es opcional pero da los thumbnails/mosaico Y la vista en vivo del
# dashboard; avisar si falta.
if [ "$WEB" = 1 ] && ! command -v ffmpeg >/dev/null 2>&1; then
  warn "ffmpeg no está instalado — thumbnails/mosaico y VISTA EN VIVO del dashboard DESACTIVADOS."
  warn "instalalo ($([ "$OS" = darwin ] && echo 'brew install ffmpeg' || echo 'apt install -y ffmpeg')) y reiniciá el servicio para habilitarlos."
elif [ "$WEB" = 1 ]; then
  printf '  vista en vivo: habilitada (ffmpeg)\n'
  printf '  hwenc    : %s%s (autodetecta; cae a software si el HW no está)\n' \
    "$HWENC" "$([ -n "$HWGROUPS" ] && printf ' [grupos: %s]' "$HWGROUPS")"
fi
if [ "$OS" = linux ]; then
  printf '  logs     : journalctl -u streamchecker -f\n\n'
else
  printf '  logs     : tail -f %s/streamchecker.log  (o log stream --predicate '"'"'process == "streamchecker"'"'"')\n\n' "$SC_HOME"
fi
