#!/usr/bin/env bash
#
# deploy_perc_monitor.sh
#
# Instalador "tudo-em-um" para pôr a monitorização do RAID PERC (perccli +
# script de verificação + alerta email/SMS Twilio) a funcionar num host
# Proxmox/Debian novo, com o mínimo de passos manuais.
#
# ------------------------------------------------------------------------
# O QUE FAZ
# ------------------------------------------------------------------------
#   1. Instala o perccli64 em /opt/MegaRAID/perccli/perccli64 (extrai de um
#      pacote .tar/.rpm da Dell que já tenhas descarregado — ver NOTA
#      abaixo sobre porque não descarrega isto sozinho da internet).
#   2. Instala o pacote msmtp (cliente SMTP), se ainda não estiver presente,
#      e cria /root/.msmtprc com placeholders (só se ainda não existir --
#      nunca apaga uma configuração já feita num rerun). FICA SEMPRE por
#      preencher à mão o email e a password de app do Gmail (ver aviso no
#      resumo final da instalação).
#   3. Escreve monitor_perc_raid.sh e send_sms.sh em /root/scripts/ — a
#      MESMA versão completa usada em produção (CLIENT_NAME, anti-spam por
#      ficheiro de estado, --test-alert, verificação de BBU, deteção de
#      discos desaparecidos, avisos preditivos por SMART/contagem de erros,
#      email HTML com template CorTesTech, histórico de alertas em CSV).
#      Este instalador já não fica com uma cópia desatualizada em separado
#      (ver NOTA sobre avisos preditivos, mais abaixo, antes de confiares
#      nesses alertas em produção).
#   4. Instala rotação de log para o msmtp (/etc/logrotate.d/msmtp).
#   5. Cria o diretório de histórico de alertas (/var/log/perc-monitor).
#   6. Cria /etc/default/perc-monitor com placeholders (só se ainda não
#      existir — nunca apaga credenciais já configuradas num rerun).
#   7. Cria e ativa o serviço + timer systemd (perc-monitor.service /
#      perc-monitor.timer), a correr a cada 10 minutos.
#   8. Testa a ligação à controladora no fim.
#   9. Mostra um resumo final com AVISO destacado de tudo o que ainda falta
#      preencher à mão (email/password de app + credenciais Twilio) --
#      nada de credenciais é inventado ou enviado sozinho por este script.
#
# ------------------------------------------------------------------------
# NOTA SOBRE O perccli
# ------------------------------------------------------------------------
# Este instalador NÃO descarrega o perccli sozinho da internet: a Dell só
# disponibiliza o pacote através do portal de drivers (dell.com/support/
# drivers), com um link que muda por versão e que não é fiável fixar num
# script (podia ficar desatualizado ou parar de funcionar de um dia para
# o outro). Descarrega tu o pacote uma vez por servidor/versão de firmware
# (procura "PERCCLI" em dell.com/support/drivers, pacote Linux .tar/.tar.gz)
# e depois corre este script apontando para o ficheiro:
#
#   ./deploy_perc_monitor.sh /root/perccli_7.1-007.0127_linux.tar
#
# Se já tiveres o pacote em /root, no teu $HOME ou no diretório atual, o
# script tenta encontrá-lo sozinho (não precisas de indicar o caminho).
#
# Se o perccli64 já estiver instalado (reexecução num servidor já feito
# antes), este passo é saltado automaticamente.
#
# ------------------------------------------------------------------------
# NOTA SOBRE FORMATO DE TABELA / VERSÃO DO perccli
# ------------------------------------------------------------------------
# O parsing de monitor_perc_raid.sh assume o formato de tabela típico do
# perccli64 (colunas separadas por espaços). Isto já foi validado numa
# PERC H710 Mini; noutro controlador/firmware as colunas podem variar
# ligeiramente. Depois de instalar num Dell novo, confirma SEMPRE à mão
# antes de confiares nos alertas:
#
#   /root/scripts/monitor_perc_raid.sh -v
#
# Isto mostra o output cru de vall/eall/bbu e se os estados "OK" batem
# certo com OK_VD_STATES / OK_PD_STATES / OK_BBU_STATES no topo do script.
#
# ------------------------------------------------------------------------
# UTILIZAÇÃO
# ------------------------------------------------------------------------
#   sudo ./deploy_perc_monitor.sh [caminho/para/perccli_..._linux.tar]
#
# Depois de correr, faltam sempre dois passos manuais por segurança:
#   1. editar /root/.msmtprc e preencher o email e a password de app do
#      Gmail REAIS deste cliente (o instalador só cria placeholders);
#   2. editar /etc/default/perc-monitor e preencher as credenciais
#      Twilio/email REAIS deste cliente (idem, só placeholders vazios).
#
# ------------------------------------------------------------------------

set -uo pipefail

SCRIPTS_DIR="/root/scripts"
PERCCLI_DEST="/opt/MegaRAID/perccli/perccli64"
CRED_FILE="/etc/default/perc-monitor"
MSMTPRC_FILE="/root/.msmtprc"
LOGROTATE_FILE="/etc/logrotate.d/msmtp"
HISTORY_DIR="/var/log/perc-monitor"

log()  { echo "[deploy] $*"; }
err()  { echo "[deploy] ERRO: $*" >&2; }

if [ "$(id -u)" -ne 0 ]; then
    err "corre como root (sudo ./deploy_perc_monitor.sh ...)."
    exit 1
fi

mkdir -p "$SCRIPTS_DIR"

# ==========================================================================
# 1. perccli64
# ==========================================================================
if [ -x "$PERCCLI_DEST" ]; then
    log "perccli64 já está instalado em $PERCCLI_DEST — a saltar este passo."
else
    PKG="${1:-}"
    PERCCLI_GLOB=( -iname 'perccli*.tar' -o -iname 'perccli*.tar.gz' -o -iname 'perccli*.tgz' -o -iname 'perccli*.rpm' )

    if [ -z "$PKG" ]; then
        # Por omissão assume-se que o .tar da Dell (ex: perccli_7.1-007.0127_linux.tar)
        # foi colocado em /root -- é o local recomendado/habitual.
        log "Nenhum ficheiro indicado — a procurar em /root (local recomendado)..."
        mapfile -t ROOT_MATCHES < <(find /root -maxdepth 1 \( "${PERCCLI_GLOB[@]}" \) 2>/dev/null | sort)

        if [ "${#ROOT_MATCHES[@]}" -eq 1 ]; then
            PKG="${ROOT_MATCHES[0]}"
        elif [ "${#ROOT_MATCHES[@]}" -gt 1 ]; then
            # Mais que um pacote em /root -- usa o modificado mais recentemente
            # (ex: upgrade de versão), mas avisa quais encontrou para confirmares.
            log "Encontrei ${#ROOT_MATCHES[@]} pacotes perccli em /root — a usar o modificado mais recentemente:"
            for m in "${ROOT_MATCHES[@]}"; do log "  - $m"; done
            PKG=$(find /root -maxdepth 1 \( "${PERCCLI_GLOB[@]}" \) -printf '%T@ %p\n' 2>/dev/null \
                    | sort -rn | head -n1 | cut -d' ' -f2-)
        else
            # Nada em /root -- tenta $HOME e o diretório atual como reserva
            log "Nada encontrado em /root — a procurar também em \$HOME e no diretório atual..."
            PKG=$(find "$HOME" . -maxdepth 2 \( "${PERCCLI_GLOB[@]}" \) 2>/dev/null | head -n1)
        fi
    fi

    if [ -z "$PKG" ] || [ ! -f "$PKG" ]; then
        err "Não encontrei nenhum pacote perccli (.tar/.tar.gz/.rpm) em /root."
        err "Descarrega em dell.com/support/drivers (procura 'PERCCLI', pacote Linux)"
        err "coloca o ficheiro em /root/ (ex: /root/perccli_7.1-007.0127_linux.tar)"
        err "e corre outra vez: $0"
        err "(ou indica o caminho explicitamente: $0 /caminho/para/perccli_..._linux.tar)"
        exit 1
    fi
    log "A usar pacote: $PKG"

    if ! command -v rpm2cpio >/dev/null 2>&1; then
        log "A instalar rpm2cpio/cpio..."
        apt-get update -qq && apt-get install -y -qq rpm2cpio cpio
    fi

    WORKDIR=$(mktemp -d)
    cp "$PKG" "$WORKDIR/"
    cd "$WORKDIR" || { err "nao consegui entrar em $WORKDIR"; exit 1; }

    case "$PKG" in
        *.tar|*.tar.gz|*.tgz)
            tar xf "$(basename "$PKG")"
            ;;
    esac

    RPM_FILE=$(find "$WORKDIR" -iname '*.rpm' | head -n1)
    DEB_FILE=$(find "$WORKDIR" -iname '*.deb' | head -n1)

    if [ -n "$RPM_FILE" ]; then
        rpm2cpio "$RPM_FILE" | cpio -idm --quiet
        FOUND_BIN=$(find "$WORKDIR" -iname 'perccli64' -type f | head -n1)
        if [ -z "$FOUND_BIN" ]; then
            err "Extraí o rpm mas não encontrei o binário perccli64 lá dentro."
            cd /; rm -rf "$WORKDIR"
            exit 1
        fi
        mkdir -p "$(dirname "$PERCCLI_DEST")"
        cp "$FOUND_BIN" "$PERCCLI_DEST"
        chmod +x "$PERCCLI_DEST"
    elif [ -n "$DEB_FILE" ]; then
        dpkg -i "$DEB_FILE" || apt-get install -f -y -qq
    else
        err "Não encontrei .rpm nem .deb dentro do pacote fornecido."
        cd /; rm -rf "$WORKDIR"
        exit 1
    fi

    cd /
    rm -rf "$WORKDIR"

    if [ -x "$PERCCLI_DEST" ]; then
        log "perccli64 instalado com sucesso em $PERCCLI_DEST"
    else
        err "perccli64 não ficou no caminho esperado ($PERCCLI_DEST) — confirma manualmente."
    fi
fi

# ==========================================================================
# 2. msmtp (cliente SMTP que envia o email) + /root/.msmtprc placeholder
# ==========================================================================
if command -v msmtp >/dev/null 2>&1; then
    log "msmtp já está instalado — a saltar este passo."
else
    log "A instalar msmtp..."
    apt-get update -qq && apt-get install -y -qq msmtp msmtp-mta ca-certificates
fi

if [ -f "$MSMTPRC_FILE" ]; then
    log "$MSMTPRC_FILE já existe — não vou tocar (evita apagar configuração já feita)."
else
    log "A criar $MSMTPRC_FILE com placeholders — FALTA preencher (ver aviso no fim)."
    cat > "$MSMTPRC_FILE" <<'MSMTPRC_EOF'
# Preenche 'from', 'user' e 'password' com os dados REAIS deste cliente
# antes de confiares nos alertas por email.
#
# 'password' e uma "password de app" da conta Gmail (Conta Google > Seguranca
# > Passwords de aplicacao) -- so existe com verificacao em 2 passos ativa,
# NAO e a password normal da conta.
#
# O log fica em ~/.msmtp.log (nao em /var/log/msmtp.log) porque nalguns
# hosts o AppArmor bloqueia escrita la -- ver /etc/logrotate.d/msmtp.
defaults
auth           on
tls            on
tls_trust_file /etc/ssl/certs/ca-certificates.crt
logfile        ~/.msmtp.log

account        gmail
host           smtp.gmail.com
port           587
from           PREENCHER@gmail.com
user           PREENCHER@gmail.com
password       PREENCHER_PASSWORD_DE_APP

account default : gmail
MSMTPRC_EOF
    chmod 600 "$MSMTPRC_FILE"
fi

# ==========================================================================
# 3. Scripts em /root/scripts (versão completa, identica a producao)
# ==========================================================================
log "A escrever monitor_perc_raid.sh em $SCRIPTS_DIR ..."
cat > "$SCRIPTS_DIR/monitor_perc_raid.sh" <<'MONITOR_SCRIPT_EOF'
#!/usr/bin/env bash
#
# monitor_perc_raid.sh
#
# Verifica o estado do RAID numa controladora Dell PERC (PERC H710 e
# similares, baseadas em LSI/Broadcom MegaRAID) usando o perccli64, e envia
# um alerta por email (HTML, marca CorTesTech) e/ou SMS (Twilio) quando
# detecta:
#   - um array (Virtual Drive) degradado;
#   - um disco fisico com estado anormal (Failed, etc.) mas ainda visivel
#     ao controlador;
#   - um disco que desapareceu por completo da controladora (removido
#     fisicamente, ou uma falha grave que o tira do barramento -- os dois
#     casos parecem iguais do ponto de vista do perccli);
#   - um AVISO PREDITIVO: um disco continua "Onln" mas os contadores de
#     erro (Media/Other Error Count, Predictive Failure Count) ou a flag
#     S.M.A.R.T subiram desde a ultima verificacao saudavel -- sinal de
#     falha a chegar, antes de se tornar um problema critico;
#   - um problema na BBU (bateria da controladora).
#
# Corre diretamente no host Proxmox (bare metal) — a controladora não é
# visivel a partir de dentro de nenhuma VM.
#
# ------------------------------------------------------------------------
# REQUISITOS
# ------------------------------------------------------------------------
#   1. perccli64 instalado. NÃO vem com o Debian/Proxmox nem existe em
#      repositório apt — tem de ser descarregado do site de suporte da
#      Dell (procurar "PERCCLI" em dell.com/support/drivers, pacote
#      Linux .tar.gz). Fica normalmente em /opt/MegaRAID/perccli/perccli64.
#      Confirma com: which perccli64  ||  find / -iname '*perccli*'
#      Se ficar noutro caminho: PERCCLI_BIN=/caminho/para/perccli64 ./monitor_perc_raid.sh
#   2. (opcional, para email) msmtp configurado no host (Gmail, etc.) --
#      o script fala com o msmtp diretamente para poder enviar HTML;
#      se o binário msmtp não estiver no PATH, cai para o comando `mail`
#      tradicional (perde a formatação bonita, mas continua a chegar).
#   3. (opcional, para SMS) curl instalado e uma conta Twilio com
#      Account SID, Auth Token, número/Sender ID de origem e destino.
#   4. bash 4+ (arrays associativos) -- já vem por omissão em qualquer
#      Debian/Proxmox atual.
#
# ------------------------------------------------------------------------
# NOTA IMPORTANTE -- CAMPOS AINDA POR VALIDAR CONTRA HARDWARE REAL
# ------------------------------------------------------------------------
# O parsing de VD/PD/BBU (estado "Optl"/"Onln"/etc.) já foi validado em
# produção numa PERC H710 Mini. A deteção de AVISOS PREDITIVOS é nova e lê
# campos do "perccli64 /c0/eX/sY show all" que NUNCA foram confirmados
# contra o output real de um disco (Media Error Count, Other Error Count,
# Predictive Failure Count, S.M.A.R.T alert flagged by drive) -- os nomes
# destes campos são os documentados pela Broadcom/LSI para o storcli/
# perccli, mas podem variar ligeiramente por versão de firmware.
#
# Antes de confiares nos avisos preditivos em produção, corre:
#     perccli64 /c0/eall/sall show all
# e confirma que esses 4 campos aparecem com esses nomes (ou parecidos).
# O modo verbose (-v) deste script mostra exatamente o que conseguiu ler
# de cada disco -- usa isso para confirmar antes de ligares os alertas.
# Se um campo não for encontrado, o script assume "0"/"desconhecido" e
# simplesmente não dispara esse aviso específico (não arrisca falso
# positivo por não encontrar o campo).
#
# ------------------------------------------------------------------------
# INSTALAÇÃO COMO TAREFA PERIÓDICA (systemd timer, recomendado)
# ------------------------------------------------------------------------
# 1. Copiar este ficheiro para /usr/local/sbin/monitor_perc_raid.sh e
#    dar permissão de execução:
#      chmod +x /usr/local/sbin/monitor_perc_raid.sh
#
# 2. Criar /etc/systemd/system/perc-monitor.service :
#
#      [Unit]
#      Description=Verifica estado do RAID PERC
#
#      [Service]
#      Type=oneshot
#      EnvironmentFile=-/etc/default/perc-monitor
#      ExecStart=/usr/local/sbin/monitor_perc_raid.sh
#
# 3. Criar /etc/systemd/system/perc-monitor.timer :
#
#      [Unit]
#      Description=Corre a verificação do RAID PERC a cada 10 minutos
#
#      [Timer]
#      OnBootSec=2min
#      OnUnitActiveSec=10min
#
#      [Install]
#      WantedBy=timers.target
#
# 4. Colocar as credenciais em /etc/default/perc-monitor (root:root, chmod
#    600, para não ficarem legíveis por todos):
#
#      CLIENT_NAME="NOME-DO-CLIENTE"
#      ALERT_EMAIL_TO="tu@exemplo.com"
#      ALERT_EMAIL_FROM="proxmox-alert@exemplo.com"
#      TWILIO_ACCOUNT_SID="ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
#      TWILIO_AUTH_TOKEN="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
#      TWILIO_FROM_NUMBER="+1XXXXXXXXXX"
#      TWILIO_TO_NUMBER="+351XXXXXXXXX"
#
# 5. Ativar:
#      systemctl daemon-reload
#      systemctl enable --now perc-monitor.timer
#
# Alternativa mais simples (cron), se preferires não usar systemd timers:
#      */10 * * * * root /usr/local/sbin/monitor_perc_raid.sh
#
# ------------------------------------------------------------------------

set -uo pipefail

VERBOSE=0
FORCE_TEST=0
for arg in "$@"; do
    case "$arg" in
        -v|--verbose) VERBOSE=1 ;;
        --test-alert) FORCE_TEST=1 ;;
    esac
done

PERCCLI="${PERCCLI_BIN:-/opt/MegaRAID/perccli/perccli64}"
CTRL="/c0"

# Estados considerados "normais" (ajustar se necessário para o teu firmware)
OK_VD_STATES="Optl"
OK_PD_STATES="Onln|GHS|DHS|UGood|UGud"
OK_BBU_STATES="Optimal"

# A partir de que aumento (desde a ultima verificacao saudavel do MESMO
# disco, identificado pelo serial) um novo erro conta como aviso preditivo.
# Predictive Failure Count e o flag S.M.A.R.T disparam com qualquer
# aumento/ativacao, sem necessidade de threshold.
MEDIA_ERROR_DELTA_THRESHOLD="${MEDIA_ERROR_DELTA_THRESHOLD:-1}"
OTHER_ERROR_DELTA_THRESHOLD="${OTHER_ERROR_DELTA_THRESHOLD:-1}"

# --- Carrega /etc/default/perc-monitor sozinho, se ainda nao veio do
#     ambiente (systemd injeta via EnvironmentFile=; corrido a mao na CLI
#     precisa disto para nao ficar silenciosamente sem credenciais) ---
if [ -z "${ALERT_EMAIL_TO:-}" ] && [ -z "${TWILIO_ACCOUNT_SID:-}" ]; then
    for _candidate in "${PERC_MONITOR_ENV_FILE:-}" "/etc/default/perc-monitor"; do
        if [ -n "$_candidate" ] && [ -r "$_candidate" ]; then
            set -a
            # shellcheck disable=SC1090
            source "$_candidate"
            set +a
            break
        fi
    done
    unset _candidate
fi

# --- Identificacao do cliente/local (aparece no SMS e no email) ---
CLIENT_NAME="${CLIENT_NAME:-$(hostname)}"

# --- Destinos de alerta (também podem vir de /etc/default/perc-monitor) ---
ALERT_EMAIL_TO="${ALERT_EMAIL_TO:-}"
ALERT_EMAIL_FROM="${ALERT_EMAIL_FROM:-proxmox-alert@localhost}"
EMAIL_FROM_NAME="${EMAIL_FROM_NAME:-CORTESTECH SOLUTIONS Monitor}"

TWILIO_ACCOUNT_SID="${TWILIO_ACCOUNT_SID:-}"
TWILIO_AUTH_TOKEN="${TWILIO_AUTH_TOKEN:-}"
TWILIO_FROM_NUMBER="${TWILIO_FROM_NUMBER:-}"
TWILIO_TO_NUMBER="${TWILIO_TO_NUMBER:-}"

# --- Anti-spam: so alerta quando o problema COMECA, depois so relembra de
#     X em X horas enquanto nao for resolvido (nunca a cada verificacao) ---
STATE_DIR="${STATE_DIR:-/var/lib/perc-monitor}"
STATE_FILE="${STATE_DIR}/state"
REMIND_HOURS="${REMIND_HOURS:-4}"
REMIND_SECONDS=$((REMIND_HOURS * 3600))

# --- "Baseline" de discos fisicos: uma fotografia (slot + serial +
#     contadores de erro/SMART) tirada sempre que o RAID esta 100%
#     saudavel. Serve para:
#       a) detetar um disco puxado/desaparecido (nao aparece como "Failed"
#          em lado nenhum, simplesmente desaparece da tabela);
#       b) detetar avisos preditivos (contadores a subir no MESMO disco,
#          confirmado pelo serial, mesmo que o estado continue "Onln").
#     So e atualizada quando tudo esta 100% ok (ver fundo do script) --
#     fica congelada enquanto o problema nao for resolvido, para nao
#     "esquecer" um incidente a meio.
BASELINE_FILE="${STATE_DIR}/pd_baseline"

# --- Historico local de alertas (CSV), independente da retencao do
#     journalctl -- um registo por incidente/lembrete/resolucao, util para
#     relatorios ao cliente ---
HISTORY_DIR="${HISTORY_DIR:-/var/log/perc-monitor}"
HISTORY_FILE="${HISTORY_DIR}/alert_history.csv"

LOG_TAG="perc-monitor"
HOST="$(hostname)"

mkdir -p "$STATE_DIR" 2>/dev/null || true
mkdir -p "$HISTORY_DIR" 2>/dev/null || true
# O historico pode conter nomes de cliente e numeros de serie -- nao deixar
# world-readable (best-effort; nao falha o script se o chmod nao for possivel)
chmod 750 "$HISTORY_DIR" 2>/dev/null || true

# ==========================================================================
# Funcoes auxiliares
# ==========================================================================

is_uint() { [[ "$1" =~ ^[0-9]+$ ]]; }

# csv_escape: envolve em aspas duplas e duplica aspas internas, para o
# campo ficar valido em CSV mesmo que contenha virgulas ou aspas.
csv_escape() {
    printf '"%s"' "$(printf '%s' "$1" | tr '\n' ' ' | sed 's/"/""/g')"
}

log_history() {
    local event="$1" summary="$2" ts_iso ts_epoch
    ts_iso=$(date '+%Y-%m-%d %H:%M:%S')
    ts_epoch=$(date +%s)
    if [ ! -f "$HISTORY_FILE" ]; then
        echo "timestamp_iso,timestamp_epoch,client,host,event,summary" > "$HISTORY_FILE" 2>/dev/null || true
        chmod 640 "$HISTORY_FILE" 2>/dev/null || true
    fi
    printf '%s,%s,%s,%s,%s,%s\n' \
        "$(csv_escape "$ts_iso")" "$ts_epoch" \
        "$(csv_escape "$CLIENT_NAME")" "$(csv_escape "$HOST")" \
        "$(csv_escape "$event")" "$(csv_escape "$summary")" \
        >> "$HISTORY_FILE" 2>/dev/null || true
}

# html_escape: escapa &, < e > para o texto poder ir dentro de HTML em
# seguranca (o output do perccli e nomes de cliente/host nao deviam ter
# nada disto, mas mais vale nao arriscar quebrar o markup do email).
html_escape() {
    printf '%s' "$1" | sed -e 's/&/\&amp;/g' -e 's/</\&lt;/g' -e 's/>/\&gt;/g'
}

html_kv() {
    printf '<p style="margin:0 0 4px 0;"><strong>%s:</strong> %s</p>\n' "$(html_escape "$1")" "$(html_escape "$2")"
}

# html_section: um bloco de email com titulo + caixa com borda colorida,
# conteudo em monospace (para as tabelas do perccli ficarem legiveis).
html_section() {
    local title="$1" color="$2" content="$3" esc
    esc=$(html_escape "$content")
    printf '<div style="margin:16px 0 0 0;">'
    printf '<div style="font-size:12px;font-weight:700;color:#334155;text-transform:uppercase;letter-spacing:.4px;margin-bottom:6px;">%s</div>' "$(html_escape "$title")"
    printf '<pre style="margin:0;padding:10px 12px;background:#f8fafc;border-left:3px solid %s;border-radius:0 4px 4px 0;font-family:"Courier New",monospace;font-size:12px;line-height:1.45;color:#1e293b;white-space:pre-wrap;word-break:break-word;">%s</pre>' \
        "$color" "$esc"
    printf '</div>'
}

# build_email_html: envolve o corpo (ja em HTML) no template de email da
# CorTesTech -- tabelas + estilos inline, sem CSS externo, para ficar
# compativel com o maior numero de clientes de email possivel (Gmail,
# Outlook, etc.). $1=severidade (critica|preditiva|resolvida|teste)
build_email_html() {
    local severity="$1" title="$2" body="$3"
    local accent label
    case "$severity" in
        critica)   accent="#dc2626"; label="ALERTA" ;;
        preditiva) accent="#d97706"; label="AVISO PREDITIVO" ;;
        resolvida) accent="#16a34a"; label="RESOLVIDO" ;;
        teste)     accent="#64748b"; label="TESTE" ;;
        *)         accent="#64748b"; label="INFO" ;;
    esac
    cat <<HTMLEOF
<!DOCTYPE html>
<html lang="pt">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1"></head>
<body style="margin:0;padding:0;background-color:#f1f5f9;font-family:Arial,Helvetica,sans-serif;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#f1f5f9;padding:24px 0;">
<tr><td align="center">
<table role="presentation" width="600" cellpadding="0" cellspacing="0" style="background-color:#ffffff;border-radius:8px;overflow:hidden;box-shadow:0 1px 3px rgba(0,0,0,.1);">
<tr><td style="background-color:#0f2942;padding:20px 28px;">
<span style="color:#ffffff;font-size:20px;font-weight:700;letter-spacing:.5px;">CORTESTECH SOLUTIONS</span>
<span style="color:#93a5b8;font-size:12px;display:block;margin-top:2px;">Monitorizacao de Infraestrutura</span>
</td></tr>
<tr><td style="padding:0;">
<div style="background-color:${accent};color:#ffffff;padding:10px 28px;font-size:13px;font-weight:700;letter-spacing:.5px;">${label} &mdash; $(html_escape "$title")</div>
</td></tr>
<tr><td style="padding:24px 28px;color:#1f2937;font-size:14px;line-height:1.5;">
${body}
</td></tr>
<tr><td style="padding:16px 28px;background-color:#f8fafc;color:#94a3b8;font-size:11px;border-top:1px solid #e2e8f0;">
Mensagem automatica gerada por monitor_perc_raid.sh em $(html_escape "$HOST"). Nao responder a este email.
</td></tr>
</table>
</td></tr>
</table>
</body>
</html>
HTMLEOF
}

# send_email: tenta enviar em HTML falando diretamente com o msmtp (para
# controlar o Content-Type). Se o binario msmtp nao estiver no PATH, cai
# para o comando `mail` tradicional com o corpo em texto simples (perde a
# formatacao, mas continua a chegar -- e o comportamento que ja tinhas).
send_email() {
    local subject="$1" html_body="$2" plain_body="$3"
    [ -n "$ALERT_EMAIL_TO" ] || return 0
    local msmtp_bin
    msmtp_bin=$(command -v msmtp 2>/dev/null || true)
    if [ -n "$msmtp_bin" ]; then
        {
            printf 'To: %s\n' "$ALERT_EMAIL_TO"
            printf 'From: %s <%s>\n' "$EMAIL_FROM_NAME" "$ALERT_EMAIL_FROM"
            printf 'Subject: %s\n' "$subject"
            printf 'MIME-Version: 1.0\n'
            printf 'Content-Type: text/html; charset=UTF-8\n'
            printf '\n'
            printf '%s\n' "$html_body"
        } | "$msmtp_bin" "$ALERT_EMAIL_TO"
    else
        echo "$plain_body" | mail -s "$subject" -r "$ALERT_EMAIL_FROM" "$ALERT_EMAIL_TO"
    fi
}

send_sms() {
    local body="$1"
    [ -n "$TWILIO_ACCOUNT_SID" ] || return 0
    # Mantem o SMS curto (1 segmento = ate 160 caracteres em GSM-7, sem
    # acentos); corta por seguranca para nunca passar disso.
    curl -s -X POST \
        "https://api.twilio.com/2010-04-01/Accounts/${TWILIO_ACCOUNT_SID}/Messages.json" \
        --data-urlencode "From=${TWILIO_FROM_NUMBER}" \
        --data-urlencode "To=${TWILIO_TO_NUMBER}" \
        --data-urlencode "Body=${body:0:155}" \
        -u "${TWILIO_ACCOUNT_SID}:${TWILIO_AUTH_TOKEN}" >/dev/null
}

# alert_problem: dispara SMS curto (so o essencial, sem acentos) + email
# HTML completo. $5 = severidade (critica|preditiva) -- so muda o prefixo
# do SMS ("RAID ALERTA" vs "RAID AVISO"), o resto e identico.
alert_problem() {
    local sms_summary="$1" email_subject="$2" email_html="$3" email_plain="$4" severity="${5:-critica}"
    local sms_tag="RAID ALERTA"
    [ "$severity" = "preditiva" ] && sms_tag="RAID AVISO"
    logger -t "$LOG_TAG" "$email_subject"
    send_email "$email_subject" "$email_html" "$email_plain"
    send_sms "[${CLIENT_NAME}] ${sms_tag}: ${sms_summary}. Detalhe no email/log."
}

alert_resolved() {
    local email_subject="$1" email_html="$2" email_plain="$3"
    logger -t "$LOG_TAG" "$email_subject"
    send_email "$email_subject" "$email_html" "$email_plain"
    send_sms "[${CLIENT_NAME}] RAID OK: recuperado, sem falhas ativas."
}

if [ "$FORCE_TEST" -eq 1 ]; then
    echo "Modo de teste (--test-alert): a enviar um alerta de teste, sem verificar o RAID nem mexer no estado guardado."
    TEST_PLAIN="Isto e um alerta de TESTE gerado manualmente com --test-alert em $(date '+%Y-%m-%d %H:%M:%S') no host ${HOST}.
Nao corresponde a nenhuma falha real no RAID -- serve so para confirmar que o email (HTML, com o template CorTesTech) e o SMS estao a chegar."
    TEST_BODY_HTML=$(
        html_kv "Cliente/Local" "$CLIENT_NAME"
        html_kv "Host" "$HOST"
        html_kv "Data" "$(date '+%Y-%m-%d %H:%M:%S')"
        printf '<p style="margin:16px 0 0 0;">Isto e um alerta de <strong>TESTE</strong> gerado manualmente com <code>--test-alert</code>. Nao corresponde a nenhuma falha real no RAID -- serve so para confirmar que o email (com este template) e o SMS estao a chegar.</p>'
    )
    TEST_HTML=$(build_email_html "teste" "alerta de teste do monitor PERC" "$TEST_BODY_HTML")
    alert_problem "TESTE (simulado, nao e uma falha real)" \
        "[TESTE] ${CLIENT_NAME}: alerta de teste do monitor PERC" \
        "$TEST_HTML" "$TEST_PLAIN" "critica"
    log_history "test" "Alerta de teste manual (--test-alert)"
    exit 0
fi

if [ ! -x "$PERCCLI" ]; then
    MSG="perccli64 nao encontrado (ou sem permissao de execucao) em: $PERCCLI
Confirma onde esta instalado (which perccli64 / find / -iname '*perccli*')
e ajusta com PERCCLI_BIN=/caminho/para/perccli64 $0"
    logger -t "$LOG_TAG" "$MSG"
    echo "ERRO: $MSG" >&2
    exit 1
fi

# --- Virtual Drives (arrays) ---
VD_OUTPUT=$("$PERCCLI" "$CTRL/vall" show 2>/dev/null)
VD_BAD=$(echo "$VD_OUTPUT" | awk -v ok="^($OK_VD_STATES)\$" \
    '$1 ~ /^[0-9]+\/[0-9]+$/ { if ($3 !~ ok) print }')

# ==========================================================================
# Discos fisicos: uma unica recolha detalhada por disco (SN + contadores de
# erro + flag S.M.A.R.T), reaproveitada para: deteção de estado mau,
# avisos preditivos e atualização da baseline -- evita chamar "show all"
# duas vezes pelo mesmo disco.
# ==========================================================================
PD_OUTPUT=$("$PERCCLI" "$CTRL/eall/sall" show 2>/dev/null)

declare -A PD_SN PD_STATE_MAP PD_MEDIA PD_OTHER PD_PRED PD_SMART PD_RAWLINE
PD_SLOT_LIST=()

while IFS= read -r line; do
    SLOT=$(echo "$line" | awk '$1 ~ /^[0-9]+:[0-9]+$/ {print $1}')
    [ -n "$SLOT" ] || continue
    STATE=$(echo "$line" | awk '{print $3}')
    PD_SLOT_LIST+=("$SLOT")
    PD_STATE_MAP["$SLOT"]="$STATE"
    PD_RAWLINE["$SLOT"]="$line"

    EID="${SLOT%%:*}"
    SLT="${SLOT##*:}"
    SHOWALL=$("$PERCCLI" "$CTRL/e${EID}/s${SLT}" show all 2>/dev/null)

    SN=$(echo "$SHOWALL" | awk -F'= ' '/^SN[[:space:]]*=/ {print $2; exit}' | sed 's/[[:space:]]*$//')
    MEDIA=$(echo "$SHOWALL" | grep -iE 'Media Error Count' | head -n1 | awk -F'=' '{print $2}' | tr -d '[:space:]')
    OTHER=$(echo "$SHOWALL" | grep -iE 'Other Error Count' | head -n1 | awk -F'=' '{print $2}' | tr -d '[:space:]')
    PRED=$(echo "$SHOWALL" | grep -iE 'Predictive Failure Count' | head -n1 | awk -F'=' '{print $2}' | tr -d '[:space:]')
    SMART=$(echo "$SHOWALL" | grep -iE 'flagged by drive' | head -n1 | awk -F'=' '{print $2}' | tr -d '[:space:]')

    PD_SN["$SLOT"]="${SN:-desconhecido}"
    PD_MEDIA["$SLOT"]="${MEDIA:-0}"
    PD_OTHER["$SLOT"]="${OTHER:-0}"
    PD_PRED["$SLOT"]="${PRED:-0}"
    PD_SMART["$SLOT"]="${SMART:-desconhecido}"
done <<< "$PD_OUTPUT"

# --- Discos com estado anormal (presentes, mas Failed/etc.) ---
PD_BAD=""
PD_BAD_DETAIL=""
for SLOT in "${PD_SLOT_LIST[@]}"; do
    STATE="${PD_STATE_MAP[$SLOT]}"
    if ! echo "$STATE" | grep -qE "^(${OK_PD_STATES})\$"; then
        PD_BAD="${PD_BAD}${PD_RAWLINE[$SLOT]}
"
        PD_BAD_DETAIL="${PD_BAD_DETAIL}  Slot ${SLOT} | Serial: ${PD_SN[$SLOT]} | ${PD_RAWLINE[$SLOT]}
"
    fi
done

# --- Le a baseline (fotografia da ultima verificacao 100% saudavel) ---
declare -A BASE_SN BASE_MEDIA BASE_OTHER BASE_PRED BASE_SMART BASE_RAWLINE
BASE_SLOT_LIST=()
if [ -f "$BASELINE_FILE" ]; then
    while IFS=$'\t' read -r B_SLOT B_SN B_MEDIA B_OTHER B_PRED B_SMART B_REST; do
        [ -n "$B_SLOT" ] || continue
        BASE_SLOT_LIST+=("$B_SLOT")
        BASE_SN["$B_SLOT"]="$B_SN"
        BASE_MEDIA["$B_SLOT"]="$B_MEDIA"
        BASE_OTHER["$B_SLOT"]="$B_OTHER"
        BASE_PRED["$B_SLOT"]="$B_PRED"
        BASE_SMART["$B_SLOT"]="$B_SMART"
        BASE_RAWLINE["$B_SLOT"]="$B_REST"
    done < "$BASELINE_FILE"
fi

# --- Discos que desapareceram por completo (baseline tinha, tabela atual
#     ja nao tem) -- cobre remocao fisica E falhas graves que tiram o
#     disco do barramento (do ponto de vista do perccli sao indistinguiveis) ---
PD_MISSING_DETAIL=""
for B_SLOT in "${BASE_SLOT_LIST[@]}"; do
    if [ -z "${PD_STATE_MAP[$B_SLOT]:-}" ]; then
        PD_MISSING_DETAIL="${PD_MISSING_DETAIL}  Slot ${B_SLOT} | Serial: ${BASE_SN[$B_SLOT]:-desconhecido} | JA NAO DETECTADO pela controladora (ultimo estado visto: ${BASE_RAWLINE[$B_SLOT]:-desconhecido})
"
    fi
done

# --- Avisos preditivos: discos presentes, com estado "OK", mas cujos
#     contadores de erro/SMART pioraram desde a ultima fotografia saudavel
#     DO MESMO disco (confirmado pelo serial -- se o serial mudou, o disco
#     foi substituido e nao faz sentido comparar contadores diferentes) ---
PD_PREDICTIVE_DETAIL=""
for SLOT in "${PD_SLOT_LIST[@]}"; do
    STATE="${PD_STATE_MAP[$SLOT]}"
    echo "$STATE" | grep -qE "^(${OK_PD_STATES})\$" || continue   # ja coberto por PD_BAD

    B_SN="${BASE_SN[$SLOT]:-}"
    CUR_SN="${PD_SN[$SLOT]:-desconhecido}"
    [ -n "$B_SN" ] || continue           # sem baseline para este slot -- primeira vez, nada para comparar
    [ "$B_SN" = "$CUR_SN" ] || continue  # serial mudou -- disco novo, ignora

    B_MEDIA="${BASE_MEDIA[$SLOT]:-}";  CUR_MEDIA="${PD_MEDIA[$SLOT]:-}"
    B_OTHER="${BASE_OTHER[$SLOT]:-}";  CUR_OTHER="${PD_OTHER[$SLOT]:-}"
    B_PRED="${BASE_PRED[$SLOT]:-}";    CUR_PRED="${PD_PRED[$SLOT]:-}"
    B_SMART="${BASE_SMART[$SLOT]:-}";  CUR_SMART="${PD_SMART[$SLOT]:-}"

    REASONS=""
    if is_uint "$B_MEDIA" && is_uint "$CUR_MEDIA" && [ "$((CUR_MEDIA - B_MEDIA))" -ge "$MEDIA_ERROR_DELTA_THRESHOLD" ] 2>/dev/null; then
        REASONS="${REASONS}Media Error Count: ${B_MEDIA} -> ${CUR_MEDIA} (+$((CUR_MEDIA - B_MEDIA))); "
    fi
    if is_uint "$B_OTHER" && is_uint "$CUR_OTHER" && [ "$((CUR_OTHER - B_OTHER))" -ge "$OTHER_ERROR_DELTA_THRESHOLD" ] 2>/dev/null; then
        REASONS="${REASONS}Other Error Count: ${B_OTHER} -> ${CUR_OTHER} (+$((CUR_OTHER - B_OTHER))); "
    fi
    if is_uint "$B_PRED" && is_uint "$CUR_PRED" && [ "$CUR_PRED" -gt "$B_PRED" ] 2>/dev/null; then
        REASONS="${REASONS}Predictive Failure Count: ${B_PRED} -> ${CUR_PRED}; "
    fi
    if [ "$CUR_SMART" = "Yes" ] && [ "$B_SMART" != "Yes" ]; then
        REASONS="${REASONS}S.M.A.R.T alert: ${B_SMART:-desconhecido} -> Yes; "
    fi

    if [ -n "$REASONS" ]; then
        PD_PREDICTIVE_DETAIL="${PD_PREDICTIVE_DETAIL}  Slot ${SLOT} | Serial: ${CUR_SN} | ${REASONS}
"
    fi
done

# --- BBU (bateria da controladora) ---
# Best-effort: le a tabela de "perccli64 /c0/bbu show", localiza a coluna
# "State" pelo cabecalho (nao por posicao fixa, porque a ordem das colunas
# pode variar por firmware) e verifica se bate com OK_BBU_STATES. Se o
# controlador nao tiver BBU, ou o comando falhar, ou o formato nao for
# reconhecido, NAO dispara alerta (evita falso positivo) -- so regista um
# aviso no log/systemd para investigares manualmente.
BBU_PRESENT=0
BBU_BAD=""
BBU_OUTPUT=$("$PERCCLI" "$CTRL/bbu" show 2>/dev/null)
if [ -n "$BBU_OUTPUT" ] && ! echo "$BBU_OUTPUT" | grep -qiE \
    'Status[[:space:]]*=[[:space:]]*Failure|no such device|not found|BBU Not Found|Battery Module Missing|no battery'; then
    if echo "$BBU_OUTPUT" | grep -qE '(^| )State( |$)'; then
        BBU_PRESENT=1
        BBU_STATE=$(echo "$BBU_OUTPUT" | awk '
            found && NF>0 { print $stateCol; exit }
            !found {
                for (i=1;i<=NF;i++) if ($i=="State") { stateCol=i; found=1 }
            }
        ')
        if [ -n "${BBU_STATE:-}" ] && ! echo "$BBU_STATE" | grep -qE "^(${OK_BBU_STATES})\$"; then
            BBU_BAD="BBU (bateria da controladora) em estado: ${BBU_STATE}"
        fi
    else
        logger -t "$LOG_TAG" "Aviso: nao encontrei a coluna 'State' em '${PERCCLI} ${CTRL}/bbu show' -- confirma o formato manualmente (BBU nao esta a ser verificada)"
    fi
fi

# --- Ler o ultimo estado guardado (para nao repetir alerta a cada verificacao) ---
PREV_STATE="ok"
LAST_ALERT_EPOCH=0
if [ -f "$STATE_FILE" ]; then
    PREV_STATE=$(sed -n '1p' "$STATE_FILE" 2>/dev/null)
    LAST_ALERT_EPOCH=$(sed -n '2p' "$STATE_FILE" 2>/dev/null)
    [ -n "$PREV_STATE" ] || PREV_STATE="ok"
    [[ "$LAST_ALERT_EPOCH" =~ ^[0-9]+$ ]] || LAST_ALERT_EPOCH=0
fi
NOW_EPOCH=$(date +%s)

save_state() {
    printf '%s\n%s\n' "$1" "$2" > "$STATE_FILE" 2>/dev/null || true
}

# --- Severidade: "critica" (VD/disco mau/desaparecido/BBU -- problema real,
#     agir agora) vs "preditiva" (so contadores/SMART a subir -- planear
#     substituicao, ainda nao e urgente) ---
HARD_BAD=0
if [ -n "$VD_BAD" ] || [ -n "$PD_BAD" ] || [ -n "$PD_MISSING_DETAIL" ] || [ -n "$BBU_BAD" ]; then
    HARD_BAD=1
fi
OVERALL_BAD=0
if [ "$HARD_BAD" -eq 1 ] || [ -n "$PD_PREDICTIVE_DETAIL" ]; then
    OVERALL_BAD=1
fi
if [ "$HARD_BAD" -eq 1 ]; then
    SEVERITY="critica"; SUBJ_TAG="[RAID ALERTA]"; SUBJ_DESC="possivel falha em disco/array (PERC)"
else
    SEVERITY="preditiva"; SUBJ_TAG="[RAID AVISO]"; SUBJ_DESC="aviso preditivo em disco (SMART/contagem de erros)"
fi

# Resumo curto para o SMS (sem acentos, uma linha, so o essencial -- o
# detalhe completo dos contadores fica no email) -- prioridade: disco
# presente mas com estado mau > disco desaparecido > array > preditivo > BBU
build_sms_summary() {
    if [ -n "$PD_BAD" ]; then
        local slot state sn
        slot=$(echo "$PD_BAD" | head -n1 | awk '{print $1}')
        state=$(echo "$PD_BAD" | head -n1 | awk '{print $3}')
        sn=$(echo "$PD_BAD_DETAIL" | head -n1 | sed -n 's/.*Serial: \([^ |]*\).*/\1/p')
        echo "Disco ${slot} ${state} SN:${sn:-desconhecido}"
    elif [ -n "$PD_MISSING_DETAIL" ]; then
        local slot sn
        slot=$(echo "$PD_MISSING_DETAIL" | head -n1 | sed -n 's/^  Slot \([^ ]*\).*/\1/p')
        sn=$(echo "$PD_MISSING_DETAIL" | head -n1 | sed -n 's/.*Serial: \([^ |]*\).*/\1/p')
        echo "Disco ${slot} desaparecido SN:${sn:-desconhecido}"
    elif [ -n "$VD_BAD" ]; then
        local vd state
        vd=$(echo "$VD_BAD" | head -n1 | awk '{print $1}')
        state=$(echo "$VD_BAD" | head -n1 | awk '{print $3}')
        echo "Array ${vd} estado ${state}"
    elif [ -n "$PD_PREDICTIVE_DETAIL" ]; then
        local slot sn
        slot=$(echo "$PD_PREDICTIVE_DETAIL" | head -n1 | sed -n 's/^  Slot \([^ ]*\).*/\1/p')
        sn=$(echo "$PD_PREDICTIVE_DETAIL" | head -n1 | sed -n 's/.*Serial: \([^ |]*\).*/\1/p')
        echo "Disco ${slot} aviso preditivo SN:${sn:-desconhecido}"
    else
        echo "${BBU_BAD:-Problema desconhecido}"
    fi
}

if [ "$OVERALL_BAD" -eq 1 ]; then
    MSG="Cliente/Local: ${CLIENT_NAME}
Host: ${HOST}
Data: $(date '+%Y-%m-%d %H:%M:%S')
Severidade: ${SEVERITY}

Virtual Drives com estado anormal (coluna State != ${OK_VD_STATES}):
${VD_BAD:-  (nenhuma)}

Discos fisicos com estado anormal (coluna State fora de: ${OK_PD_STATES}):
${PD_BAD_DETAIL:-  (nenhum)}

Discos que desapareceram da controladora desde a ultima verificacao saudavel
(removidos fisicamente, ou falha grave que os tirou do barramento):
${PD_MISSING_DETAIL:-  (nenhum)}

Avisos preditivos (contadores de erro/SMART a subir num disco que continua
'OK' no estado, mas pode estar a preparar uma falha):
${PD_PREDICTIVE_DETAIL:-  (nenhum)}

BBU (bateria da controladora):
$([ "$BBU_PRESENT" -eq 1 ] && echo "  ${BBU_BAD:-OK (${OK_BBU_STATES})}" || echo "  (nao detectada / nao verificada -- ver log para detalhe)")

Correr manualmente para mais detalhe:
  perccli64 /c0/vall show
  perccli64 /c0/eall/sall show all
  perccli64 /c0/bbu show
"
    echo "$MSG"

    EMAIL_BODY_HTML=$(
        html_kv "Cliente/Local" "$CLIENT_NAME"
        html_kv "Host" "$HOST"
        html_kv "Data" "$(date '+%Y-%m-%d %H:%M:%S')"
        html_kv "Severidade" "$SEVERITY"
        html_section "Virtual Drives com estado anormal (State != ${OK_VD_STATES})" "#dc2626" "${VD_BAD:-  (nenhuma)}"
        html_section "Discos fisicos com estado anormal (State fora de: ${OK_PD_STATES})" "#dc2626" "${PD_BAD_DETAIL:-  (nenhum)}"
        html_section "Discos desaparecidos (removidos ou falha grave no barramento)" "#dc2626" "${PD_MISSING_DETAIL:-  (nenhum)}"
        html_section "Avisos preditivos (SMART / contagem de erros a subir)" "#d97706" "${PD_PREDICTIVE_DETAIL:-  (nenhum)}"
        html_section "BBU (bateria da controladora)" "#d97706" "$([ "$BBU_PRESENT" -eq 1 ] && echo "${BBU_BAD:-OK (${OK_BBU_STATES})}" || echo "(nao detectada / nao verificada)")"
        printf '<p style="margin:16px 0 0 0;font-size:12px;color:#64748b;">Correr manualmente para mais detalhe: <code>perccli64 %s/vall show</code> &middot; <code>perccli64 %s/eall/sall show all</code> &middot; <code>perccli64 %s/bbu show</code></p>' \
            "$(html_escape "$CTRL")" "$(html_escape "$CTRL")" "$(html_escape "$CTRL")"
    )
    EMAIL_HTML=$(build_email_html "$SEVERITY" "$SUBJ_DESC" "$EMAIL_BODY_HTML")

    if [ "$PREV_STATE" != "bad" ]; then
        # Problema novo -- alerta sempre
        SUMMARY="$(build_sms_summary)"
        alert_problem "$SUMMARY" "${SUBJ_TAG} ${CLIENT_NAME}: ${SUBJ_DESC}" "$EMAIL_HTML" "$MSG" "$SEVERITY"
        log_history "problem_new" "$SUMMARY"
        save_state "bad" "$NOW_EPOCH"
    elif [ $((NOW_EPOCH - LAST_ALERT_EPOCH)) -ge "$REMIND_SECONDS" ]; then
        # Ja tinha alertado, mas passou o intervalo de lembrete -- relembra
        SUMMARY="$(build_sms_summary) (ainda por resolver)"
        alert_problem "$SUMMARY" "${SUBJ_TAG} - continua ${CLIENT_NAME}: ${SUBJ_DESC}" "$EMAIL_HTML" "$MSG" "$SEVERITY"
        log_history "problem_reminder" "$SUMMARY"
        save_state "bad" "$NOW_EPOCH"
    else
        # Ja tinha alertado ha pouco tempo -- so regista no log, sem SMS/email
        logger -t "$LOG_TAG" "RAID continua mau (${CLIENT_NAME}), sem novo alerta (proximo lembrete em ate ${REMIND_HOURS}h)"
        save_state "bad" "$LAST_ALERT_EPOCH"
    fi
    exit 2
fi

# --- RAID normal ---
if [ "$PREV_STATE" = "bad" ]; then
    RESOLVED_PLAIN="Cliente/Local: ${CLIENT_NAME}
Host: ${HOST}
Data: $(date '+%Y-%m-%d %H:%M:%S')

O RAID voltou ao estado normal (todas as Virtual Drives em ${OK_VD_STATES}, todos os discos fisicos em ${OK_PD_STATES}$([ "$BBU_PRESENT" -eq 1 ] && echo ", BBU em ${OK_BBU_STATES}"), sem avisos preditivos ativos)."
    RESOLVED_BODY_HTML=$(
        html_kv "Cliente/Local" "$CLIENT_NAME"
        html_kv "Host" "$HOST"
        html_kv "Data" "$(date '+%Y-%m-%d %H:%M:%S')"
        printf '<p style="margin:16px 0 0 0;">O RAID voltou ao estado normal: todas as Virtual Drives em <strong>%s</strong>, todos os discos fisicos em <strong>%s</strong>%s, sem avisos preditivos ativos.</p>' \
            "$(html_escape "$OK_VD_STATES")" "$(html_escape "$OK_PD_STATES")" \
            "$([ "$BBU_PRESENT" -eq 1 ] && echo ", BBU em <strong>$(html_escape "$OK_BBU_STATES")</strong>" || echo "")"
    )
    RESOLVED_HTML=$(build_email_html "resolvida" "RAID recuperado" "$RESOLVED_BODY_HTML")
    alert_resolved "[RAID OK] ${CLIENT_NAME}: RAID recuperado" "$RESOLVED_HTML" "$RESOLVED_PLAIN"
    log_history "resolved" "RAID recuperado"
fi
save_state "ok" "0"

# Atualiza a "baseline" de discos (slot + serial + contadores) agora que
# tudo esta saudavel -- reaproveita os dados ja recolhidos no topo do
# script (nao faz novas chamadas ao perccli). E o que permite, num proximo
# incidente, apontar exatamente qual baia/serial desapareceu ou comecou a
# acumular erros. So corre aqui (RAID 100% ok) para nao "esquecer" um
# problema a meio de um incidente ainda por resolver.
if [ "${#PD_SLOT_LIST[@]}" -gt 0 ]; then
    BASELINE_TMP="${BASELINE_FILE}.tmp.$$"
    : > "$BASELINE_TMP" 2>/dev/null
    for SLOT in "${PD_SLOT_LIST[@]}"; do
        printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
            "$SLOT" "${PD_SN[$SLOT]:-desconhecido}" \
            "${PD_MEDIA[$SLOT]:-0}" "${PD_OTHER[$SLOT]:-0}" "${PD_PRED[$SLOT]:-0}" "${PD_SMART[$SLOT]:-desconhecido}" \
            "${PD_RAWLINE[$SLOT]:-}" >> "$BASELINE_TMP" 2>/dev/null
    done
    mv -f "$BASELINE_TMP" "$BASELINE_FILE" 2>/dev/null || rm -f "$BASELINE_TMP" 2>/dev/null
    chmod 640 "$BASELINE_FILE" 2>/dev/null || true
fi

if [ "$VERBOSE" -eq 1 ]; then
    echo "OK: todas as Virtual Drives em (${OK_VD_STATES}) e todos os discos fisicos em (${OK_PD_STATES})."
    echo
    echo "Virtual Drives:"
    echo "$VD_OUTPUT"
    echo
    echo "Discos fisicos:"
    echo "$PD_OUTPUT"
    echo
    echo "Detalhe recolhido por disco (para validar os campos de aviso preditivo):"
    for SLOT in "${PD_SLOT_LIST[@]}"; do
        echo "  Slot ${SLOT} | SN:${PD_SN[$SLOT]} | State:${PD_STATE_MAP[$SLOT]} | MediaErr:${PD_MEDIA[$SLOT]} | OtherErr:${PD_OTHER[$SLOT]} | PredFail:${PD_PRED[$SLOT]} | SMART:${PD_SMART[$SLOT]}"
    done
    echo
    if [ "$BBU_PRESENT" -eq 1 ]; then
        echo "BBU: OK (estado dentro de ${OK_BBU_STATES})"
    else
        echo "BBU: nao detectada, ou formato de '${CTRL}/bbu show' nao reconhecido (ver journalctl -u perc-monitor.service para aviso)"
    fi
    echo
    echo "Saida completa de '${CTRL}/bbu show':"
    echo "${BBU_OUTPUT:-  (sem saida)}"
fi

exit 0
MONITOR_SCRIPT_EOF
chmod +x "$SCRIPTS_DIR/monitor_perc_raid.sh"

log "A escrever send_sms.sh em $SCRIPTS_DIR ..."
cat > "$SCRIPTS_DIR/send_sms.sh" <<'SMS_SCRIPT_EOF'
#!/usr/bin/env bash
#
# send_sms.sh — envia um SMS através da API do Twilio.
#
# ------------------------------------------------------------------------
# UTILIZAÇÃO
# ------------------------------------------------------------------------
#   ./send_sms.sh "+351912345678" "Mensagem de teste"
#
# As credenciais Twilio não vão como argumento (para não ficarem visíveis
# em `ps aux` ou no histórico da shell) — são lidas de variáveis de
# ambiente. O script carrega-as AUTOMATICAMENTE, por esta ordem de
# prioridade (a primeira fonte que definir uma variável vence; não é
# preciso fazer `source` manual):
#
#   1. Variáveis já exportadas na sessão/serviço que chama o script.
#   2. O ficheiro apontado por $TWILIO_ENV_FILE, se essa variável existir.
#   3. ./twilio.env (no diretório atual), se existir.
#   4. /etc/default/twilio, se existir.
#
# Ficheiro recomendado (chmod 600, root:root, para ninguém mais o ler):
#
#   /etc/default/twilio
#     TWILIO_ACCOUNT_SID="ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
#     TWILIO_AUTH_TOKEN="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
#     TWILIO_FROM_NUMBER="+1XXXXXXXXXX"
#     TWILIO_TO_NUMBER="+351912345678"   # opcional, número por omissão
#
# Com o ficheiro em /etc/default/twilio já criado, basta:
#
#   ./send_sms.sh "+351912345678" "Teste"
#
# NÃO TENS NÚMERO TWILIO? Não é obrigatório. A Twilio permite usar um
# "Alphanumeric Sender ID" — um texto (até 11 caracteres, letras/números,
# sem espaços) em vez de um número — em muitos países, Portugal incluído,
# sem registo prévio. Basta pores esse texto em TWILIO_FROM_NUMBER:
#
#     TWILIO_FROM_NUMBER="CORTESTECH"
#
# Limitação: é só de saída — quem recebe não consegue responder ao SMS.
# Não funciona para destinos nos EUA/Canadá (aí exige-se sempre um número).
#
# ------------------------------------------------------------------------
# REQUISITOS
# ------------------------------------------------------------------------
#   - curl instalado
#   - (opcional) jq instalado, só para o output do erro/sucesso ficar
#     mais legível — o script funciona na mesma sem ele.
#
# ------------------------------------------------------------------------
# CÓDIGOS DE SAÍDA
# ------------------------------------------------------------------------
#   0 = SMS aceite pela Twilio para envio
#   1 = erro de utilização / configuração (faltam credenciais, argumentos)
#   2 = a Twilio recusou o pedido (número inválido, saldo insuficiente,
#       credenciais erradas, etc.) — o corpo da resposta é mostrado
#
set -uo pipefail

# --- Carrega credenciais de um ficheiro, só se ainda não vierem do ambiente ---
if [ -z "${TWILIO_ACCOUNT_SID:-}" ]; then
    for _candidate in "${TWILIO_ENV_FILE:-}" "./twilio.env" "/etc/default/twilio"; do
        if [ -n "$_candidate" ] && [ -r "$_candidate" ]; then
            # shellcheck disable=SC1090
            source "$_candidate"
            break
        fi
    done
    unset _candidate
fi

usage() {
    echo "Uso: $0 <numero_destino_E.164> <mensagem>" >&2
    echo "  ex: $0 \"+351912345678\" \"Ola, isto e um teste\"" >&2
    exit 1
}

# --- Credenciais (variáveis de ambiente) ---
: "${TWILIO_ACCOUNT_SID:?Falta definir TWILIO_ACCOUNT_SID}"
: "${TWILIO_AUTH_TOKEN:?Falta definir TWILIO_AUTH_TOKEN}"
: "${TWILIO_FROM_NUMBER:?Falta definir TWILIO_FROM_NUMBER (numero Twilio +1XXXXXXXXXX, ou um Alphanumeric Sender ID tipo CORTESTECH)}"

# --- Argumentos ---
TO_NUMBER="${1:-${TWILIO_TO_NUMBER:-}}"
MESSAGE="${2:-}"

if [ -z "$TO_NUMBER" ] || [ -z "$MESSAGE" ]; then
    usage
fi

# Validação simples do formato E.164 (+ seguido de 8 a 15 dígitos)
if ! [[ "$TO_NUMBER" =~ ^\+[0-9]{8,15}$ ]]; then
    echo "Erro: numero de destino '$TO_NUMBER' nao parece estar em formato E.164 (ex: +351912345678)" >&2
    exit 1
fi

# O FROM tanto pode ser um numero Twilio (E.164) como um Alphanumeric
# Sender ID (texto, max 11 caracteres, so letras/numeros, sem comecar por
# digito nem conter so digitos)
if [[ "$TWILIO_FROM_NUMBER" =~ ^\+[0-9]{8,15}$ ]]; then
    : # numero valido
elif [[ "$TWILIO_FROM_NUMBER" =~ ^[A-Za-z][A-Za-z0-9]{0,10}$ ]]; then
    : # alphanumeric sender id valido (max 11 chars, comeca por letra)
else
    echo "Erro: TWILIO_FROM_NUMBER='$TWILIO_FROM_NUMBER' invalido." >&2
    echo "  Tem de ser um numero E.164 (ex: +1XXXXXXXXXX) ou um Alphanumeric" >&2
    echo "  Sender ID (ex: CORTESTECH) com no maximo 11 caracteres, so letras/numeros," >&2
    echo "  a comecar por uma letra." >&2
    exit 1
fi

# A Twilio limita SMS a 1600 caracteres por mensagem; corta por segurança
MESSAGE="${MESSAGE:0:1500}"

RESPONSE=$(curl -s -w '\n%{http_code}' -X POST \
    "https://api.twilio.com/2010-04-01/Accounts/${TWILIO_ACCOUNT_SID}/Messages.json" \
    --data-urlencode "From=${TWILIO_FROM_NUMBER}" \
    --data-urlencode "To=${TO_NUMBER}" \
    --data-urlencode "Body=${MESSAGE}" \
    -u "${TWILIO_ACCOUNT_SID}:${TWILIO_AUTH_TOKEN}")

HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
BODY=$(echo "$RESPONSE" | sed '$d')

if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 300 ]; then
    if command -v jq >/dev/null 2>&1; then
        SID=$(echo "$BODY" | jq -r '.sid // empty')
        STATUS=$(echo "$BODY" | jq -r '.status // empty')
        echo "SMS aceite pela Twilio. sid=${SID} status=${STATUS}"
    else
        echo "SMS aceite pela Twilio (HTTP $HTTP_CODE)."
    fi
    exit 0
else
    echo "Falha ao enviar SMS (HTTP $HTTP_CODE):" >&2
    if command -v jq >/dev/null 2>&1; then
        echo "$BODY" | jq -r '"  " + (.message // "erro desconhecido") + " (code=" + ((.code // 0)|tostring) + ")"' >&2
    else
        echo "$BODY" >&2
    fi
    exit 2
fi
SMS_SCRIPT_EOF
chmod +x "$SCRIPTS_DIR/send_sms.sh"

# ==========================================================================
# 4. Rotação de log do msmtp
# ==========================================================================
if [ -f "$LOGROTATE_FILE" ]; then
    log "$LOGROTATE_FILE já existe — não vou tocar."
else
    log "A instalar rotação de log do msmtp em $LOGROTATE_FILE ..."
    cat > "$LOGROTATE_FILE" <<'LOGROTATE_EOF'
# Ver /root/scripts/../HANDOVER para contexto (AppArmor bloqueia
# /var/log/msmtp.log nalguns hosts, por isso o log fica em ~/.msmtp.log).
# Se este host tiver o log noutro caminho, ajusta antes de confiar nisto.
/root/.msmtp.log {
    weekly
    rotate 8
    missingok
    notifempty
    compress
    delaycompress
    copytruncate
    su root root
}
LOGROTATE_EOF
    log "Criado $LOGROTATE_FILE (assume log em /root/.msmtp.log — ajusta se for diferente neste host)."
fi

# ==========================================================================
# 5. Diretório de histórico de alertas (CSV)
# ==========================================================================
mkdir -p "$HISTORY_DIR"
chmod 750 "$HISTORY_DIR" 2>/dev/null || true
log "Diretório de histórico de alertas: $HISTORY_DIR (criado agora pelo script na 1ª execução, se ainda não existir)."

# ==========================================================================
# 6. Ficheiro de credenciais (nunca sobrescreve um já existente)
# ==========================================================================
if [ -f "$CRED_FILE" ]; then
    log "$CRED_FILE já existe — não vou tocar (evita apagar credenciais já configuradas)."
else
    cat > "$CRED_FILE" <<'CRED_EOF'
# Preenche com os dados REAIS deste cliente antes de confiar nos alertas.
CLIENT_NAME=""

ALERT_EMAIL_TO=""
ALERT_EMAIL_FROM="proxmox-alert@localhost"

TWILIO_ACCOUNT_SID=""
TWILIO_AUTH_TOKEN=""
TWILIO_FROM_NUMBER=""
TWILIO_TO_NUMBER=""

# Opcional: de quantas em quantas horas relembrar enquanto o problema nao
# for resolvido (default 4 se nao definires nada aqui)
#REMIND_HOURS=4
CRED_EOF
    chmod 600 "$CRED_FILE"
    log "Criado $CRED_FILE com placeholders vazios — FALTA EDITAR (inclui CLIENT_NAME, que antes só existia no host pve01)."
fi

# ==========================================================================
# 7. systemd (service + timer)
# ==========================================================================
log "A criar o serviço e o timer systemd..."
cat > /etc/systemd/system/perc-monitor.service <<EOF
[Unit]
Description=Verifica estado do RAID PERC

[Service]
Type=oneshot
EnvironmentFile=-${CRED_FILE}
ExecStart=${SCRIPTS_DIR}/monitor_perc_raid.sh
EOF

cat > /etc/systemd/system/perc-monitor.timer <<'EOF'
[Unit]
Description=Corre a verificacao do RAID PERC a cada 10 minutos

[Timer]
OnBootSec=2min
OnUnitActiveSec=10min

[Install]
WantedBy=timers.target
EOF

systemctl daemon-reload
systemctl enable --now perc-monitor.timer

# ==========================================================================
# 8. Teste final
# ==========================================================================
log "A testar a ligação à controladora..."
if [ -x "$PERCCLI_DEST" ] && "$PERCCLI_DEST" /c0 show >/dev/null 2>&1; then
    log "Controlador detectado com sucesso."
else
    err "Não consegui falar com a controladora via '$PERCCLI_DEST /c0 show' — confirma manualmente."
fi

echo
log "----------------------------------------------------------------"
log "INSTALAÇÃO CONCLUÍDA."
log "Scripts em:      $SCRIPTS_DIR"
log "Config msmtp em: $MSMTPRC_FILE  (email + password de app do Gmail)"
log "Credenciais em:  $CRED_FILE  (edita com os dados reais deste cliente, incluindo CLIENT_NAME)"
log "Timer systemd:   perc-monitor.timer (a cada 10 min)"
log "Historico CSV:   $HISTORY_DIR/alert_history.csv (criado na 1a execucao)"
log "Logrotate:       $LOGROTATE_FILE (confirma o caminho do log do msmtp neste host)"
log ""

FALTA_ALGO=0
if grep -q "PREENCHER" "$MSMTPRC_FILE" 2>/dev/null; then
    log "!!! FALTA: $MSMTPRC_FILE ainda tem placeholders (email/password de app por preencher) !!!"
    FALTA_ALGO=1
fi
if grep -qE '^(CLIENT_NAME|ALERT_EMAIL_TO)=""$' "$CRED_FILE" 2>/dev/null; then
    log "!!! FALTA: $CRED_FILE ainda tem placeholders (CLIENT_NAME/email/Twilio por preencher) !!!"
    FALTA_ALGO=1
fi
[ "$FALTA_ALGO" -eq 1 ] && log ""

log "Passos que faltam:"
log "  1. nano $MSMTPRC_FILE                     # preencher email + password de app do Gmail"
log "  2. nano $CRED_FILE                        # preencher CLIENT_NAME, email/Twilio reais"
log "  3. $SCRIPTS_DIR/monitor_perc_raid.sh -v    # testar e confirmar formato de tabela (VD/PD/BBU)"
log "  4. $SCRIPTS_DIR/monitor_perc_raid.sh --test-alert   # confirmar que email/SMS chegam"
log "----------------------------------------------------------------"
