#!/usr/bin/env bash
#
# dbbackup — Sauvegarde MySQL/MariaDB + transfert VPS + rotation locale et distante.
#
# Installé par le paquet .deb dans /usr/bin. La configuration (secrets, chemins,
# liste des bases planifiées) vit dans /etc/dbbackup.conf (chmod 600, root:root),
# créée à l'installation depuis le modèle fourni.
#
# Usage :
#     dbbackup <base1> [base2] ...   Sauvegarde les bases indiquées.
#     dbbackup --scheduled           Sauvegarde les bases de $DATABASES (timer).
#     dbbackup list [base ...]       Liste les archives locales.
#     dbbackup doctor                Diagnostique configuration, accès et planification.
#     dbbackup version | -v          Affiche la version.
#     dbbackup help                  Cette aide.
#
# Interrompt le script si une commande échoue (sécurité)
set -eo pipefail

# Version injectée au build par debian/rules (placeholder 1.1-1).
VERSION="1.1-1"
if [ "$VERSION" = "1.1-1" ]; then
    VERSION="$(dpkg-query -W -f='${Version}' dbbackup 2>/dev/null || echo 'dev')"
fi

# PATH explicite : cron/systemd démarrent avec un PATH minimal.
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"

CONFIG_FILE="/etc/dbbackup.conf"

GREEN='\033[0;32m'
BLUE='\033[0;34m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m'

usage() {
    cat <<EOF
dbbackup $VERSION — sauvegarde MySQL/MariaDB vers un VPS distant.

Usage :
    dbbackup <base1> [base2] ...   Sauvegarde les bases indiquées.
    dbbackup --scheduled           Sauvegarde les bases listées dans DATABASES
                                   (variable de $CONFIG_FILE ; utilisé par le timer).
    dbbackup list [base ...]       Liste les archives locales (toutes les bases si
                                   aucun nom n'est donné).
    dbbackup doctor                Diagnostique la configuration, les accès MySQL
                                   et VPS, et la planification.
    dbbackup version | -v          Affiche la version.
    dbbackup help | -h             Affiche cette aide.

Exemple :
    dbbackup mawena factury xpress

Configuration : $CONFIG_FILE
EOF
}

# --- Commandes ne nécessitant pas la configuration -------------------------
case "${1:-}" in
    version|-v|--version) echo "dbbackup $VERSION"; exit 0 ;;
    help|-h|--help)       usage; exit 0 ;;
    "")                   usage >&2; exit 1 ;;
esac

# ==========================================
# CHARGEMENT DE LA CONFIGURATION
# ==========================================
if [ ! -r "$CONFIG_FILE" ]; then
    echo "Erreur : configuration introuvable ou illisible : $CONFIG_FILE" >&2
    exit 1
fi
# shellcheck source=/dev/null
source "$CONFIG_FILE"

# Valeurs par défaut si absentes du fichier de conf
VPS_PORT="${VPS_PORT:-22}"
KEEP="${KEEP:-10}"
BACKUP_ROOT="${BACKUP_ROOT:-/home/support/mysql}"

# ==========================================
# IDENTIFIANTS MySQL (hors ligne de commande)
# ==========================================
# Fichier temporaire lisible par le seul propriétaire, pour ne pas exposer le
# mot de passe dans « ps » (visible par tous les utilisateurs).
MYSQL_DEFAULTS=""
setup_mysql_defaults() {
    [ -n "$MYSQL_DEFAULTS" ] && return 0
    if [ -n "${DB_USER:-}" ] || [ -n "${DB_PASS:-}" ]; then
        MYSQL_DEFAULTS="$(mktemp)"
        chmod 600 "$MYSQL_DEFAULTS"
        {
            echo "[client]"
            [ -n "${DB_USER:-}" ] && echo "user=$DB_USER"
            [ -n "${DB_PASS:-}" ] && echo "password=$DB_PASS"
        } > "$MYSQL_DEFAULTS"
    fi
    return 0
}

# « return 0 » obligatoire : sous « set -e », un trap EXIT qui se termine sur une
# commande en échec écrase le code de sortie du script (un succès deviendrait 1).
# shellcheck disable=SC2317  # appelée indirectement par le trap EXIT
cleanup() {
    [ -n "$MYSQL_DEFAULTS" ] && rm -f "$MYSQL_DEFAULTS"
    return 0
}
trap cleanup EXIT

# Options SSH communes : jamais d'invite interactive (le timer tourne sans tty).
ssh_opts() {
    echo "-p $VPS_PORT -i $SSH_KEY -o StrictHostKeyChecking=accept-new -o BatchMode=yes"
}

# ==========================================
# DIAGNOSTIC (dbbackup doctor)
# ==========================================
DOCTOR_ERRORS=0
DOCTOR_WARNINGS=0

d_ok()   { echo -e "  ${GREEN}●${NC} $1"; }
d_ko()   { echo -e "  ${RED}○${NC} $1"; DOCTOR_ERRORS=$((DOCTOR_ERRORS + 1)); }
d_warn() { echo -e "  ${YELLOW}◐${NC} $1"; DOCTOR_WARNINGS=$((DOCTOR_WARNINGS + 1)); }
d_info() { echo "      $1"; }

# Client MySQL utilisé pour les tests de connexion (mysql ou mariadb).
mysql_client() {
    command -v mysql 2>/dev/null || command -v mariadb 2>/dev/null || return 1
}

list_mysql_databases() {
    local client="$1"
    local filter='^(information_schema|performance_schema|mysql|sys)$'
    if [ -n "$MYSQL_DEFAULTS" ]; then
        "$client" --defaults-extra-file="$MYSQL_DEFAULTS" -N -B -e 'SHOW DATABASES' 2>/dev/null \
            | grep -Ev "$filter" || true
    else
        "$client" -N -B -e 'SHOW DATABASES' 2>/dev/null | grep -Ev "$filter" || true
    fi
}

remote_archive_count() {
    # shellcheck disable=SC2046
    ssh $(ssh_opts) -o ConnectTimeout=8 "$VPS_USER@$VPS_HOST" \
        "ls -1 '$VPS_DIR'/*.sql.gz 2>/dev/null | wc -l" 2>/dev/null || return 1
}

doctor() {
    echo -e "${BLUE}=== Diagnostic dbbackup $VERSION ===${NC}"

    # --- Configuration ---
    echo -e "\n${YELLOW}Configuration :${NC}"
    local perms owner
    perms="$(stat -c '%a' "$CONFIG_FILE" 2>/dev/null || echo '?')"
    owner="$(stat -c '%U:%G' "$CONFIG_FILE" 2>/dev/null || echo '?')"
    if [ "$perms" = "600" ] && [ "$owner" = "root:root" ]; then
        d_ok "$CONFIG_FILE ($owner $perms) — secrets protégés"
    else
        d_warn "$CONFIG_FILE : $owner $perms — attendu root:root 600"
        d_info "sudo chown root:root $CONFIG_FILE && sudo chmod 600 $CONFIG_FILE"
    fi
    local var
    for var in VPS_USER VPS_HOST VPS_DIR SSH_KEY BACKUP_ROOT; do
        if [ -n "${!var:-}" ]; then
            d_ok "$var = ${!var}"
        else
            d_ko "$var non défini dans $CONFIG_FILE"
        fi
    done
    d_ok "KEEP = $KEEP archive(s) conservée(s), en local comme sur le VPS"

    # --- Dépendances ---
    echo -e "\n${YELLOW}Dépendances :${NC}"
    local cmd
    for cmd in mysqldump gzip rsync ssh; do
        if command -v "$cmd" >/dev/null 2>&1; then
            d_ok "$cmd disponible"
        else
            d_ko "$cmd manquant"
        fi
    done

    # --- Clé SSH ---
    echo -e "\n${YELLOW}Clé SSH :${NC}"
    if [ -z "${SSH_KEY:-}" ]; then
        d_ko "SSH_KEY non définie"
    elif [ ! -r "$SSH_KEY" ]; then
        d_ko "Clé introuvable ou illisible : $SSH_KEY"
    else
        local key_perms
        key_perms="$(stat -c '%a' "$SSH_KEY" 2>/dev/null || echo '?')"
        d_ok "Clé lisible : $SSH_KEY ($key_perms)"
        case "$key_perms" in
            600|400) ;;
            *) d_warn "Permissions trop larges ($key_perms) — attendu 600" ;;
        esac
    fi

    # --- Stockage local ---
    echo -e "\n${YELLOW}Stockage local :${NC}"
    if [ -d "$BACKUP_ROOT" ]; then
        if [ -w "$BACKUP_ROOT" ]; then
            d_ok "$BACKUP_ROOT accessible en écriture"
        else
            d_ko "$BACKUP_ROOT non accessible en écriture"
        fi
        d_info "Disque : $(df -h "$BACKUP_ROOT" 2>/dev/null | awk 'NR==2 {print $4" libres sur "$2}')"
        d_info "Occupé par les archives : $(du -sh "$BACKUP_ROOT" 2>/dev/null | cut -f1)"
    else
        d_warn "$BACKUP_ROOT absent (créé à la première sauvegarde)"
    fi

    # --- Accès MySQL ---
    echo -e "\n${YELLOW}Accès MySQL/MariaDB :${NC}"
    local client
    if client="$(mysql_client)"; then
        setup_mysql_defaults
        local probe rc=0
        if [ -n "$MYSQL_DEFAULTS" ]; then
            probe="$("$client" --defaults-extra-file="$MYSQL_DEFAULTS" -N -B -e 'SELECT 1' 2>&1)" || rc=$?
        else
            probe="$("$client" -N -B -e 'SELECT 1' 2>&1)" || rc=$?
        fi
        if [ "$rc" -ne 0 ]; then
            d_ko "Connexion refusée"
            d_info "$probe"
        else
            if [ -n "${DB_USER:-}" ]; then
                d_ok "Connexion réussie (utilisateur $DB_USER)"
            else
                d_ok "Connexion réussie (socket local, sans mot de passe)"
            fi
            local dbs
            dbs="$(list_mysql_databases "$client" | tr '\n' ' ')"
            [ -n "$dbs" ] && d_info "Bases visibles : $dbs"
        fi
    else
        d_ko "Aucun client MySQL (mysql/mariadb) installé"
    fi

    # --- VPS distant ---
    echo -e "\n${YELLOW}VPS distant :${NC}"
    if [ -z "${VPS_HOST:-}" ] || [ -z "${SSH_KEY:-}" ] || [ ! -r "${SSH_KEY:-/nonexistent}" ]; then
        d_ko "Test impossible (VPS_HOST ou SSH_KEY invalide)"
    else
        local answer
        # shellcheck disable=SC2046
        if answer="$(ssh $(ssh_opts) -o ConnectTimeout=8 "$VPS_USER@$VPS_HOST" \
                "test -d '$VPS_DIR' && echo OK || echo NODIR" 2>&1)"; then
            case "$answer" in
                *OK*)    d_ok "SSH $VPS_USER@$VPS_HOST:$VPS_PORT — $VPS_DIR présent"
                         local n
                         n="$(remote_archive_count || echo '')"
                         [ -n "$n" ] && d_info "Archives sur le VPS : $n" ;;
                *NODIR*) d_warn "SSH établi, mais $VPS_DIR absent sur le VPS"
                         d_info "ssh -p $VPS_PORT $VPS_USER@$VPS_HOST 'mkdir -p $VPS_DIR'" ;;
                *)       d_warn "SSH établi, réponse inattendue : $answer" ;;
            esac
        else
            d_ko "Connexion SSH impossible vers $VPS_USER@$VPS_HOST:$VPS_PORT"
            d_info "${answer:-aucune sortie}"
        fi
    fi

    # --- Planification ---
    echo -e "\n${YELLOW}Planification :${NC}"
    if [ -n "${DATABASES:-}" ]; then
        d_ok "DATABASES = $DATABASES"
    else
        d_warn "DATABASES vide — le timer ne sauvegardera aucune base"
    fi
    if systemctl is-active --quiet dbbackup.timer 2>/dev/null; then
        d_ok "dbbackup.timer actif"
        local next
        next="$(systemctl show -p NextElapseUSecRealtime --value dbbackup.timer 2>/dev/null || echo '')"
        if [[ "$next" =~ ^[0-9]+$ ]] && [ "$next" -gt 0 ]; then
            d_info "Prochaine exécution : $(date -d "@$((next / 1000000))" '+%Y-%m-%d %H:%M:%S' 2>/dev/null)"
        fi
    else
        d_warn "dbbackup.timer inactif — sudo systemctl enable --now dbbackup.timer"
    fi

    # --- Résumé ---
    echo ""
    if [ "$DOCTOR_ERRORS" -gt 0 ]; then
        echo -e "${RED}$DOCTOR_ERRORS problème(s) bloquant(s)${NC}, $DOCTOR_WARNINGS avertissement(s)."
        return 1
    fi
    if [ "$DOCTOR_WARNINGS" -gt 0 ]; then
        echo -e "${YELLOW}Aucun problème bloquant, $DOCTOR_WARNINGS avertissement(s).${NC}"
    else
        echo -e "${GREEN}Tout est en ordre : dbbackup est prêt.${NC}"
    fi
}

# ==========================================
# INVENTAIRE LOCAL (dbbackup list)
# ==========================================
list_backups() {
    local -a targets=()
    if [ "$#" -gt 0 ]; then
        targets=("$@")
    else
        local dir
        for dir in "$BACKUP_ROOT"/*/; do
            [ -d "$dir" ] || continue
            targets+=("$(basename "$dir")")
        done
    fi

    if [ "${#targets[@]}" -eq 0 ]; then
        echo "Aucune sauvegarde locale dans $BACKUP_ROOT."
        return 0
    fi

    echo -e "${BLUE}=== Archives locales — $BACKUP_ROOT (rotation à $KEEP) ===${NC}"
    local db sql_dir count f i
    for db in "${targets[@]}"; do
        sql_dir="$BACKUP_ROOT/$db/sql"
        echo -e "\n${YELLOW}$db${NC}"
        local -a files=()
        [ -d "$sql_dir" ] && mapfile -t files < <(ls -1t "$sql_dir"/"$db"-*.sql.gz 2>/dev/null || true)
        count="${#files[@]}"
        if [ "$count" -eq 0 ]; then
            echo "  Aucune archive."
            continue
        fi
        echo "  $count archive(s), $(du -sh "$sql_dir" 2>/dev/null | cut -f1) au total"
        i=0
        for f in "${files[@]}"; do
            i=$((i + 1))
            if [ "$i" -gt 5 ]; then
                echo "  … et $((count - 5)) archive(s) plus ancienne(s)."
                break
            fi
            printf '  %s  %6s  %s\n' \
                "$(date -r "$f" '+%Y-%m-%d %H:%M' 2>/dev/null)" \
                "$(du -h "$f" 2>/dev/null | cut -f1)" \
                "$(basename "$f")"
        done
    done
}

# ==========================================
# SOUS-COMMANDES SANS SAUVEGARDE
# ==========================================
case "${1:-}" in
    doctor)
        doctor
        exit $?
        ;;
    list|ls)
        shift
        list_backups "$@"
        exit 0
        ;;
esac

# --- Mode planifié : remplace les arguments par la liste DATABASES ----------
SCHEDULED=0
if [ "${1:-}" = "--scheduled" ]; then
    SCHEDULED=1
    if [ -z "${DATABASES:-}" ]; then
        echo "Mode planifié : aucune base définie (DATABASES vide dans $CONFIG_FILE). Rien à faire."
        exit 0
    fi
    # word-splitting volontaire : DATABASES est une liste séparée par des espaces
    # shellcheck disable=SC2086
    set -- $DATABASES
fi

# ==========================================
# VÉRIFICATIONS PRÉALABLES
# ==========================================
for cmd in mysqldump gzip rsync ssh; do
    command -v "$cmd" >/dev/null 2>&1 || {
        echo "Erreur : commande requise manquante : $cmd" >&2
        exit 1
    }
done

if [ -z "${SSH_KEY:-}" ] || [ ! -r "$SSH_KEY" ]; then
    echo "Erreur : clé SSH introuvable ou illisible : ${SSH_KEY:-<non définie>}" >&2
    echo "Diagnostic complet : dbbackup doctor" >&2
    exit 1
fi

setup_mysql_defaults

# ==========================================
# FONCTION DE SAUVEGARDE
# ==========================================
# Chaque étape est vérifiée explicitement plutôt que via « set -e » : une base en
# échec n'interrompt plus les suivantes — indispensable en mode planifié, où
# plusieurs bases sont traitées d'affilée par le timer.
backup_database() {
    local database_name="$1"
    local sql_dir="$BACKUP_ROOT/$database_name/sql"
    local timestamp
    timestamp="$(date +"%Y-%m-%d_%H-%M-%S")"
    local file_name="$database_name-$timestamp.sql.gz"
    local file_path="$sql_dir/$file_name"

    echo "=========================================="
    echo "[$(date +'%Y-%m-%d %H:%M:%S')] Début de sauvegarde : $database_name"
    echo "=========================================="

    if ! mkdir -p "$sql_dir"; then
        echo "✗ Impossible de créer $sql_dir" >&2
        return 1
    fi

    # --- 1. DUMP & COMPRESSION ---
    echo "--> Exportation et compression de la base..."
    if [ -n "$MYSQL_DEFAULTS" ]; then
        mysqldump --defaults-extra-file="$MYSQL_DEFAULTS" \
            --single-transaction --quick "$database_name" | gzip > "$file_path"
    else
        mysqldump --single-transaction --quick "$database_name" | gzip > "$file_path"
    fi || {
        # Ne jamais conserver une archive tronquée : la rotation la compterait
        # comme une sauvegarde valide et écarterait une archive saine.
        echo "✗ Échec de l'export de $database_name — archive incomplète supprimée." >&2
        rm -f "$file_path"
        return 1
    }
    echo "✓ Fichier créé : $file_path ($(du -h "$file_path" | cut -f1))"

    # --- 2. TRANSFERT VERS VPS ---
    echo "--> Envoi vers le VPS (Port $VPS_PORT)..."
    # shellcheck disable=SC2046
    if ! rsync -avz -e "ssh $(ssh_opts)" "$file_path" "$VPS_USER@$VPS_HOST:$VPS_DIR/"; then
        echo "✗ Transfert vers $VPS_HOST échoué — l'archive locale est conservée." >&2
        return 1
    fi
    echo "✓ Transfert réussi."

    # --- 3. ROTATION LOCALE (conserver les $KEEP derniers) ---
    echo "--> Nettoyage local..."
    local local_files
    mapfile -t local_files < <(ls -1t "$sql_dir"/"$database_name"-*.sql.gz 2>/dev/null)
    if [ "${#local_files[@]}" -gt "$KEEP" ]; then
        for ((i=KEEP; i<${#local_files[@]}; i++)); do
            echo "    Suppression locale : ${local_files[i]}"
            rm -f "${local_files[i]}"
        done
    fi

    # --- 4. ROTATION DISTANTE SUR LE VPS ---
    echo "--> Nettoyage sur le VPS..."
    # SC2029 : l'expansion côté client est voulue (le nom de base et le dossier
    # distant viennent de la configuration locale).
    # shellcheck disable=SC2029,SC2046
    if ! ssh $(ssh_opts) "$VPS_USER@$VPS_HOST" \
        "ls -1t $VPS_DIR/$database_name-*.sql.gz 2>/dev/null | tail -n +$((KEEP + 1)) | xargs -r rm -f"; then
        echo "✗ Nettoyage distant échoué (la sauvegarde, elle, est bien en place)." >&2
        return 1
    fi
    echo "✓ Nettoyage distant terminé."

    echo "✓ Sauvegarde de $database_name terminée avec succès !"
    echo ""
}

# ==========================================
# BOUCLE PRINCIPALE
# ==========================================
TOTAL=$#
SUCCESS=0
declare -a FAILED=()
for db in "$@"; do
    if backup_database "$db"; then
        SUCCESS=$((SUCCESS + 1))
    else
        FAILED+=("$db")
        echo ""
    fi
done

# Bilan : c'est cette ligne que l'on retrouve dans le journal systemd du mode
# planifié (journalctl -u dbbackup.service).
if [ "${#FAILED[@]}" -eq 0 ]; then
    echo -e "${GREEN}Bilan : $SUCCESS/$TOTAL base(s) sauvegardée(s) avec succès.${NC}"
    exit 0
fi

echo -e "${RED}Bilan : $SUCCESS/$TOTAL base(s) sauvegardée(s). Échec : ${FAILED[*]}${NC}" >&2
[ "$SCHEDULED" -eq 1 ] && echo "Diagnostic : dbbackup doctor" >&2
exit 1
