#!/usr/bin/env bash
# deploy.sh — pública a aplicação.
#
# Publica em releases/<carimbo>, liga os arquivos compartilhados, constroi,
# troca o symlink current de forma ATOMICA, reinicia e verifica a saude.
# A versão anterior fica intacta o tempo todo: se a saude falhar, o rollback
# e imediato.
#
# Uso:
#   deploy.sh --host <ip> --user <deploy> --strategy <git|rsync|compose> \
#             [--repo <url>] [--branch main] [--src .] [--app-path /srv/app] \
#             [--build "npm ci --omit=dev"] [--health http://127.0.0.1/] \
#             [--keep 5] [--key <chave>] [--shared-dir storage]... \
#             [--ignore-file <caminho>] [--no-ignore-file] --confirmed
#
# O que sobe e o que não sobe está em .sshepherdignore, na raiz do projeto.
# Vale para rsync e compose. Na estratégia git quem manda e o .gitignore.

set -uo pipefail

# Saída de build e de registro do serviço volta para quem chamou, e quem chamou
# costuma ser o agente. Passa pela mesma redação do executor.
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/redact.sh"

HOST=""; USER_NAME="deploy"; PORT=22; KEY_PATH=""
STRATEGY=""; REPO=""; BRANCH="main"; SRC="."
APP_PATH="/srv/app"; BUILD_CMD=""; HEALTH_URL="http://127.0.0.1/"
KEEP=5; CONFIRMED=0; SERVICE="sshepherd-app"
HEALTH_RETRIES=6; HEALTH_INTERVAL=5
IGNORE_FILE=""; NO_IGNORE_FILE=0
SHARED_DIRS=""

while [ $# -gt 0 ]; do
  case "$1" in
    # Pasta que sobrevive às versões e que a aplicação escreve: storage/ do
    # Laravel, .next/cache, uploads/. Vira link para shared/<pasta>. Repetível.
    --shared-dir)
      case "$2" in
        ""|/*|*..*|*[!A-Za-z0-9._/-]*) echo "ERRO: --shared-dir aceita caminho relativo simples, como storage ou .next/cache" >&2; exit 2 ;;
      esac
      SHARED_DIRS="${SHARED_DIRS} ${2%/}"; shift 2 ;;
    --host) HOST="$2"; shift 2 ;;
    --user) USER_NAME="$2"; shift 2 ;;
    --port) PORT="$2"; shift 2 ;;
    --key)  KEY_PATH="$2"; shift 2 ;;
    --strategy) STRATEGY="$2"; shift 2 ;;
    --repo) REPO="$2"; shift 2 ;;
    --branch) BRANCH="$2"; shift 2 ;;
    --src) SRC="$2"; shift 2 ;;
    --app-path) APP_PATH="$2"; shift 2 ;;
    --build) BUILD_CMD="$2"; shift 2 ;;
    --health) HEALTH_URL="$2"; shift 2 ;;
    --keep) KEEP="$2"; shift 2 ;;
    --service) SERVICE="$2"; shift 2 ;;
    --ignore-file) IGNORE_FILE="$2"; shift 2 ;;
    --no-ignore-file) NO_IGNORE_FILE=1; shift ;;
    --confirmed) CONFIRMED=1; shift ;;
    *) echo "argumento desconhecido: $1" >&2; exit 2 ;;
  esac
done

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

# O comando de build roda no servidor como o usuário de operação, com o texto
# vindo de quem chamou. Build não precisa de sudo nem do cofre. Isto é lombada,
# não muro: regex não segura shell. O muro é o cofre pertencer a root.
case "$BUILD_CMD" in
  *sudo*|*" su "*|*sshepherd-secrets*|*/var/lib/sshepherd*)
    echo "RECUSADO: o comando de build não pode conter sudo, su, sshepherd-secrets nem /var/lib/sshepherd." >&2
    echo "  Build roda como '${USER_NAME}' e não precisa de nada disso." >&2
    exit 12 ;;
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)
SSH_TARGET="${USER_NAME}@${HOST}"

RELEASE=$(date -u +%Y%m%d%H%M%S)

echo "--------------------------------------------"
echo " PUBLICACAO em ${HOST}"
echo "  estratégia : ${STRATEGY}"
echo "  release    : ${RELEASE}"
echo "  caminho    : ${APP_PATH}"
echo "  saude      : ${HEALTH_URL}"
echo "  manter     : ${KEEP} versões anteriores"
echo "--------------------------------------------"

if [ "$CONFIRMED" -ne 1 ]; then
  echo "RECUSADO: publicação altera o servidor e exige --confirmed." >&2
  exit 10
fi

# --- 0. o que sobe e o que não sobe -----------------------------------------
# Piso de segurança: estes nunca sobem, não importa o que o arquivo de regras
# diga. Segredo e histórico de repositorio não têm volta depois de expostos.
RSYNC_RULES=()
for pattern in '.git/' '.env' '.env.*' '.sshepherd/' '*.pem' '*.key' 'id_rsa' 'id_ed25519'; do
  RSYNC_RULES+=(--exclude "$pattern")
done

# Na estratégia git o piso acima não vale: o servidor clona o repositório, e
# arquivo rastreado chega inteiro, com o .gitignore que for. Antes de tocar no
# servidor, confere na cópia local se algum segredo está versionado.
if [ "$STRATEGY" = "git" ] && command -v git >/dev/null 2>&1 \
   && git -C "$SRC" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
  RASTREADOS=$(git -C "$SRC" ls-files -- '.env' '.env.*' '*.pem' '*.key' 'id_rsa' 'id_ed25519' 2>/dev/null)
  if [ -n "$RASTREADOS" ]; then
    echo "RECUSADO: o repositório rastreia arquivo de segredo, e por git ele vai para o servidor:" >&2
    printf '%s\n' "$RASTREADOS" | sed 's/^/  /' >&2
    echo "" >&2
    echo "Tire do histórico (git rm --cached <arquivo>, e troque o segredo: o histórico guarda)," >&2
    echo "ou publique por rsync, onde o piso de segurança vale." >&2
    exit 12
  fi
fi

if [ "$STRATEGY" != "git" ]; then
  [ -n "$IGNORE_FILE" ] || IGNORE_FILE="${SRC%/}/.sshepherdignore"

  echo ""
  echo "== o que sobe =="
  if [ -f "$IGNORE_FILE" ]; then
    REGRAS=$(grep -cvE '^[[:space:]]*(#|$)' "$IGNORE_FILE" || true)
    RSYNC_RULES+=(--exclude-from "$IGNORE_FILE")
    echo "  regras: ${IGNORE_FILE} (${REGRAS} linhas ativas)"
  elif [ "$NO_IGNORE_FILE" -eq 1 ]; then
    echo "  regras: NENHUMA (--no-ignore-file). Só o piso de segurança vale."
  else
    echo "RECUSADO: não encontrei ${IGNORE_FILE}." >&2
    echo "" >&2
    echo "Sem esse arquivo eu não sei o que pode subir para o servidor, e mandar" >&2
    echo "a pasta inteira leva especificação, material interno e chave junto." >&2
    echo "" >&2
    echo "Rode 'npx sshepherd install' para criar o modelo, ou peça ao agente:" >&2
    echo "  /sshp-setup  → ele monta o arquivo com você, olhando seu projeto" >&2
    echo "" >&2
    echo "Para publicar mesmo assim, sem regras: --no-ignore-file" >&2
    exit 12
  fi
fi

# --- trava contra publicação simultanea (EC-06 da spec) ---------------------
LOCK="${APP_PATH}/.sshepherd.lock"
if ! ssh "${SSH_OPTS[@]}" "$SSH_TARGET" "set -e; mkdir '${LOCK}' 2>/dev/null" ; then
  echo "BLOQUEADO: já existe uma publicação em andamento neste servidor." >&2
  echo "Se você tem certeza de que não ha, remova ${LOCK} no servidor." >&2
  exit 11
fi
cleanup() { ssh "${SSH_OPTS[@]}" "$SSH_TARGET" "rmdir '${LOCK}' 2>/dev/null" >/dev/null 2>&1 || true; }
trap cleanup EXIT

REL_DIR="${APP_PATH}/releases/${RELEASE}"

# --- 1. espaço em disco antes de enviar (EC-01) -----------------------------
echo ""
echo "== espaço em disco =="
FREE=$(ssh "${SSH_OPTS[@]}" "$SSH_TARGET" "df -Pm '${APP_PATH}' | tail -n1 | awk '{print \$4}'")
echo "  livre: ${FREE} MB"
if [ "${FREE:-0}" -lt 200 ]; then
  echo "INTERROMPIDO: menos de 200 MB livres. Rode a limpeza de versões antigas antes." >&2
  exit 7
fi

# --- 2. enviar o código -----------------------------------------------------
echo ""
echo "== enviando código (${STRATEGY}) =="
case "$STRATEGY" in
  git)
    [ -n "$REPO" ] || { echo "ERRO: estratégia git exige --repo" >&2; exit 2; }
    ssh "${SSH_OPTS[@]}" "$SSH_TARGET" "
      set -e
      mkdir -p '${REL_DIR}'
      git clone --depth 1 --branch '${BRANCH}' '${REPO}' '${REL_DIR}' 2>&1 | tail -n3
      cd '${REL_DIR}' && git rev-parse --short HEAD
    " || { echo "FALHOU ao clonar o repositorio." >&2; exit 3; }
    # A checagem local olhou a cópia daqui; o que chegou foi o que o remoto
    # tinha. Confere no clone, que é o que vai para o ar.
    RASTREADOS=$(ssh "${SSH_OPTS[@]}" "$SSH_TARGET" "cd '${REL_DIR}' && git ls-files -- '.env' '.env.*' '*.pem' '*.key' 'id_rsa' 'id_ed25519' 2>/dev/null")
    if [ -n "$RASTREADOS" ]; then
      ssh "${SSH_OPTS[@]}" "$SSH_TARGET" "rm -rf '${REL_DIR}'" >/dev/null 2>&1
      echo "RECUSADO: o repositório publicado rastreia arquivo de segredo; a release foi apagada:" >&2
      printf '%s\n' "$RASTREADOS" | sed 's/^/  /' >&2
      echo "Tire do histórico e troque o segredo: o histórico guarda o que foi removido." >&2
      exit 12
    fi
    ;;
  rsync)
    command -v rsync >/dev/null 2>&1 || { echo "ERRO: rsync não está instalado nesta máquina" >&2; exit 3; }
    ssh "${SSH_OPTS[@]}" "$SSH_TARGET" "mkdir -p '${REL_DIR}'"
    rsync -az --delete "${RSYNC_RULES[@]}" \
      -e "ssh ${SSH_OPTS[*]}" \
      "${SRC}/" "${SSH_TARGET}:${REL_DIR}/" || { echo "FALHOU ao sincronizar arquivos." >&2; exit 3; }
    echo "  arquivos sincronizados"
    ;;
  compose)
    command -v rsync >/dev/null 2>&1 || { echo "ERRO: rsync não está instalado nesta máquina" >&2; exit 3; }
    ssh "${SSH_OPTS[@]}" "$SSH_TARGET" "mkdir -p '${REL_DIR}'"
    rsync -az "${RSYNC_RULES[@]}" -e "ssh ${SSH_OPTS[*]}" \
      "${SRC}/" "${SSH_TARGET}:${REL_DIR}/" || { echo "FALHOU ao enviar arquivos." >&2; exit 3; }
    ;;
  *)
    echo "ERRO: estratégia desconhecida '${STRATEGY}'" >&2; exit 2 ;;
esac

# --- 3. ligar compartilhados e construir ------------------------------------
echo ""
echo "== compartilhados e construção =="
# Quem roda a aplicação decide onde estão os segredos. Serviço como
# `sshepherd-app` (provisionado com o cofre): o systemd entrega o ambiente, e
# não existe .env na release, nem como link. Serviço como o usuário de operação
# (compose, ou servidor provisionado antes do cofre): link para shared/.env,
# como sempre foi.
ssh "${SSH_OPTS[@]}" "$SSH_TARGET" "
  set -e
  UNIT_USER=\$(systemctl show -p User --value '${SERVICE}' 2>/dev/null || true)
  if [ \"\$UNIT_USER\" = 'sshepherd-app' ]; then
    rm -f '${REL_DIR}/.env'
    echo '  segredos: cofre, entregues pelo systemd (sem .env na release)'
  else
    ln -sfn '${APP_PATH}/shared/.env' '${REL_DIR}/.env'
    echo '  segredos: shared/.env'
  fi
  ln -sfn '${APP_PATH}/shared/logs' '${REL_DIR}/logs'
  for d in ${SHARED_DIRS}; do
    mkdir -p \"${APP_PATH}/shared/\$d\" \"\$(dirname \"${REL_DIR}/\$d\")\"
    # Primeira vez: a release traz a pasta com a estrutura que a aplicação
    # espera (storage/framework/... do Laravel). Ela vira a semente do shared.
    if [ -d \"${REL_DIR}/\$d\" ] && [ ! -L \"${REL_DIR}/\$d\" ] && [ -z \"\$(ls -A \"${APP_PATH}/shared/\$d\" 2>/dev/null)\" ]; then
      cp -a \"${REL_DIR}/\$d/.\" \"${APP_PATH}/shared/\$d/\" 2>/dev/null || true
    fi
    chmod -R g+rwX \"${APP_PATH}/shared/\$d\" 2>/dev/null || true
    chmod g+s \"${APP_PATH}/shared/\$d\" 2>/dev/null || true
    rm -rf \"${REL_DIR}/\$d\"
    ln -sfn \"${APP_PATH}/shared/\$d\" \"${REL_DIR}/\$d\"
    echo \"  pasta compartilhada: \$d -> shared/\$d\"
  done
  echo '  compartilhados ligados'
" || { echo "FALHOU ao ligar arquivos compartilhados." >&2; exit 4; }

if [ -n "$BUILD_CMD" ]; then
  echo "  construindo: ${BUILD_CMD}"
  # redact ANTES do tail: cortado primeiro, uma chave privada perdia o cabeçalho
  # que a redação usa para reconhecê-la e o corpo passava em claro.
  if ! ssh "${SSH_OPTS[@]}" "$SSH_TARGET" "cd '${REL_DIR}' && ${BUILD_CMD}" 2>&1 | redact | tail -n 15; then
    echo "" >&2
    echo "FALHOU na construção. A versão em uso NAO foi tocada." >&2
    echo "A versão parcial ficou em ${REL_DIR} para você diagnosticar." >&2
    exit 5
  fi
fi

# --- 3b. permissões para quem roda ------------------------------------------
# O código é de quem publica e roda como outro usuário. Ele precisa ler e
# executar tudo, e não precisa escrever nada: o que a aplicação escreve mora
# em shared/. rsync preserva a permissão de origem, e um arquivo 600 na máquina
# local chegava ilegível para o serviço.
ssh "${SSH_OPTS[@]}" "$SSH_TARGET" "chmod -R u+rwX,go+rX,go-w '${REL_DIR}' 2>/dev/null || true"

# --- 4. ativacao atômica ----------------------------------------------------
echo ""
echo "== colocando a versão nova no ar =="
echo "   Mudo o atalho 'current' para a pasta nova, de uma vez so."
echo "   Não existe um instante em que ele aponte para o vazio."
PREVIOUS=$(ssh "${SSH_OPTS[@]}" "$SSH_TARGET" "readlink '${APP_PATH}/current' 2>/dev/null | xargs -r basename" || true)
echo "  versão anterior: ${PREVIOUS:-nenhuma (primeira publicação)}"

ssh "${SSH_OPTS[@]}" "$SSH_TARGET" "
  set -e
  ln -sfn '${REL_DIR}' '${APP_PATH}/current.new'
  mv -Tf '${APP_PATH}/current.new' '${APP_PATH}/current'
  echo '  current -> ${RELEASE}'
" || { echo "FALHOU ao ativar a nova versão." >&2; exit 6; }

# --- 5. reiniciar -----------------------------------------------------------
echo ""
echo "== reiniciando o serviço =="
ssh "${SSH_OPTS[@]}" "$SSH_TARGET" "
  sudo -n systemctl restart '${SERVICE}' 2>/dev/null || true
  sleep 2
  sudo -n systemctl is-active '${SERVICE}' 2>/dev/null || systemctl is-active '${SERVICE}' 2>/dev/null || echo inativo
" | sed 's/^/  estado do serviço: /'

# --- 6. verificação de saude ------------------------------------------------
echo ""
echo "== verificação de saude =="
HEALTHY=0
for i in $(seq 1 "$HEALTH_RETRIES"); do
  CODE=$(ssh "${SSH_OPTS[@]}" "$SSH_TARGET" "curl -s -o /dev/null -w '%{http_code}' --max-time 5 '${HEALTH_URL}' 2>/dev/null || echo 000")
  echo "  tentativa ${i}/${HEALTH_RETRIES}: HTTP ${CODE}"
  case "$CODE" in
    2*|3*) HEALTHY=1; break ;;
  esac
  [ "$i" -lt "$HEALTH_RETRIES" ] && sleep "$HEALTH_INTERVAL"
done

if [ "$HEALTHY" -ne 1 ]; then
  echo ""
  echo "############################################" >&2
  echo " A APLICACAO NAO RESPONDEU APOS A PUBLICACAO" >&2
  echo "############################################" >&2
  echo "" >&2
  echo "Últimas linhas do log do serviço:" >&2
  ssh "${SSH_OPTS[@]}" "$SSH_TARGET" "sudo -n journalctl --no-pager -u '${SERVICE}' -n 20 2>/dev/null || tail -n 20 '${APP_PATH}/shared/logs/app.err' 2>/dev/null" 2>&1 | redact | sed 's/^/  /' >&2
  echo "" >&2
  if [ -n "$PREVIOUS" ]; then
    echo "VOLTE PARA A VERSAO ANTERIOR AGORA:" >&2
    echo "  rollback.sh --host ${HOST} --user ${USER_NAME} --app-path ${APP_PATH} --to ${PREVIOUS} --confirmed" >&2
  else
    echo "Não ha versão anterior para a qual voltar: esta foi a primeira publicação." >&2
  fi
  echo "DEPLOY=falhou_saude" >&2
  echo "RELEASE=${RELEASE}" >&2
  echo "PREVIOUS=${PREVIOUS}" >&2
  exit 8
fi

# --- 6b. o cofre que subiu com esta versão é o que funcionou -----------------
# Só quando o serviço roda com o cofre. É daqui que sai o "último cofre que
# funcionou" para o apply reverter, inclusive na primeira publicação.
ssh "${SSH_OPTS[@]}" "$SSH_TARGET" "
  if [ \"\$(systemctl show -p User --value '${SERVICE}' 2>/dev/null)\" = 'sshepherd-app' ]; then
    sudo -n /usr/local/sbin/sshepherd-secrets promote 2>/dev/null | sed 's/^/  cofre: /' || echo '  cofre: não promovido'
  fi
" 2>/dev/null | redact

# --- 7. limpeza de versões antigas (nunca a ativa, EC-08) -------------------
echo ""
echo "== limpeza de versões antigas =="
ssh "${SSH_OPTS[@]}" "$SSH_TARGET" "
  cd '${APP_PATH}/releases' 2>/dev/null || exit 0
  ATIVA=\$(readlink '${APP_PATH}/current' | xargs -r basename)
  ls -1t | grep -v \"^\${ATIVA}\$\" | tail -n +$((KEEP)) | while read -r old; do
    rm -rf -- \"\$old\" && echo \"  removida: \$old\"
  done
  true
" 2>&1 || echo "  aviso: limpeza falhou, mas a publicação está ativa e saudavel"

echo ""
echo "============================================"
echo " PUBLICADO: ${RELEASE}"
echo " Saude: confirmada"
echo ""
echo " Para voltar atrás a qualquer momento:"
echo "   rollback.sh --host ${HOST} --user ${USER_NAME} --app-path ${APP_PATH} --confirmed"
echo "============================================"
echo "DEPLOY=sucesso"
echo "RELEASE=${RELEASE}"
echo "PREVIOUS=${PREVIOUS}"
exit 0
