#!/usr/bin/env bash
# ════════════════════════════════════════════════════════════════════════════
# IDros bootstrap — ETAPA A do provisionamento (Fleet 2 etapas, 2026-07-08)
# ════════════════════════════════════════════════════════════════════════════
#
# Instala IDros num Rock zerado e coloca o robô no ORFANATO — tenant builtin
# da IDtrack onde robôs sem cliente aguardam adoção. A Etapa B (adoção) é
# feita depois via Hub: escolhe tenant + location e o robô se reprovisiona
# sozinho (manifest + mapas + tailnet do tenant), sem ninguém tocar nele.
#
# Fluxo:
#   1. Instala deps de sistema
#   2. Apresenta o bootstrap token ao Fleet → recebe o pacote da Etapa A
#      (identidade do robô, preauthkey da tailnet, creds MQTT do orfanato)
#   3. Instala tailscale + entra na tailnet (user __orphanage__)
#   4. Baixa IDros tarball do artifact server VIA TAILNET
#   5. Extrai + venv + deps Python
#   6. Manifest do orfanato (com provisioning.bootstrap_token pro announce)
#   7. Sudoers drop-in (reprovisionamento precisa de tailscale up + restart)
#   8. systemd → IDros sobe automático no boot
#
# USO (no Rock zerado com Ubuntu/Debian + acesso root):
#   1. Operador registra o robô no Hub (POST /fleet/robots) → recebe o token
#   2. curl -fsSL https://bootstrap.idtrack.net | sudo bash -s -- \
#          --bootstrap-token="<token>"
#
# Args opcionais:
#   --hostname=<nome>           hostname na tailnet (default: robot_id do Fleet)
#   --fleet-api=<url>           API do Fleet (default: https://api.idtrack.net)
#   --idros-version=<version>   pin de versão (default: latest)
#   --headscale-url=<url>       servidor Headscale (default: nosso)
#   --artifact-server=<url>     artifact server (default: http://100.64.0.1:8080)
#   --serial-device=<dev>       device serial do chassi (default: /dev/reeman,
#                               symlink udev estável instalado por este script)
#   --rfid-net-ip=<cidr>        IP estático na rede-ilha do switch do leitor RFID
#                               (ex: 192.168.72.50/24). USE UM RANGE DEDICADO,
#                               DIFERENTE do WiFi do local (ex: 192.168.72.0/24),
#                               senão os 2 segmentos colidem (ARP flux). Opt-in:
#                               sem isso, não configura eth nenhuma. Ver 1c/1e.
#   --rfid-net-iface=<iface>    interface eth no switch (default: auto-detecta a
#                               1ª ethernet com fio via nmcli)
#   --rfid-reader-ip=<ip>       IP do reader nessa ilha (ex: 192.168.72.174)
#
# ⚠️  DEPLOY: este arquivo é SERVIDO por https://bootstrap.idtrack.net (o
#     `curl | bash` acima puxa a cópia do droplet, NÃO o git). Mudança aqui só
#     vale depois de republicar o .sh no droplet — deploy ≠ git. Passo do Julio.

set -euo pipefail

# ─── Defaults ───────────────────────────────────────────────────────────────
FLEET_API="https://api.idtrack.net"
HEADSCALE_URL="https://headscale.idtrack.net"
ARTIFACT_SERVER="http://100.64.0.1:8080"
IDROS_VERSION="latest"
BOOTSTRAP_TOKEN=""
HOSTNAME_OVERRIDE=""
SERIAL_DEVICE="/dev/reeman"   # symlink udev estável (regra instalada na seção 1b)
RFID_NET_IP=""                # ex: 192.168.72.50/24 — IP do Rock na ilha do reader (RANGE DEDICADO ≠ WiFi; opt-in)
RFID_NET_IFACE=""             # interface eth no switch (default: auto-detect via nmcli)
RFID_READER_IP=""             # ex: 192.168.72.174 — IP do reader na ilha
MAINBOARD_ETH_IP="192.168.72.60"  # IP da eth (USB) da mainboard Reeman na ilha — gw dela = o Rock (NAT, seção 1e)
ADMIN_SUDO_PASSWORD="${ADMIN_SUDO_PASSWORD:-}"  # senha sudo padrão (rock + user idtrack); vazio=não mexe. Passe via env/flag — NUNCA no git.

INSTALL_DIR="/opt/idros"
MANIFEST_PATH="${INSTALL_DIR}/manifest.json"

# ─── Parse args ─────────────────────────────────────────────────────────────
while [[ $# -gt 0 ]]; do
  case "$1" in
    --bootstrap-token=*) BOOTSTRAP_TOKEN="${1#*=}"; shift ;;
    --bootstrap-token)   BOOTSTRAP_TOKEN="$2"; shift 2 ;;
    --hostname=*)        HOSTNAME_OVERRIDE="${1#*=}"; shift ;;
    --hostname)          HOSTNAME_OVERRIDE="$2"; shift 2 ;;
    --fleet-api=*)       FLEET_API="${1#*=}"; shift ;;
    --fleet-api)         FLEET_API="$2"; shift 2 ;;
    --idros-version=*)   IDROS_VERSION="${1#*=}"; shift ;;
    --idros-version)     IDROS_VERSION="$2"; shift 2 ;;
    --headscale-url=*)   HEADSCALE_URL="${1#*=}"; shift ;;
    --headscale-url)     HEADSCALE_URL="$2"; shift 2 ;;
    --artifact-server=*) ARTIFACT_SERVER="${1#*=}"; shift ;;
    --artifact-server)   ARTIFACT_SERVER="$2"; shift 2 ;;
    --serial-device=*)   SERIAL_DEVICE="${1#*=}"; shift ;;
    --serial-device)     SERIAL_DEVICE="$2"; shift 2 ;;
    --rfid-net-ip=*)     RFID_NET_IP="${1#*=}"; shift ;;
    --rfid-net-ip)       RFID_NET_IP="$2"; shift 2 ;;
    --rfid-net-iface=*)  RFID_NET_IFACE="${1#*=}"; shift ;;
    --rfid-net-iface)    RFID_NET_IFACE="$2"; shift 2 ;;
    --rfid-reader-ip=*)  RFID_READER_IP="${1#*=}"; shift ;;
    --rfid-reader-ip)    RFID_READER_IP="$2"; shift 2 ;;
    --admin-password=*)  ADMIN_SUDO_PASSWORD="${1#*=}"; shift ;;
    --admin-password)    ADMIN_SUDO_PASSWORD="$2"; shift 2 ;;
    -h|--help)
      sed -n '/^# USO/,/^# Args/p' "$0"
      exit 0 ;;
    *)
      echo "Arg desconhecido: $1" >&2
      exit 1 ;;
  esac
done

# ─── Validação ──────────────────────────────────────────────────────────────
if [[ $EUID -ne 0 ]]; then
  echo "ERRO: precisa rodar como root (sudo)." >&2
  exit 1
fi
if [[ -z "${BOOTSTRAP_TOKEN}" ]]; then
  echo "ERRO: --bootstrap-token obrigatório." >&2
  echo "  Registre o robô no Hub (Fleet → Robôs → Registrar) e cole o" >&2
  echo "  comando gerado, ou passe o token manualmente." >&2
  exit 1
fi

echo "═══════════════════════════════════════════════════════════════════"
echo "IDros bootstrap — Etapa A (orfanato)"
echo "═══════════════════════════════════════════════════════════════════"
echo "  Fleet API:       ${FLEET_API}"
echo "  Headscale:       ${HEADSCALE_URL}"
echo "  Artifact server: ${ARTIFACT_SERVER}"
echo "  IDros version:   ${IDROS_VERSION}"
echo "  Install dir:     ${INSTALL_DIR}"
echo "═══════════════════════════════════════════════════════════════════"
echo ""

# ─── 1. Instala deps de sistema ─────────────────────────────────────────────
echo "→ [1/8] Instalando dependências de sistema (apt)..."
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
apt-get install -y -qq \
  python3 python3-venv python3-pip \
  curl ca-certificates jq \
  > /dev/null
echo "✓ deps instaladas"

# ─── 1b. Regra udev: /dev/reeman estável pro chassi ─────────────────────────
# O nome /dev/ttyUSBn muda quando o adaptador USB-serial cai e volta (cabo,
# glitch de energia, reboot). Um symlink fixo /dev/reeman → o adaptador PL2303
# do chassi torna o manifest imune a isso. Ver infra/idros/99-idtrack-reeman.rules.
echo "→ [1b] Instalando regra udev /dev/reeman..."
cat > /etc/udev/rules.d/99-idtrack-reeman.rules <<'UDEV'
# /dev/reeman — symlink estável pro USB-serial do chassi Reeman (PL2303).
# Gerado por infra/idros/bootstrap.sh. Detalhes: infra/idros/99-idtrack-reeman.rules.
SUBSYSTEM=="tty", ATTRS{idVendor}=="067b", ATTRS{idProduct}=="2303", SYMLINK+="reeman", GROUP="dialout", MODE="0660"
UDEV
udevadm control --reload-rules 2>/dev/null || true
udevadm trigger --subsystem-match=tty 2>/dev/null || true
if [ -e /dev/reeman ]; then
  echo "✓ /dev/reeman → $(readlink -f /dev/reeman)"
else
  echo "⚠ /dev/reeman ainda não presente (chassi desligado/desconectado?). O"
  echo "  IDros reconecta sozinho quando o adaptador aparecer."
fi

# ─── 1c. Rede-ilha do leitor RFID (switch dedicado) ─────────────────────────
# Setup: reader Impinj (R700), a eth da mainboard Reeman e o Rock no MESMO
# switch, numa REDE-ILHA com RANGE DEDICADO (ex: 192.168.72.0/24) — SEPARADO do
# WiFi do local. IMPORTANTE (aprendido no campo 2026-08-04): se a ilha usar o
# MESMO range do WiFi (ex: ambos 192.168.1.0/24), os dois segmentos colidem —
# a mainboard fica dual-homed no mesmo /24 (WiFi + cabo) e dá ARP flux (hosts
# não sabem qual porta usar; o robô "some" da rede). Range dedicado resolve.
# A eth do Rock recebe um IP estático nessa ilha pra alcançar o reader (sem
# isso: "No route to host"). Opt-in via --rfid-net-ip. Persiste no reboot
# (NetworkManager, autoconnect). never-default: a ilha NÃO é rota default do
# Rock (ele sai pela WiFi/tailnet).
if [[ -n "${RFID_NET_IP}" ]]; then
  echo ""
  echo "→ [1c] Configurando rede do leitor RFID (${RFID_NET_IP})..."
  if ! command -v nmcli &> /dev/null; then
    echo "⚠ nmcli não encontrado — pulei a config da rede do reader."
    echo "  (Rock usa NetworkManager; configure a eth manualmente se preciso.)"
  else
    IFACE="${RFID_NET_IFACE}"
    if [[ -z "${IFACE}" ]]; then
      IFACE=$(nmcli -t -f DEVICE,TYPE device 2>/dev/null | awk -F: '$2=="ethernet"{print $1; exit}')
      echo "  interface auto-detectada: ${IFACE:-<nenhuma>}"
    fi
    if [[ -z "${IFACE}" ]]; then
      echo "⚠ nenhuma interface ethernet com fio encontrada — pulei a config."
    else
      # Rotas /32 via eth pro reader E pra mainboard (num /24 plano, força o
      # tráfego deles pela eth — senão o Rock tenta pela WiFi e falha quando o
      # WiFi da mainboard cai).
      ROUTES=""
      [[ -n "${RFID_READER_IP}" ]] && ROUTES="${RFID_READER_IP}/32"
      [[ -n "${MAINBOARD_ETH_IP}" ]] && ROUTES="${ROUTES:+${ROUTES},}${MAINBOARD_ETH_IP%%/*}/32"
      ROUTES_ARG=()
      [[ -n "${ROUTES}" ]] && ROUTES_ARG=(ipv4.routes "${ROUTES}")
      nmcli con delete impinj-net 2>/dev/null || true   # idempotente (re-bootstrap)
      nmcli con add type ethernet ifname "${IFACE}" con-name impinj-net \
        ipv4.method manual ipv4.addresses "${RFID_NET_IP}" \
        ipv4.never-default yes ipv4.route-metric 700 \
        "${ROUTES_ARG[@]}" \
        ipv6.method disabled \
        connection.autoconnect yes
      nmcli con up impinj-net 2>/dev/null || true
      echo "✓ eth ${IFACE} = ${RFID_NET_IP} (con impinj-net; autoconnect; never-default)"
    fi
  fi
fi

# ─── 1d. Proxy web da mainboard + alias de nome (padrão) ────────────────────
# A mainboard Reeman fica na eth (MAINBOARD_ETH_IP); com o WiFi dela desligado
# (padrão no campo — sai pela internet do Rock), ela só é alcançável PELO Rock.
# Este proxy (socat) expõe a web/API da mainboard nas interfaces do Rock
# (WiFi/tailnet) na :8090 → Mac e suporte remoto chegam via Rock. Alias
# 'mainboard'/'robo' no /etc/hosts pro lado do Rock referir por nome.
if [[ -n "${RFID_NET_IP}" && -n "${MAINBOARD_ETH_IP}" ]]; then
  MB_IP="${MAINBOARD_ETH_IP%%/*}"
  echo ""
  echo "→ [1d] Proxy web da mainboard (${MB_IP}:80 → Rock:8090)..."
  if command -v socat &> /dev/null || apt-get install -y socat >/dev/null 2>&1; then
    cat > /etc/systemd/system/idros-mainboard-proxy.service <<UNIT
[Unit]
Description=IDros mainboard web proxy (Rock -> mainboard eth ${MB_IP}:80)
After=network-online.target

[Service]
ExecStart=/usr/bin/socat TCP-LISTEN:8090,fork,reuseaddr TCP:${MB_IP}:80
Restart=always
RestartSec=3

[Install]
WantedBy=multi-user.target
UNIT
    systemctl daemon-reload
    systemctl enable --now idros-mainboard-proxy.service 2>/dev/null || true
    echo "✓ proxy da mainboard em :8090 (systemd idros-mainboard-proxy)"
  else
    echo "⚠ socat indisponível — pulei o proxy da mainboard."
  fi
  grep -q "reeman-mainboard" /etc/hosts 2>/dev/null || \
    echo "${MB_IP} mainboard robo reeman-mainboard" >> /etc/hosts
fi

# ─── 1e. Rock = gateway/NAT da ilha (internet p/ a mainboard) ───────────────
# Com a WiFi da mainboard desligada (alvo do piloto), a mainboard sai pra
# internet PELO cabo → o Rock → WiFi/tailnet. Pra isso o Rock roteia+NATeia a
# rede-ilha. Assim o suporte remoto da Reeman (túnel de saída da mainboard,
# frp p/ o servidor deles) continua funcionando atrás do nosso NAT, sem WiFi e
# sem abrir porta de entrada. A mainboard precisa ter o gateway default = IP do
# Rock na ilha (${RFID_NET_IP%%/*}) — isso é config NA mainboard (feita pela
# Reeman; a gente não tem shell nela). Validado no campo 2026-08-04.
if [[ -n "${RFID_NET_IP}" ]]; then
  ISLAND_NET="$(echo "${RFID_NET_IP%%/*}" | awk -F. '{print $1"."$2"."$3".0/24"}')"
  echo ""
  echo "→ [1e] NAT da ilha ${ISLAND_NET} (Rock = gateway de internet)..."
  sysctl -w net.ipv4.ip_forward=1 >/dev/null 2>&1 || true
  grep -q "net.ipv4.ip_forward" /etc/sysctl.d/99-idtrack-nat.conf 2>/dev/null || \
    echo "net.ipv4.ip_forward=1" > /etc/sysctl.d/99-idtrack-nat.conf
  # MASQUERADE: reescreve a origem da ilha na saída (WiFi/tailnet).
  iptables -t nat -C POSTROUTING -s "${ISLAND_NET}" ! -d "${ISLAND_NET}" -j MASQUERADE 2>/dev/null || \
    iptables -t nat -A POSTROUTING -s "${ISLAND_NET}" ! -d "${ISLAND_NET}" -j MASQUERADE
  # FORWARD ACCEPT: o Docker no Rock põe a policy da cadeia FORWARD em DROP e
  # gerencia o iptables — SEM estas regras, o encaminhamento ilha→WiFi é
  # DROPADO antes do POSTROUTING e a mainboard NÃO alcança a internet (o túnel
  # de suporte da Reeman pelo cabo não sobe). Idempotente.
  iptables -C FORWARD -s "${ISLAND_NET}" -j ACCEPT 2>/dev/null || \
    iptables -A FORWARD -s "${ISLAND_NET}" -j ACCEPT
  iptables -C FORWARD -d "${ISLAND_NET}" -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || \
    iptables -A FORWARD -d "${ISLAND_NET}" -m state --state RELATED,ESTABLISHED -j ACCEPT
  # MSS clamp: o uplink do local (WiFi/roteador) às vezes tem MTU < 1500. Sem
  # isto, o TCP encaminhado da ilha negocia MSS de 1500 e os pacotes GRANDES são
  # descartados em silêncio (DF + PMTUD quebrado) — o controle passa mas transfer
  # trava. Pegou o suporte remoto da Reeman (frp): "vejo o robô mas a interface
  # não carrega". set-mss 1400 (não clamp-to-pmtu: nossa iface é 1500, o gargalo
  # é upstream). Ver robot-network-topology.md.
  iptables -t mangle -C FORWARD -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss 1400 2>/dev/null || \
    iptables -t mangle -A FORWARD -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss 1400
  # Persiste. Instala o pacote se faltar (espelha o padrão do socat na 1d) —
  # senão as regras somem no reboot (ex: queda de energia no hospital).
  command -v netfilter-persistent &> /dev/null || \
    DEBIAN_FRONTEND=noninteractive apt-get install -y iptables-persistent >/dev/null 2>&1 || true
  if command -v netfilter-persistent &> /dev/null; then
    netfilter-persistent save >/dev/null 2>&1 || true
    echo "✓ NAT ${ISLAND_NET} ativo + FORWARD + persistido (netfilter-persistent)"
  else
    echo "⚠ netfilter-persistent indisponível — regras ativas mas NÃO persistem no reboot"
  fi
fi

# ─── 2. Apresenta o token ao Fleet → pacote da Etapa A ──────────────────────
# Endpoint PÚBLICO (via Caddy) — o robô ainda NÃO está na tailnet; o
# preauthkey vem na resposta. Auth pelo bootstrap token one-time.
echo ""
echo "→ [2/8] Apresentando bootstrap token ao Fleet..."
HOSTNAME_ARG="${HOSTNAME_OVERRIDE:-$(hostname)}"
BOOTSTRAP_RESPONSE=$(curl -fsS -X POST \
  -H "Content-Type: application/json" \
  -d "{\"token\":\"${BOOTSTRAP_TOKEN}\",\"hostname\":\"${HOSTNAME_ARG}\"}" \
  --max-time 30 \
  "${FLEET_API}/api/v1/fleet/provisioning/bootstrap")

ROBOT_ID=$(echo "${BOOTSTRAP_RESPONSE}"        | jq -r '.robot_id')
# robot_type = TIPO do robô (qualquer `type` do catálogo, ver
# apps/core-api/src/fleet/robot-types.catalog.ts); model = MARCA do chassi
# (reeman_moonknight|fake), ausente pra tipos sem marca de chassi.
ROBOT_TYPE=$(echo "${BOOTSTRAP_RESPONSE}"      | jq -r '.robot_type')
MODEL=$(echo "${BOOTSTRAP_RESPONSE}"           | jq -r '.model')
TENANT_ID=$(echo "${BOOTSTRAP_RESPONSE}"       | jq -r '.tenant_id')
TS_AUTHKEY=$(echo "${BOOTSTRAP_RESPONSE}"      | jq -r '.headscale_preauthkey')
MQTT_HOST=$(echo "${BOOTSTRAP_RESPONSE}"       | jq -r '.mqtt.mqtt_host')
MQTT_PORT=$(echo "${BOOTSTRAP_RESPONSE}"       | jq -r '.mqtt.mqtt_port')
MQTT_USER=$(echo "${BOOTSTRAP_RESPONSE}"       | jq -r '.mqtt.mqtt_user')
MQTT_PASSWORD=$(echo "${BOOTSTRAP_RESPONSE}"   | jq -r '.mqtt.mqtt_password')
# cortex_id: presente SÓ no ramo CLONE (troca de hardware) — o Fleet devolve o
# executor_id lógico do robô pra o Rock novo re-anunciar como o MESMO executor
# (preserva a identidade). Ausente/null no bootstrap normal (robô novo anuncia
# do zero e o engine gera o executor_id).
CORTEX_ID=$(echo "${BOOTSTRAP_RESPONSE}"       | jq -r '.cortex_id // empty')
# Valida o formato (defesa: roda como root e o valor vai cru no heredoc
# Python/JSON abaixo — Rotsen). Fora do padrão exec-xxx → descarta (trata como
# ausente), não aborta o bootstrap.
if [[ -n "${CORTEX_ID}" && ! "${CORTEX_ID}" =~ ^exec-[a-z0-9-]+$ ]]; then
  echo "AVISO: cortex_id '${CORTEX_ID}' fora do padrão exec-xxx — ignorando" >&2
  CORTEX_ID=""
fi
# WiFi de sistema (opcional): presente só se o operador preencheu no pré-voo
# (vale tanto pro bootstrap normal quanto pro ramo CLONE — o robô clonado
# herda o WiFi do original). `// empty` deixa a var vazia quando o campo não
# veio. NUNCA ecoar WIFI_PSK.
WIFI_SSID=$(echo "${BOOTSTRAP_RESPONSE}"       | jq -r '.wifi.ssid // empty')
WIFI_SECURITY=$(echo "${BOOTSTRAP_RESPONSE}"   | jq -r '.wifi.security // empty')
WIFI_PSK=$(echo "${BOOTSTRAP_RESPONSE}"        | jq -r '.wifi.psk // empty')

if [[ -z "${ROBOT_ID}" || "${ROBOT_ID}" == "null" ]]; then
  echo "ERRO: resposta do Fleet inválida:" >&2
  echo "${BOOTSTRAP_RESPONSE}" >&2
  exit 1
fi
HOSTNAME_OVERRIDE="${HOSTNAME_OVERRIDE:-${ROBOT_ID}}"
# Sanitiza o hostname pra ser um DNS label VÁLIDO — o `tailscale up` rejeita
# nome com '_', espaço ou maiúscula ("... is not a valid DNS label"). Converte
# pra minúsculas, troca qualquer char inválido por '-', remove '-' das pontas e
# corta em 63 chars. Assim um nome de robô como "teste_sergio" vira
# "teste-sergio" em vez de abortar o provisionamento.
HOSTNAME_SAFE=$(printf '%s' "${HOSTNAME_OVERRIDE}" \
  | tr '[:upper:]' '[:lower:]' \
  | sed -E 's/[^a-z0-9-]+/-/g; s/^-+//; s/-+$//' \
  | cut -c1-63)
[[ -z "${HOSTNAME_SAFE}" ]] && HOSTNAME_SAFE="robot"
if [[ "${HOSTNAME_SAFE}" != "${HOSTNAME_OVERRIDE}" ]]; then
  echo "→ hostname '${HOSTNAME_OVERRIDE}' ajustado p/ DNS label válido: '${HOSTNAME_SAFE}'"
fi
HOSTNAME_OVERRIDE="${HOSTNAME_SAFE}"
# Coalesce genérico (sem citar tipo nenhum): manifest.robot.type historicamente
# é a MARCA (driver de hardware do IDros escolhe por aqui); tipo sem marca
# cai pro TIPO puro. Mesma regra pra qualquer tipo futuro do catálogo.
MANIFEST_TYPE="${MODEL}"
if [[ -z "${MANIFEST_TYPE}" || "${MANIFEST_TYPE}" == "null" ]]; then
  MANIFEST_TYPE="${ROBOT_TYPE}"
fi
echo "✓ Pacote recebido: robot_id=${ROBOT_ID} type=${ROBOT_TYPE} model=${MODEL} (orfanato)"

# ─── 2b. WiFi de sistema (NetworkManager) ───────────────────────────────────
# Se o pré-voo trouxe WiFi, grava uma conexão de SISTEMA (todos os usuários,
# sobe headless no boot). Elimina o PSK bakeado na imagem — cada local põe o
# seu no diálogo do Hub. Idempotente (reescreve o keyfile a cada re-bootstrap).
#
# Segurança:
#  - O PSK é escrito DIRETO no keyfile (0600 root) — NUNCA via argv/`nmcli`
#    (vazaria em `ps`). O keyfile é um .ini (GKeyFile): CR/LF no ssid/psk
#    forjariam seções → rejeitamos ANTES de qualquer log (o Core já valida;
#    defesa em profundidade no Rock). E escapamos `\` + espaço/tab de BORDA
#    (senão o GKeyFile corta na leitura → keyfile quebrado SILENCIOSO).
#  - NUNCA ecoamos o PSK (sem `set -x`); e no erro NÃO ecoamos nem o ssid cru
#    (evita forjar linhas no log do instalador).
#  - NÃO subimos a conexão (`nmcli con up`): headless, o NM pega no próximo
#    autoconnect/reload — evita derrubar a rede atual no meio do bootstrap.
if [[ -n "${WIFI_SSID}" ]]; then
  echo ""
  WIFI_SECURITY="${WIFI_SECURITY:-wpa-psk}"
  # Escapa um valor pro GKeyFile (NetworkManager): `\`→`\\` e espaço/tab de
  # BORDA → `\s`/`\t`. Basta escapar o 1º/último char de borda: a linha deixa
  # de começar/terminar com whitespace cru e o GKeyFile não corta mais nada.
  kf_escape() {
    local v="${1//\\/\\\\}"
    case "$v" in
      ' '*) v="\\s${v:1}" ;;
      $'\t'*) v="\\t${v:1}" ;;
    esac
    case "$v" in
      *' ') v="${v:0:${#v}-1}\\s" ;;
      *$'\t') v="${v:0:${#v}-1}\\t" ;;
    esac
    printf '%s' "$v"
  }
  # Anti-injeção de keyfile: barra CR/LF no ssid/psk ANTES de qualquer log.
  case "${WIFI_SSID}${WIFI_PSK}" in
    *$'\n'* | *$'\r'*)
      echo "ERRO: config de WiFi inválida (quebra de linha no ssid/psk) — pulei." >&2
      ;;
    *)
      echo "→ [2b] Configurando WiFi de sistema (ssid=${WIFI_SSID})..."
      WIFI_SSID_ESC="$(kf_escape "${WIFI_SSID}")"
      WIFI_PSK_ESC="$(kf_escape "${WIFI_PSK}")"
      WIFI_KEYFILE="/etc/NetworkManager/system-connections/idtrack-wifi.nmconnection"
      mkdir -p /etc/NetworkManager/system-connections
      # Cria o arquivo já 0600 (umask) ANTES de escrever o segredo.
      ( umask 077
        {
          printf '[connection]\n'
          printf 'id=idtrack-wifi\n'
          printf 'type=wifi\n'
          printf 'autoconnect=true\n'
          printf 'permissions=\n'
          printf '\n[wifi]\n'
          printf 'mode=infrastructure\n'
          printf 'ssid=%s\n' "${WIFI_SSID_ESC}"
          if [[ "${WIFI_SECURITY}" == "none" ]]; then
            :  # rede aberta: sem seção [wifi-security]
          else
            printf '\n[wifi-security]\n'
            printf 'key-mgmt=wpa-psk\n'
            printf 'psk-flags=0\n'
            printf 'psk=%s\n' "${WIFI_PSK_ESC}"
          fi
          printf '\n[ipv4]\n'
          printf 'method=auto\n'
          printf '\n[ipv6]\n'
          printf 'method=auto\n'
        } > "${WIFI_KEYFILE}"
      )
      chown root:root "${WIFI_KEYFILE}"
      chmod 600 "${WIFI_KEYFILE}"
      # Recarrega os keyfiles sem derrubar a rede atual (sem `con up`).
      nmcli con reload 2>/dev/null || true
      echo "✓ WiFi gravado (${WIFI_KEYFILE}, 0600 root) — sobe no próximo autoconnect"
      ;;
  esac
fi

# ─── 3. Instala tailscale + entra na tailnet ────────────────────────────────
echo ""
echo "→ [3/8] Instalando tailscale + conectando na tailnet (orfanato)..."
if ! command -v tailscale &> /dev/null; then
  curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/jammy.noarmor.gpg \
    | tee /usr/share/keyrings/tailscale-archive-keyring.gpg > /dev/null
  curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/jammy.tailscale-keyring.list \
    | tee /etc/apt/sources.list.d/tailscale.list > /dev/null
  apt-get update -qq
  apt-get install -y -qq tailscale > /dev/null
  echo "✓ tailscale instalado: $(tailscale version | head -1)"
else
  echo "✓ tailscale já instalado: $(tailscale version | head -1)"
fi
systemctl enable --now tailscaled

tailscale up \
  --login-server="${HEADSCALE_URL}" \
  --authkey="${TS_AUTHKEY}" \
  --hostname="${HOSTNAME_OVERRIDE}" \
  --accept-routes=false \
  --reset

TS_IP=$(tailscale ip -4)
echo "✓ Tailnet IP: ${TS_IP}"

# ─── 4. Baixa IDros tarball via tailnet ─────────────────────────────────────
echo ""
echo "→ [4/8] Baixando IDros via tailnet..."
if [[ "${IDROS_VERSION}" == "latest" ]]; then
  LATEST_JSON=$(curl -fsSL "${ARTIFACT_SERVER}/idros/latest.json")
  IDROS_VERSION=$(echo "${LATEST_JSON}" | jq -r '.current_version')
  TARBALL_URL=$(echo "${LATEST_JSON}" | jq -r '.tarball_url')
  EXPECTED_SHA=$(echo "${LATEST_JSON}" | jq -r '.sha256')
else
  TARBALL_URL="${ARTIFACT_SERVER}/idros/idros-${IDROS_VERSION}.tar.gz"
  EXPECTED_SHA=""
fi
echo "  Versão: ${IDROS_VERSION}"
echo "  URL:    ${TARBALL_URL}"

TMP_TARBALL="/tmp/idros-${IDROS_VERSION}.tar.gz"
curl -fsSL "${TARBALL_URL}" -o "${TMP_TARBALL}"

if [[ -n "${EXPECTED_SHA}" ]]; then
  GOT_SHA=$(sha256sum "${TMP_TARBALL}" | awk '{print $1}')
  if [[ "${GOT_SHA}" != "${EXPECTED_SHA}" ]]; then
    echo "ERRO: sha256 mismatch" >&2
    echo "  Esperado: ${EXPECTED_SHA}" >&2
    echo "  Recebido: ${GOT_SHA}" >&2
    exit 1
  fi
  echo "✓ sha256 OK"
fi

# ─── 5. Extrai + venv + deps Python ─────────────────────────────────────────
echo ""
echo "→ [5/8] Extraindo em ${INSTALL_DIR} + venv..."
mkdir -p "${INSTALL_DIR}"
tar -xzf "${TMP_TARBALL}" -C "${INSTALL_DIR}"
rm "${TMP_TARBALL}"
python3 -m venv "${INSTALL_DIR}/.venv"
"${INSTALL_DIR}/.venv/bin/pip" install -q --upgrade pip
"${INSTALL_DIR}/.venv/bin/pip" install -q \
  -r "${INSTALL_DIR}/apps/ros/requirements.txt" \
  -e "${INSTALL_DIR}/python-libs/capability-sdk"
echo "✓ IDros ${IDROS_VERSION} + venv prontos"

# ─── 6. Manifest do orfanato ────────────────────────────────────────────────
# Robô órfão: sem mapas, sem location — só identidade + MQTT do orfanato +
# provisioning.bootstrap_token (pro IDros anunciar o cortex_id ao Fleet).
# O manifest DEFINITIVO chega na adoção via reprovisionamento (top-down,
# Fleet é a fonte da verdade).
echo ""
echo "→ [6/8] Configurando manifest (orfanato)..."
if [[ "${MANIFEST_TYPE}" == "fake" ]]; then
  CONNECTION_TYPE="http"
else
  CONNECTION_TYPE="serial"
fi

# cortex_id do manifest: no CLONE (troca de HW) o Fleet mandou o executor_id
# lógico pra preservar a identidade; no bootstrap normal vem vazio → null JSON.
if [[ -n "${CORTEX_ID}" ]]; then
  CORTEX_ID_JSON="\"${CORTEX_ID}\""
else
  CORTEX_ID_JSON="null"
fi

if [[ -f "${MANIFEST_PATH}" ]]; then
  # Re-bootstrap: preserva o resto, atualiza cortex.* + provisioning
  echo "✓ manifest existe — atualizando cortex.* + provisioning..."
  python3 <<PYEOF
import json
p = "${MANIFEST_PATH}"
m = json.load(open(p))
m.setdefault("cortex", {})
m["cortex"]["mqtt_host"] = "${MQTT_HOST}"
m["cortex"]["mqtt_port"] = ${MQTT_PORT}
m["cortex"]["mqtt_user"] = "${MQTT_USER}" or None
m["cortex"]["mqtt_password"] = "${MQTT_PASSWORD}" or None
m["cortex"]["tenant_id"] = "${TENANT_ID}"
# provisioning: SEMPRE regrava fleet_api_url + bootstrap_token (o token ATUAL
# da resposta). Sem isto, o re-bootstrap/clone seguiria usando o token ANTIGO e
# o announce daria 403.
m["provisioning"] = {
    "fleet_api_url": "${FLEET_API}",
    "bootstrap_token": "${BOOTSTRAP_TOKEN}",
}
# CLONE (troca de HW): grava o cortex_id lógico que o Fleet devolveu, pra o
# Rock novo re-anunciar como o MESMO executor. Vazio = bootstrap normal, não
# mexe no cortex_id existente (robô órfão o descobre no primeiro announce).
cortex_id = "${CORTEX_ID}"
if cortex_id:
    m.setdefault("robot", {})
    m["robot"]["cortex_id"] = cortex_id
json.dump(m, open(p, "w"), indent=2, ensure_ascii=False)
print(f"  ✓ cortex + provisioning atualizados (user={m['cortex']['mqtt_user']})")
PYEOF
else
  cat > "${MANIFEST_PATH}" <<EOF
{
  "robot": {
    "id": "${ROBOT_ID}",
    "name": "${HOSTNAME_OVERRIDE}",
    "type": "${MANIFEST_TYPE}",
    "cortex_id": ${CORTEX_ID_JSON}
  },
  "cortex": {
    "mqtt_host": "${MQTT_HOST}",
    "mqtt_port": ${MQTT_PORT},
    "mqtt_user": "${MQTT_USER}",
    "mqtt_password": "${MQTT_PASSWORD}",
    "tenant_id": "${TENANT_ID}"
  },
  "provisioning": {
    "fleet_api_url": "${FLEET_API}",
    "bootstrap_token": "${BOOTSTRAP_TOKEN}"
  },
  "os": {
    "telemetry_api_port": 8099,
    "heartbeat_interval": 30,
    "pose_interval": 2
  },
  "capabilities": [],
  "meeting_points": [],
  "safety": {
    "battery_minimum": 15,
    "connectivity_timeout": 120,
    "emergency_button_inverted": true
  },
  "timeouts": {
    "zone_navigation": 180,
    "zone_scan": 300,
    "return_to_base": 240
  },
  "hardware": {
    "connection_type": "${CONNECTION_TYPE}",
    "serial_device": "${SERIAL_DEVICE}",
    "serial_baud": 115200,
    "api_url": "",
    "charge_flag_docked": 2,
    "charge_flag_docking": 8,
    "map_to_location": {},
    "discovered_maps": [],
    "rfid_reader": null
  }
}
EOF
  echo "✓ manifest criado em ${MANIFEST_PATH}"
fi
chmod 600 "${MANIFEST_PATH}"

mkdir -p "${INSTALL_DIR}/identity" "${INSTALL_DIR}/maps"

# ─── 7. User + sudoers pro reprovisionamento ────────────────────────────────
echo ""
echo "→ [7/8] User idros + sudoers do reprovisionamento..."
if ! id -u idros &> /dev/null; then
  useradd --system --no-create-home --shell /usr/sbin/nologin idros
fi
# Grupos: journald logs (admin_server) + dialout (serial /dev/ttyUSB*)
usermod -a -G systemd-journal,adm,dialout idros
chown -R idros:idros "${INSTALL_DIR}"

# O reprovisionamento (adoção) precisa re-autenticar a tailnet no user do
# tenant destino e reiniciar o IDros — ambos exigem root. Permitimos
# EXATAMENTE esses comandos, nada além.
TAILSCALE_BIN=$(command -v tailscale)
SYSTEMCTL_BIN=$(command -v systemctl)
BASH_BIN=$(command -v bash)

# Script de update remoto num caminho ESTÁVEL (fora de /opt/idros, que ele
# mesmo sobrescreve). O admin_server chama via `sudo /usr/local/bin/idros-update`.
install -m 755 "${INSTALL_DIR}/apps/ros/scripts/update-idros.sh" /usr/local/bin/idros-update

cat > /etc/sudoers.d/idros-provisioning <<SUDOEOF
# IDros reprovisionamento (Fleet adoção) + operação remota — gerado pelo bootstrap.sh
idros ALL=(root) NOPASSWD: ${TAILSCALE_BIN} up *
idros ALL=(root) NOPASSWD: ${SYSTEMCTL_BIN} restart idros
idros ALL=(root) NOPASSWD: ${BASH_BIN} -c sleep 2 && systemctl restart idros
# Atualizar IDros / reiniciar o host pela tela (admin_server /update, /reboot-host)
idros ALL=(root) NOPASSWD: /usr/local/bin/idros-update
idros ALL=(root) NOPASSWD: ${SYSTEMCTL_BIN} reboot
# Gravar a senha da tela viva (:0) + reiniciar idtrack-screen (reprovision.py)
idros ALL=(root) NOPASSWD: /usr/local/bin/idtrack-screen-setpass
SUDOEOF
chmod 440 /etc/sudoers.d/idros-provisioning
echo "✓ sudoers drop-in + update-script instalados"

# ─── 7b. Usuário admin sudo + hardening de SSH (opt-in) ─────────────────────
# Padroniza o acesso do robô: cria o user 'idtrack' (sudo, admin não-default),
# define a senha sudo de 'rock' e 'idtrack', e DESABILITA login root por SSH.
# Opt-in via ADMIN_SUDO_PASSWORD (env ou --admin-password) — a senha NUNCA fica
# no git. Sem a var, não mexe em usuário/senha/ssh.
if [[ -n "${ADMIN_SUDO_PASSWORD}" ]]; then
  echo ""
  echo "→ [7b] User admin sudo (idtrack) + senha sudo + hardening de SSH..."
  id -u idtrack &>/dev/null || useradd -m -s /bin/bash idtrack
  usermod -aG sudo idtrack
  echo "idtrack:${ADMIN_SUDO_PASSWORD}" | chpasswd
  id -u rock &>/dev/null && echo "rock:${ADMIN_SUDO_PASSWORD}" | chpasswd
  # desabilita login root por SSH (valida a config ANTES de recarregar)
  mkdir -p /etc/ssh/sshd_config.d
  echo 'PermitRootLogin no' > /etc/ssh/sshd_config.d/99-idtrack-hardening.conf
  if sshd -t; then
    systemctl reload ssh 2>/dev/null || systemctl reload sshd 2>/dev/null || true
    echo "✓ user idtrack + senha sudo (rock/idtrack) + root SSH desabilitado"
  else
    echo "⚠ sshd -t falhou — removi o drop-in pra não quebrar o SSH"
    rm -f /etc/ssh/sshd_config.d/99-idtrack-hardening.conf
  fi
fi

# ─── 8. systemd ─────────────────────────────────────────────────────────────
echo ""
echo "→ [8/8] Configurando systemd..."
install -m 644 "${INSTALL_DIR}/apps/ros/systemd/idros.service" /etc/systemd/system/idros.service

systemctl daemon-reload
systemctl enable idros.service
systemctl restart idros.service

sleep 3
if systemctl is-active --quiet idros.service; then
  echo "✓ idros.service rodando (PID $(systemctl show idros -p MainPID --value))"
else
  echo "⚠ idros.service não subiu — logs:"
  journalctl -u idros --no-pager -n 30
  exit 1
fi

# ─── 8b. Sidecar RFID LLRP (Impinj R700) ────────────────────────────────────
# O sidecar C++ fala LLRP com o reader e expõe um UDS que o driver Python do
# IDros consome. O binário (arm32 estático) linka libs LTK PROPRIETÁRIAS (EULA
# Impinj), é gitignored e NÃO cabe num secret (~4.7MB) → fica no ARTIFACT SERVER
# e é baixado aqui (best-effort). Rebuild+upload quando o .cpp mudar (ver runbook
# impinj-sidecar-build-e-secret.md). Só instala o serviço SE o binário existir.
SIDECAR_BIN="${INSTALL_DIR}/apps/ros/impinj-sidecar/impinj-sidecar"
SIDECAR_UNIT="${INSTALL_DIR}/apps/ros/impinj-sidecar/idros-impinj-sidecar.service"
echo ""
echo "→ [8b] Sidecar RFID LLRP (Impinj)..."
if curl -fsSL "${ARTIFACT_SERVER}/idros/impinj-sidecar" -o "${SIDECAR_BIN}.new" 2>/dev/null; then
  mv "${SIDECAR_BIN}.new" "${SIDECAR_BIN}"
  echo "  ✓ binário baixado de ${ARTIFACT_SERVER}/idros/impinj-sidecar"
else
  rm -f "${SIDECAR_BIN}.new" 2>/dev/null || true
  echo "  ⚠ binário do sidecar indisponível no artifact server — RFID/LLRP fica off"
fi
if [[ -f "${SIDECAR_BIN}" ]]; then
  chmod +x "${SIDECAR_BIN}"
  chown idros:idros "${SIDECAR_BIN}" 2>/dev/null || true
  # Env com o IP do reader. Cria se não existe; se re-bootstrap com --rfid-reader-ip
  # explícito, atualiza a linha; senão preserva o que já está lá.
  mkdir -p /etc/idros
  if [[ ! -f /etc/idros/impinj-sidecar.env ]]; then
    echo "READER_IP=${RFID_READER_IP}" > /etc/idros/impinj-sidecar.env
    chmod 644 /etc/idros/impinj-sidecar.env
    echo "  ✓ /etc/idros/impinj-sidecar.env criado (READER_IP=${RFID_READER_IP:-<vazio>})"
  elif [[ -n "${RFID_READER_IP}" ]]; then
    if grep -q '^READER_IP=' /etc/idros/impinj-sidecar.env; then
      sed -i "s|^READER_IP=.*|READER_IP=${RFID_READER_IP}|" /etc/idros/impinj-sidecar.env
    else
      echo "READER_IP=${RFID_READER_IP}" >> /etc/idros/impinj-sidecar.env
    fi
    echo "  ✓ /etc/idros/impinj-sidecar.env atualizado (READER_IP=${RFID_READER_IP})"
  else
    echo "  ✓ /etc/idros/impinj-sidecar.env preservado ($(grep '^READER_IP=' /etc/idros/impinj-sidecar.env || echo READER_IP=?))"
  fi
  install -m 644 "${SIDECAR_UNIT}" /etc/systemd/system/idros-impinj-sidecar.service
  systemctl daemon-reload
  systemctl enable idros-impinj-sidecar.service 2>/dev/null || true
  # Só sobe agora se há um READER_IP setado — sem ele o sidecar só churna
  # (Restart=always). Com o IP vazio, deixa habilitado pro próximo boot/adoção.
  EFFECTIVE_READER_IP="$(sed -n 's/^READER_IP=//p' /etc/idros/impinj-sidecar.env 2>/dev/null | head -1)"
  if [[ -n "${EFFECTIVE_READER_IP}" ]]; then
    systemctl restart idros-impinj-sidecar.service 2>/dev/null || true
    sleep 2
    if systemctl is-active --quiet idros-impinj-sidecar.service; then
      echo "✓ idros-impinj-sidecar.service rodando (reader ${EFFECTIVE_READER_IP})"
    else
      echo "⚠ idros-impinj-sidecar.service não subiu (reader offline/modo REST?). Logs: journalctl -u idros-impinj-sidecar"
    fi
  else
    echo "⚠ READER_IP vazio — serviço habilitado mas NÃO iniciado. Preencha"
    echo "  /etc/idros/impinj-sidecar.env e rode: systemctl start idros-impinj-sidecar"
  fi
else
  echo "⚠ binário do sidecar ausente (${SIDECAR_BIN}) — robô SEM RFID/LLRP."
  echo "  O CI injeta o binário no tarball via secret RFID_SIDECAR_BIN_B64"
  echo "  (ver runbook impinj-sidecar-build-e-secret.md)."
fi

# ─── 8c. Tela viva (:0) via x11vnc — senha por-robô (Guacamole) ─────────────
# A senha nasce no Core e desce em os.screen_vnc_password no manifest de ADOÇÃO
# (o manifest do orfanato normalmente NÃO a tem — aí este passo é no-op). O
# idtrack-screen.service + o helper /usr/local/bin/idtrack-screen-setpass + o
# pacote x11vnc vêm BACADOS na imagem (ver infra/screen/README.md). Aqui só
# habilitamos o serviço e, se o manifest já trouxer a senha (re-bootstrap de um
# robô adotado), gravamos via o mesmo helper que o reprovision.py usa.
echo ""
echo "→ [8c] Tela viva (:0) via x11vnc (Guacamole)..."
if [[ -f /etc/systemd/system/idtrack-screen.service ]]; then
  systemctl daemon-reload
  systemctl enable idtrack-screen.service 2>/dev/null || true
  SCREEN_VNC_PASSWORD="$(jq -r '.os.screen_vnc_password // empty' "${MANIFEST_PATH}" 2>/dev/null || true)"
  if [[ -n "${SCREEN_VNC_PASSWORD}" && -x /usr/local/bin/idtrack-screen-setpass ]]; then
    # Senha pelo STDIN — nunca em argv (PII-adjacente, vazaria em `ps`).
    if printf '%s' "${SCREEN_VNC_PASSWORD}" | /usr/local/bin/idtrack-screen-setpass >/dev/null 2>&1; then
      echo "✓ senha da tela viva gravada + idtrack-screen reiniciado"
    else
      echo "⚠ idtrack-screen-setpass falhou — tela remota indisponível (acessório)"
    fi
    unset SCREEN_VNC_PASSWORD
  else
    echo "  ✓ idtrack-screen habilitado (senha chega na adoção, via reprovision)"
  fi
else
  echo "⚠ idtrack-screen.service ausente (imagem sem o delta infra/screen) — tela viva off"
fi

# ─── Pronto ─────────────────────────────────────────────────────────────────
echo ""
echo "═══════════════════════════════════════════════════════════════════"
echo "✓ Etapa A concluída — robô no ORFANATO, aguardando adoção"
echo "═══════════════════════════════════════════════════════════════════"
echo ""
echo "Tailnet IP:    ${TS_IP}"
echo "Robot ID:      ${ROBOT_ID}"
echo "IDros version: ${IDROS_VERSION}"
echo "Logs:          sudo journalctl -u idros -f"
echo ""
echo "Próximo passo (Etapa B): no Hub, Fleet → Orfanato → Adotar."
echo "O robô recebe manifest + mapas do tenant e se reprovisiona sozinho."