#!/usr/bin/env bash
# provision.sh — prepara o servidor endurecido para receber a aplicação.
#
# Instala o runtime, cria a estrutura de publicação, configura o serviço
# gerenciado é o servidor web. Idempotente: rodar de novo não refaz o que já existe.
#
# Uso:
#   provision.sh --host <ip> --user <deploy> --stack <node|python|php|rust|estático|compose> \
#                [--runtime-version 20] [--app-path /srv/app] [--app-port 3000] \
#                [--domain exemplo.com] [--web caddy|nginx] [--start-cmd "npm start"] [--key <chave>]

set -uo pipefail

HOST=""; USER_NAME="deploy"; PORT=22; KEY_PATH=""
STACK=""; RUNTIME_VERSION=""; APP_PATH="/srv/app"; APP_PORT=3000
DOMAIN=""; WEB="caddy"; START_CMD=""

while [ $# -gt 0 ]; do
  case "$1" in
    --host)            HOST="$2"; shift 2 ;;
    --user)            USER_NAME="$2"; shift 2 ;;
    --port)            PORT="$2"; shift 2 ;;
    --key)             KEY_PATH="$2"; shift 2 ;;
    --stack)           STACK="$2"; shift 2 ;;
    --runtime-version) RUNTIME_VERSION="$2"; shift 2 ;;
    --app-path)        APP_PATH="$2"; shift 2 ;;
    --app-port)        APP_PORT="$2"; shift 2 ;;
    --domain)          DOMAIN="$2"; shift 2 ;;
    --web)             WEB="$2"; shift 2 ;;
    --start-cmd)       START_CMD="$2"; shift 2 ;;
    *) echo "argumento desconhecido: $1" >&2; exit 2 ;;
  esac
done

[ -n "$HOST" ] || { echo "ERRO: --host é obrigatório" >&2; exit 2; }
[ -n "$STACK" ] || { echo "ERRO: --stack é obrigatório" >&2; exit 2; }

# Tudo que entra no script remoto (que roda como root) é validado aqui pela
# forma, não só pela presença. Um `--domain 'x"; comando'` virava comando de
# root dentro do heredoc. Conjunto fechado onde dá; caracteres fechados onde
# não dá.
erro_arg() { echo "ERRO: $1" >&2; exit 2; }
case "$STACK" in node|python|php|rust|estático|compose) ;; *) erro_arg "--stack aceita node, python, php, rust, estático ou compose" ;; esac
case "$WEB" in caddy|nginx) ;; *) erro_arg "--web aceita caddy ou nginx" ;; esac
case "$APP_PATH" in /*) ;; *) erro_arg "--app-path precisa ser caminho absoluto" ;; esac
case "$APP_PATH" in *[!A-Za-z0-9._/-]*|*..*) erro_arg "--app-path com caractere inválido" ;; esac
case "$APP_PORT" in ""|*[!0-9]*) erro_arg "--app-port precisa ser número" ;; esac
case "$RUNTIME_VERSION" in *[!0-9.]*) erro_arg "--runtime-version aceita só número, como 20, 3.12 ou 1.88" ;; esac
case "$DOMAIN" in *[!A-Za-z0-9.-]*) erro_arg "--domain com caractere inválido" ;; esac
case "$USER_NAME" in ""|root|*[!a-z0-9_-]*) erro_arg "--user inválido: '${USER_NAME}'" ;; esac
# O comando de início vai para dentro da unidade do systemd e roda como o
# usuário de serviço, com os segredos no ambiente. Sem aspas, crase, cifrão ou
# barra invertida (sairiam do heredoc como root), e sem sudo nem cofre (lombada
# declarada: o muro é o cofre pertencer a root).
case "$START_CMD" in
  *\"*|*\`*|*\$*|*\\*|*$'\n'*) erro_arg "--start-cmd não pode conter aspas duplas, crase, cifrão nem barra invertida" ;;
  *sudo*|*" su "*|*sshepherd-secrets*|*/var/lib/sshepherd*) erro_arg "--start-cmd não pode conter sudo, su, sshepherd-secrets nem /var/lib/sshepherd" ;;
esac

SSH_OPTS=(-o BatchMode=yes -o StrictHostKeyChecking=accept-new -o ForwardAgent=no -o ConnectTimeout=10 -p "$PORT")
[ -n "$KEY_PATH" ] && SSH_OPTS+=(-i "${KEY_PATH/#\~/$HOME}" -o IdentitiesOnly=yes)

SERVICE_NAME="sshepherd-app"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

# O cofre de segredos: usuário de serviço, pasta de root, helper e sudoers.
# Compose fica FORA do cofre de propósito. A unidade de compose roda o YAML da
# release, e YAML escrito pelo usuário de operação rodando como root seria root
# para quem publica. Em compose os segredos continuam em shared/.env, legíveis
# pelo usuário de operação, e isso está escrito no README como limite.
. "${SCRIPT_DIR}/vault-install.sh"
VAULT_BLOCK=""
[ "$STACK" = "compose" ] || VAULT_BLOCK=$(vault_install_block "$USER_NAME" "$APP_PATH")

# Retrato antes de mexer. Provisionar instala runtime, servidor web e serviço:
# se algo sair torto, este e o único caminho de volta que não depende do provedor.
echo "== 0. retrato do servidor antes da mudança =="
"${SCRIPT_DIR}/snapshot-etc.sh" --host "$HOST" --user "$USER_NAME" --port "$PORT" \
  ${KEY_PATH:+--key "$KEY_PATH"} --motivo provision || \
  echo "aviso: não consegui gravar o retrato; seguindo assim mesmo" >&2

echo "--------------------------------------------"
echo " PROVISIONAMENTO de ${HOST}"
echo "  tecnologia   : ${STACK}"
echo "  caminho      : ${APP_PATH}"
echo "  porta interna: ${APP_PORT}"
echo "  servidor web : ${WEB}"
echo "  domínio      : ${DOMAIN:-nenhum, respondera pelo IP sem certificado}"
echo "--------------------------------------------"

REMOTE=$(cat <<REMOTE_SCRIPT
set -uo pipefail
export DEBIAN_FRONTEND=noninteractive
STACK="${STACK}"; APP_PATH="${APP_PATH}"; APP_PORT="${APP_PORT}"
DOMAIN="${DOMAIN}"; WEB="${WEB}"; OPS="${USER_NAME}"
RUNTIME_VERSION="${RUNTIME_VERSION}"; START_CMD="${START_CMD}"
SERVICE="${SERVICE_NAME}"

echo "== espaço em disco =="
FREE_MB=\$(df -Pm / | tail -n1 | awk '{print \$4}')
echo "disco_livre_mb=\${FREE_MB}"
if [ "\${FREE_MB}" -lt 1024 ]; then
  echo "ERRO: menos de 1 GB livre. Necessário ao menos 1 GB para provisionar."
  exit 7
fi

echo "== memória e arquivo de troca =="
MEM_MB=\$(free -m | awk '/^Mem:/{print \$2}')
echo "memoria_mb=\${MEM_MB}"
if [ "\${MEM_MB}" -lt 2048 ] && [ ! -f /swapfile ]; then
  fallocate -l 2G /swapfile 2>/dev/null || dd if=/dev/zero of=/swapfile bs=1M count=2048 2>/dev/null
  chmod 600 /swapfile && mkswap /swapfile >/dev/null 2>&1 && swapon /swapfile 2>/dev/null
  grep -q '/swapfile' /etc/fstab || echo '/swapfile none swap sw 0 0' >> /etc/fstab
  echo "swap=criado_2gb"
else
  echo "swap=\$([ -f /swapfile ] && echo ja_existia || echo desnecessario)"
fi

echo "== runtime =="
apt-get update -qq >/dev/null 2>&1 || true
case "\$STACK" in
  node)
    if ! command -v node >/dev/null 2>&1; then
      MAJOR="\${RUNTIME_VERSION:-20}"
      curl -fsSL "https://deb.nodesource.com/setup_\${MAJOR}.x" | bash - >/dev/null 2>&1
      apt-get install -y -qq nodejs >/dev/null 2>&1
    fi
    echo "runtime=node \$(node --version 2>/dev/null || echo falhou)" ;;
  python)
    apt-get install -y -qq python3 python3-venv python3-pip >/dev/null 2>&1
    echo "runtime=python \$(python3 --version 2>/dev/null || echo falhou)" ;;
  php)
    apt-get install -y -qq php-fpm php-cli >/dev/null 2>&1
    echo "runtime=php \$(php --version 2>/dev/null | head -n1 || echo falhou)" ;;
  rust)
    # O binário é compilado no servidor pelo deploy, como \$OPS. A toolchain fica
    # em /opt/rust (de root, só leitura para os outros), com atalhos em
    # /usr/local/bin; o cache de crates vai para o ~/.cargo de quem compila.
    apt-get install -y -qq build-essential pkg-config libssl-dev curl >/dev/null 2>&1
    TOOLCHAIN="\${RUNTIME_VERSION:-stable}"
    export RUSTUP_HOME=/opt/rust/rustup CARGO_HOME=/opt/rust/cargo
    if [ ! -x /opt/rust/cargo/bin/rustup ]; then
      curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \\
        | sh -s -- -y --no-modify-path --profile minimal --default-toolchain none >/dev/null 2>&1
    fi
    /opt/rust/cargo/bin/rustup toolchain install "\$TOOLCHAIN" --profile minimal >/dev/null 2>&1
    /opt/rust/cargo/bin/rustup default "\$TOOLCHAIN" >/dev/null 2>&1
    chmod -R a+rX /opt/rust
    for B in cargo rustc rustup; do
      printf '#!/bin/sh\nexport RUSTUP_HOME=/opt/rust/rustup\nexec /opt/rust/cargo/bin/%s "\$@"\n' "\$B" > "/usr/local/bin/\$B"
      chmod 755 "/usr/local/bin/\$B"
    done
    unset RUSTUP_HOME CARGO_HOME
    # Pasta de compilação fora de releases/: cada versão levaria um target/ de
    # gigabytes, e reaproveitar deixa o próximo build incremental. Só \$OPS
    # escreve; em shared/ o usuário de serviço plantaria o que o build executa.
    install -d -m 700 -o "\$OPS" -g "\$OPS" "\$APP_PATH/build-cache"
    echo "runtime=rust \$(rustc --version 2>/dev/null || echo falhou)" ;;
  estático)
    echo "runtime=nenhum (conteúdo estático)" ;;
  compose)
    if ! command -v docker >/dev/null 2>&1; then
      curl -fsSL https://get.docker.com | sh >/dev/null 2>&1
    fi
    usermod -aG docker "\$OPS" 2>/dev/null || true
    echo "runtime=docker \$(docker --version 2>/dev/null || echo falhou)" ;;
  *)
    echo "runtime=stack_nao_suportada (\$STACK); provisionando só as camadas independentes" ;;
esac

echo "== estrutura de publicação =="
SVC="sshepherd-app"
install -d -m 755 -o "\$OPS" -g "\$OPS" "\$APP_PATH" "\$APP_PATH/releases"
if [ "\$STACK" = "compose" ]; then
  install -d -o "\$OPS" -g "\$OPS" "\$APP_PATH/shared" "\$APP_PATH/shared/logs"
  if [ ! -f "\$APP_PATH/shared/.env" ]; then
    install -m 600 -o "\$OPS" -g "\$OPS" /dev/null "\$APP_PATH/shared/.env"
    echo "env_file=criado_vazio"
  else
    echo "env_file=ja_existia"
  fi
  echo "cofre=nao_aplicavel_em_compose (segredos em shared/.env, legíveis por \$OPS)"
else
  echo "== cofre de segredos =="
  ${VAULT_BLOCK}
  # shared/ é de quem publica, com escrita para quem roda: uploads, cache e
  # registro da aplicação moram aqui. O código, em releases/, só leitura.
  install -d -m 2775 -o "\$OPS" -g "\$SVC" "\$APP_PATH/shared" "\$APP_PATH/shared/logs"
  # Grupo docker equivale a root. Sem compose não há motivo para \$OPS estar nele.
  if id -nG "\$OPS" 2>/dev/null | tr ' ' '\n' | grep -qx docker; then
    gpasswd -d "\$OPS" docker >/dev/null 2>&1 && echo "grupo_docker=removido_de_\$OPS (equivale a root; sem compose não precisa)"
  fi
fi
echo "estrutura=\$APP_PATH (releases, shared, current)"

echo "== porta interna livre? =="
if ss -ltn 2>/dev/null | awk '{print \$4}' | grep -qE ":\${APP_PORT}\$"; then
  echo "AVISO: porta \${APP_PORT} já esta ocupada por outro serviço"
  echo "porta_ocupada=sim"
else
  echo "porta_ocupada=não"
fi

echo "== serviço gerenciado =="
if [ "\$STACK" = "compose" ]; then
  cat > "/etc/systemd/system/\${SERVICE}.service" <<UNIT
[Unit]
Description=Aplicação gerenciada pelo SSHepherd (compose)
After=docker.service
Requires=docker.service

[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=\${APP_PATH}/current
ExecStart=/usr/bin/docker compose up -d
ExecStop=/usr/bin/docker compose down
User=\${OPS}

[Install]
WantedBy=multi-user.target
UNIT
elif [ "\$STACK" = "estático" ]; then
  echo "servico=nao_aplicavel (conteúdo estático servido pelo servidor web)"
else
  CMD="\$START_CMD"
  if [ -z "\$CMD" ]; then
    case "\$STACK" in
      node)   CMD="/usr/bin/node index.js" ;;
      python) CMD="/usr/bin/python3 app.py" ;;
      php)    CMD="/usr/bin/php -S 127.0.0.1:\${APP_PORT}" ;;
      rust)   CMD="\${APP_PATH}/current/app" ;;
    esac
  fi
  # A aplicação roda como o usuário de serviço, não como quem publica. O systemd
  # lê o cofre como root e só então troca de usuário: o processo recebe os
  # segredos no ambiente, e nenhum dos dois usuários consegue abrir o arquivo.
  #
  # Registro no journal, não em arquivo de shared/: shared/ pertence a quem
  # publica, e um symlink no lugar de app.log fazia o systemd (root) escrever
  # onde o symlink apontasse. Journal não tem esse caminho.
  #
  # StartLimit: Restart=on-failure a cada 5s nunca batia no limite padrão (5 em
  # 10s) e um cofre errado virava reinício infinito. Cinco falhas em cinco
  # minutos e a unidade para, com estado "failed" para o doctor ler.
  cat > "/etc/systemd/system/\${SERVICE}.service" <<UNIT
[Unit]
Description=Aplicação gerenciada pelo SSHepherd
After=network.target
StartLimitIntervalSec=300
StartLimitBurst=5

[Service]
Type=simple
User=\${SVC}
Group=\${SVC}
WorkingDirectory=\${APP_PATH}/current
EnvironmentFile=-/var/lib/sshepherd/secrets/app.env
Environment=PORT=\${APP_PORT}
ExecStart=\${CMD}
Restart=on-failure
RestartSec=5
TimeoutStartSec=90
TimeoutStopSec=30
NoNewPrivileges=yes
PrivateTmp=yes
UMask=0002
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target
UNIT
fi

if [ -f "/etc/systemd/system/\${SERVICE}.service" ]; then
  systemctl daemon-reload
  systemctl enable "\${SERVICE}" >/dev/null 2>&1
  echo "servico=\${SERVICE} habilitado (sobrevive a reinício do servidor)"
  # Processo que já estava no ar continua rodando como o usuário antigo, com
  # os segredos no ambiente, até ser substituído. daemon-reload não troca UID.
  if systemctl is-active --quiet "\${SERVICE}" 2>/dev/null; then
    systemctl restart "\${SERVICE}" >/dev/null 2>&1
    sleep 3
    echo "servico_reiniciado=\$(systemctl is-active "\${SERVICE}" 2>/dev/null || echo inativo) (para rodar com a unidade nova)"
  fi
fi

echo "== servidor web =="
if [ "\$WEB" = "caddy" ]; then
  if ! command -v caddy >/dev/null 2>&1; then
    apt-get install -y -qq debian-keyring debian-archive-keyring apt-transport-https curl >/dev/null 2>&1
    curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg 2>/dev/null
    echo "deb [signed-by=/usr/share/keyrings/caddy-stable-archive-keyring.gpg] https://dl.cloudsmith.io/public/caddy/stable/deb/debian any-version main" > /etc/apt/sources.list.d/caddy-stable.list
    apt-get update -qq >/dev/null 2>&1 && apt-get install -y -qq caddy >/dev/null 2>&1
  fi
  # Arquivo próprio, para nunca sobrescrever configuração pré-existente (EC-05).
  if [ "\$STACK" = "estático" ]; then
    BODY="root * \${APP_PATH}/current
    file_server"
  else
    BODY="reverse_proxy 127.0.0.1:\${APP_PORT}"
  fi
  if [ -n "\$DOMAIN" ]; then SITE="\$DOMAIN"; else SITE=":80"; fi
  cat > /etc/caddy/Caddyfile.sshepherd <<CADDY
\${SITE} {
    encode gzip
    header -Server
    \${BODY}
}
CADDY
  grep -q 'Caddyfile.sshepherd' /etc/caddy/Caddyfile 2>/dev/null || echo "import /etc/caddy/Caddyfile.sshepherd" >> /etc/caddy/Caddyfile
  systemctl enable --now caddy >/dev/null 2>&1
  systemctl reload caddy >/dev/null 2>&1 || systemctl restart caddy >/dev/null 2>&1
  echo "web=caddy configurado (certificado automático quando ha domínio)"
else
  apt-get install -y -qq nginx >/dev/null 2>&1
  if [ "\$STACK" = "estático" ]; then
    LOC="root \${APP_PATH}/current; index index.html;"
  else
    LOC="proxy_pass http://127.0.0.1:\${APP_PORT}; proxy_set_header Host \\\$host; proxy_set_header X-Real-IP \\\$remote_addr;"
  fi
  cat > /etc/nginx/sites-available/sshp <<NGINX
server {
    listen 80;
    server_name \${DOMAIN:-_};
    server_tokens off;
    add_header X-Content-Type-Options nosniff;
    add_header X-Frame-Options SAMEORIGIN;
    location / { \${LOC} }
}
NGINX
  ln -sf /etc/nginx/sites-available/sshp /etc/nginx/sites-enabled/sshp
  if nginx -t >/dev/null 2>&1; then
    systemctl enable --now nginx >/dev/null 2>&1
    systemctl reload nginx >/dev/null 2>&1
    echo "web=nginx configurado"
  else
    rm -f /etc/nginx/sites-enabled/sshp
    echo "web=nginx_config_invalida, removida"
  fi
fi

echo "== firewall: liberar portas públicas =="
if command -v ufw >/dev/null 2>&1; then
  ufw allow 80/tcp >/dev/null 2>&1
  ufw allow 443/tcp >/dev/null 2>&1
  echo "firewall=80 e 443 liberadas"
fi

echo "== certificado =="
if [ -n "\$DOMAIN" ]; then
  RESOLVED=\$(getent hosts "\$DOMAIN" | awk '{print \$1}' | head -n1)
  MYIP=\$(curl -s --max-time 5 https://api.ipify.org 2>/dev/null || hostname -I | awk '{print \$1}')
  if [ "\$RESOLVED" = "\$MYIP" ]; then
    echo "dns=apontando_certo (\$RESOLVED)"
    [ "\$WEB" = "caddy" ] && echo "certificado=o caddy emite sozinho no primeiro acesso"
    if [ "\$WEB" = "nginx" ]; then
      apt-get install -y -qq certbot python3-certbot-nginx >/dev/null 2>&1
      certbot --nginx -d "\$DOMAIN" --non-interactive --agree-tos --register-unsafely-without-email --redirect >/dev/null 2>&1 \\
        && echo "certificado=emitido" || echo "certificado=falhou, confira o apontamento e tente depois"
    fi
  else
    echo "dns=NAO_APONTA (domínio resolve para '\${RESOLVED:-nada}', servidor e '\${MYIP}')"
    echo "certificado=adiado (emitir agora gastaria tentativa e bloquearia o domínio por horas)"
  fi
else
  echo "certificado=nao_aplicavel (sem domínio)"
fi

echo "provisionamento=concluído"
REMOTE_SCRIPT
)

OUT=$(ssh "${SSH_OPTS[@]}" "${USER_NAME}@${HOST}" "sudo -n bash -s" <<< "$REMOTE" 2>&1)
CODE=$?
printf '%s\n' "$OUT"

if printf '%s' "$OUT" | grep -q "password is required"; then
  echo "" >&2
  echo "'${USER_NAME}' não tem sudo sem senha para bash, e o SSHepherd não concede isso." >&2
  echo "O provisionamento precisa de root. Caminhos: rode este script conectando como" >&2
  echo "o usuário administrativo (antes do endurecimento), ou libere o sudo você mesmo." >&2
  exit 14
fi

if [ $CODE -ne 0 ]; then
  echo "" >&2
  echo "Provisionamento interrompido (código ${CODE}). O que já foi feito permanece;" >&2
  echo "rode de novo para retomar apenas o que falta." >&2
  exit $CODE
fi

echo ""
echo "== verificação final =="
ssh "${SSH_OPTS[@]}" "${USER_NAME}@${HOST}" "
  systemctl is-enabled ${SERVICE_NAME} 2>/dev/null | sed 's/^/  serviço habilitado: /' || echo '  serviço habilitado: não aplicavel'
  curl -s -o /dev/null -w '  porta pública responde: %{http_code}\n' --max-time 5 http://127.0.0.1/ || echo '  porta pública responde: sem resposta (normal antes do primeiro deploy)'
  ls -d ${APP_PATH}/releases ${APP_PATH}/shared >/dev/null 2>&1 && echo '  estrutura de publicação: ok'
" 2>&1

echo ""
echo "Servidor pronto. Próximo passo: /sshp-deploy"
exit 0
