#!/usr/bin/env bash
#
# pitr-drill.sh — ensaio de **PITR** (recuperação pontual) do PostgreSQL (ADR-042 / F6.I2.T2).
#
# Prova que o cluster é recuperável até um **instante** (não só até o dump):
#   1. cria uma base de ensaio com uma tabela marcadora (`base` → `after` → `late`);
#   2. tira um **base backup** (`pg_basebackup`) e força o arquivamento do WAL (`pg_switch_wal`);
#   3. escreve `late` **depois** do alvo e restaura o backup com `recovery_target_lsn` no meio;
#   4. valida que `base`/`after` existem e `late` **não** (a recuperação parou onde devia) e mede o RTO.
#
# Roda inteiro **dentro** do container `postgres` (mesmo filesystem/usuário): nada toca a base de
# produção e nenhum volume extra é necessário. A base de ensaio é removida no fim.
#
# Uso:
#   tools/pitr-drill.sh            # ensaio completo, limpando ao final
#   tools/pitr-drill.sh --keep     # mantém diretórios/log para inspeção
#
set -Eeuo pipefail

ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT"

KEEP=0
[ "${1:-}" = "--keep" ] && KEEP=1

[ -f .env ] || { echo "'.env' ausente." >&2; exit 2; }

env_value() {
    local key="$1" fallback="$2" value
    value="$(grep -E "^${key}=" .env 2>/dev/null | tail -1 | cut -d= -f2- | tr -d "\"' ")"
    printf '%s' "${value:-$fallback}"
}

DB_USER="$(env_value DB_USERNAME concordia)"
PROJECT="$(bash "$ROOT/tools/project-name.sh" 2>/dev/null)"
# shellcheck source=tools/runtime.sh
. "$ROOT/tools/runtime.sh"
COMPOSE=("${ENGINE[@]}" compose -p "$PROJECT" --env-file .env "${COMPOSE_FILES[@]}")
dc() { "${COMPOSE[@]}" exec -T "$@"; }

STAMP="$(date -u +%Y%m%dT%H%M%SZ)"
DRILL_DB="concordia_pitr_$(printf '%s' "$STAMP" | tr '[:upper:]' '[:lower:]')"
DRILL_ROOT="/tmp/pitr-${STAMP}"
PORT=5433
PGDATA_DIR="$(dc postgres sh -c 'printf %s "${PGDATA:-/var/lib/postgresql/pgdata}"')"
[ -n "$PGDATA_DIR" ] || { echo "FALHA: PGDATA vazio no container postgres." >&2; exit 1; }

# O container roda como root; o PostgreSQL recusa rodar como root → `su postgres`.
DC_ENV=(-e "DRILL_DB=${DRILL_DB}" -e "DRILL_ROOT=${DRILL_ROOT}" -e "PGUSER_=${DB_USER}" -e "PORT_=${PORT}")

echo "==> arquivamento:"
dc postgres psql -U "$DB_USER" -d postgres -tAc "show archive_mode" | sed 's/^/    archive_mode = /'
mode="$(dc postgres psql -U "$DB_USER" -d postgres -tAc "show archive_mode" | tr -d '[:space:]')"
[ "$mode" = "on" ] || { echo "FALHA: archive_mode=off (ver docker/postgres/postgresql.conf)." >&2; exit 1; }

echo "==> base de ensaio: ${DRILL_DB} · diretório: ${DRILL_ROOT}"
started=$(date +%s)
dc -e "DRILL_DB=${DRILL_DB}" -e "DRILL_ROOT=${DRILL_ROOT}" -e "PGUSER_=${DB_USER}" -e "PORT_=${PORT}" -e "PGDATA_=${PGDATA_DIR}" -e "KEEP_=${KEEP}" postgres \
    bash -s <<'EOS'
set -Eeuo pipefail
export PGPASSWORD="${PGPASSWORD:-}"

psql_primary() { psql -v ON_ERROR_STOP=1 -U "$PGUSER_" -d concordia -tA "$@"; }
psql_scratch() { psql -v ON_ERROR_STOP=1 -U "$PGUSER_" -d "$DRILL_DB" -tA "$@"; }

# O container roda como root; o postgres não roda como root.
run_as_pg() { su postgres -c "$1"; }

cleanup() {
    run_as_pg "pg_ctl -D ${DRILL_ROOT}/data -m immediate stop" >/dev/null 2>&1 || true
    psql_primary -c "DROP DATABASE IF EXISTS \"${DRILL_DB}\" WITH (FORCE);" >/dev/null 2>&1 || true
    rm -rf "$DRILL_ROOT" >/dev/null 2>&1 || true
}

# Ensaio interrompido (falha em qualquer passo) não deixa base/diretório para trás.
# `--keep` é diagnóstico: só limpa quando o chamador não pediu para inspecionar.
trap 'rc=$?; if [ "$rc" -ne 0 ] && [ "${KEEP_:-0}" = "0" ]; then cleanup; fi' EXIT

psql_primary -c "CREATE DATABASE \"${DRILL_DB}\";" >/dev/null
psql_scratch -c "CREATE TABLE marker (tag text PRIMARY KEY, at timestamptz NOT NULL DEFAULT now());" >/dev/null
psql_scratch -c "INSERT INTO marker (tag) VALUES ('base');" >/dev/null

install -d -o postgres -g postgres -m 750 "${DRILL_ROOT}"
install -d -o postgres -g postgres -m 700 "${DRILL_ROOT}/data"
run_as_pg "pg_basebackup -D ${DRILL_ROOT}/data -U ${PGUSER_} -Fp -Xs -c fast --no-password" >/dev/null
chmod 700 "${DRILL_ROOT}/data"

psql_scratch -c "INSERT INTO marker (tag) VALUES ('after');" >/dev/null

# Alvo por **LSN**, não por relógio: `recovery_target_time` depende de fuso e da corrida do
# arquivador (o alvo pode cair depois do último WAL já arquivado). O LSN da transação `after` é
# exato e determinístico (ADR-001).
target_lsn="$(psql_primary -c "SELECT pg_current_wal_flush_lsn();" | tr -d '[:space:]')"
target_seg="$(psql_primary -c "SELECT pg_walfile_name('${target_lsn}');" | tr -d '[:space:]')"
psql_primary -c "SELECT pg_switch_wal();" >/dev/null

# O segmento que contém o alvo tem de estar **arquivado** antes de restaurar. Sem esta
# verificação o ensaio seguiria com o WAL faltando e poderia "passar" recuperando menos do que
# deveria — verde silencioso é pior que vermelho (ADR-001).
archived=0
last=""
for _ in $(seq 1 60); do
    last="$(psql_primary -c "SELECT last_archived_wal FROM pg_stat_archiver;" | tr -d '[:space:]')"
    if [ -n "$last" ] && { [ "$last" = "$target_seg" ] || [[ "$last" > "$target_seg" ]]; }; then
        archived=1
        break
    fi
    sleep 1
done
if [ "$archived" != 1 ]; then
    echo "FALHA: arquivamento do WAL não concluído em 60s (último arquivado: ${last:-<nenhum>}, alvo: ${target_seg})." >&2
    psql_primary -c "SELECT archived_count, failed_count, last_archived_wal, last_failed_wal FROM pg_stat_archiver;" >&2 || true
    exit 1
fi

psql_scratch -c "INSERT INTO marker (tag) VALUES ('late');" >/dev/null

cat >> "${DRILL_ROOT}/data/postgresql.auto.conf" <<CONF
restore_command = 'cp ${PGDATA_}/wal_archive/%f %p'
recovery_target_lsn = '${target_lsn}'
recovery_target_inclusive = true
recovery_target_action = 'promote'
CONF
chown postgres:postgres "${DRILL_ROOT}/data/postgresql.auto.conf"
# Sem `recovery.signal` o PostgreSQL faz só *crash recovery* (para no LSN do backup) e ignora
# `restore_command`/`recovery_target_*` — é o sinal que liga a **archive recovery** (PITR).
: > "${DRILL_ROOT}/data/recovery.signal"
chown postgres:postgres "${DRILL_ROOT}/data/recovery.signal"

# Instância de recuperação em porta separada (não toca a de produção).
run_as_pg "pg_ctl -D ${DRILL_ROOT}/data -o '-p ${PORT_} -c listen_addresses=' -l ${DRILL_ROOT}/recovery.log start" >/dev/null

promoted=0
for _ in $(seq 1 60); do
    state="$(psql -v ON_ERROR_STOP=1 -U "$PGUSER_" -p "$PORT_" -d postgres -tAc "select pg_is_in_recovery()" 2>/dev/null || echo t)"
    [ "$state" = "f" ] && { promoted=1; break; }
    sleep 1
done
[ "$promoted" = 1 ] || { echo "FALHA: a instância de recuperação não promoveu."; tail -20 "${DRILL_ROOT}/recovery.log"; exit 1; }

rows="$(psql -v ON_ERROR_STOP=1 -U "$PGUSER_" -p "$PORT_" -d "$DRILL_DB" -tAc "select string_agg(tag, ',' order by at) from marker")"
run_as_pg "pg_ctl -D ${DRILL_ROOT}/data -m fast stop" >/dev/null

echo "    recuperado até ${target_lsn} → marker = ${rows}"
[ "$rows" = "base,after" ] || { echo "FALHA: esperado 'base,after' (sem 'late')." >&2; exit 1; }
EOS

finished=$(date +%s); rto=$((finished - started))
printf '{"event":"db.pitr_drill","drill_db":"%s","recovered_to":"after","excluded":"late","rto_s":%s}\n' "$DRILL_DB" "$rto"

if [ "$KEEP" = 0 ]; then
    dc -e "DRILL_DB=${DRILL_DB}" -e "DRILL_ROOT=${DRILL_ROOT}" -e "PGUSER_=${DB_USER}" postgres \
        bash -c 'rm -rf "$DRILL_ROOT"; psql -U "$PGUSER_" -d postgres -c "DROP DATABASE IF EXISTS \"$DRILL_DB\" WITH (FORCE);" >/dev/null 2>&1 || true' || true
fi

echo "==> OK — PITR validado (alvo respeitado, 'late' ausente) em ${rto}s."
exit 0
