#!/bin/bash
#
# Rafraîchit les statistiques InnoDB (ANALYZE TABLE) des tables d'une base
# MySQL/MariaDB, sur un serveur de production SANS Docker : il suffit d'un
# client mysqlcheck/mariadb-check et d'un login/mot de passe de connexion.
#
# Pourquoi : innodb_stats_auto_recalc ne se déclenche qu'après ~10% de lignes
# modifiées depuis le dernier calcul. Sur une grosse table à faible churn
# relatif (ou après un import/restore), les statistiques peuvent rester
# fausses pendant des mois et l'optimiseur choisit alors de mauvais plans
# d'exécution (requêtes parfois plusieurs fois plus lentes que nécessaire).
#
# Configuration (au choix, les variables d'environnement l'emportent) :
#   1. un fichier de config "analyze_tables_s217.conf" placé à côté du script
#      (ou un autre chemin via CONFIG_FILE), au format shell :
#          DB_HOST=localhost
#          DB_PORT=3306
#          DB_USER=mon_user
#          DB_PASSWORD=mon_mot_de_passe
#          DB_NAME=ma_base
#      => à protéger : chmod 600 analyze_tables_s217.conf
#   2. des variables d'environnement :
#          DB_USER=... DB_PASSWORD=... DB_NAME=... ./analyze_tables_s217.sh
#
# Variables :
#   DB_USER, DB_PASSWORD  obligatoires
#   DB_NAME               base à analyser (espaces possibles pour plusieurs
#                         bases : "base1 base2"). Si vide : toutes les bases
#                         accessibles à ce user, SAUF les bases système listées
#                         dans DB_EXCLUDE (nécessite les droits adéquats)
#   DB_EXCLUDE            bases exclues quand DB_NAME est vide (séparées par
#                         des espaces). Défaut : information_schema mysql
#                         performance_schema sys
#   DB_HOST               défaut 127.0.0.1
#   DB_PORT               défaut 3306
#   LOG_DIR               défaut ./logs à côté du script
#   LOG_RETENTION_DAYS    défaut 30
#
# Exemple de cron (tous les dimanches à 5h, à adapter : de préférence en heure
# creuse, ANALYZE TABLE pose brièvement des verrous sur chaque table) :
#   0 5 * * 7 /chemin/vers/analyze_tables_s217.sh >> /chemin/vers/logs/cron.log 2>&1

set -euo pipefail

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

CONFIG_FILE="${CONFIG_FILE:-$SCRIPT_DIR/analyze_tables_s217.conf}"
if [ -f "$CONFIG_FILE" ]; then
	# Les variables déjà présentes dans l'environnement gardent la priorité.
	_saved=()
	for _v in DB_HOST DB_PORT DB_USER DB_PASSWORD DB_NAME DB_EXCLUDE LOG_DIR LOG_RETENTION_DAYS; do
		if [ -n "${!_v+x}" ]; then
			_saved+=("$_v=${!_v}")
		fi
	done
	# On retire les \r éventuels (fichier édité sous Windows, fins de ligne CRLF) :
	# sinon chaque valeur se termine par un \r invisible (ex. user "marco\r").
	# shellcheck disable=SC1090
	. <(tr -d '\r' <"$CONFIG_FILE")
	for _kv in ${_saved[@]+"${_saved[@]}"}; do
		printf -v "${_kv%%=*}" '%s' "${_kv#*=}"
	done
	unset _saved _v _kv
fi

DB_HOST="${DB_HOST:-127.0.0.1}"
DB_PORT="${DB_PORT:-3306}"
DB_USER="${DB_USER:-}"
DB_PASSWORD="${DB_PASSWORD:-}"
DB_NAME="${DB_NAME:-}"
DB_EXCLUDE="${DB_EXCLUDE-information_schema mysql performance_schema sys}"
LOG_DIR="${LOG_DIR:-$SCRIPT_DIR/logs}"
LOG_RETENTION_DAYS="${LOG_RETENTION_DAYS:-30}"

mkdir -p "$LOG_DIR"
LOG_FILE="$LOG_DIR/analyze_$(date +%Y%m%d_%H%M%S).log"

log() {
	echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"
}

if [ -z "$DB_USER" ] || [ -z "$DB_PASSWORD" ]; then
	log "ERREUR : DB_USER et DB_PASSWORD sont obligatoires (variables d'environnement ou fichier $CONFIG_FILE)."
	exit 1
fi

# Selon la distribution, le client s'appelle "mysqlcheck" ou "mariadb-check".
CHECK_BIN=""
for candidate in mysqlcheck mariadb-check; do
	if command -v "$candidate" >/dev/null 2>&1; then
		CHECK_BIN="$candidate"
		break
	fi
done

if [ -z "$CHECK_BIN" ]; then
	log "ERREUR : ni mysqlcheck ni mariadb-check n'est installé sur ce serveur."
	exit 1
fi

# Évite que deux exécutions se chevauchent (ANALYZE TABLE peut être long sur
# de grosses tables).
LOCK_FILE="$LOG_DIR/.analyze.lock"
if command -v flock >/dev/null 2>&1; then
	exec 9>"$LOCK_FILE"
	if ! flock -n 9; then
		log "Une autre exécution est déjà en cours, abandon."
		exit 0
	fi
fi

# Le mot de passe est passé via un fichier temporaire (droits 600) plutôt que
# via --password=..., pour ne pas apparaître dans la liste des processus (ps).
CNF_FILE="$(mktemp)"
trap 'rm -f "$CNF_FILE"' EXIT
chmod 600 "$CNF_FILE"
{
	echo "[client]"
	echo "host=$DB_HOST"
	echo "port=$DB_PORT"
	echo "user=$DB_USER"
	echo "password=\"$(printf '%s' "$DB_PASSWORD" | sed 's/\\/\\\\/g; s/"/\\"/g')\""
} >"$CNF_FILE"

if [ -n "$DB_NAME" ]; then
	TARGET_ARGS=(--databases $DB_NAME)
	TARGET_LABEL="base(s) : $DB_NAME"
else
	# mysqlcheck n'a pas d'option d'exclusion : on liste les bases via le
	# client SQL puis on retire les bases système.
	LIST_CMD=()
	for candidate in mysql mariadb; do
		if command -v "$candidate" >/dev/null 2>&1; then
			LIST_CMD=("$candidate" --defaults-extra-file="$CNF_FILE" -N -B -e "SHOW DATABASES")
			break
		fi
	done
	if [ ${#LIST_CMD[@]} -eq 0 ]; then
		log "ERREUR : ni mysql ni mariadb (client SQL) n'est installé : impossible de lister les bases à analyser. Renseigner DB_NAME."
		exit 1
	fi

	if ! ALL_DBS="$("${LIST_CMD[@]}" 2>&1)"; then
		log "ERREUR : impossible de lister les bases : $ALL_DBS"
		exit 1
	fi

	TARGET_DBS=()
	while IFS= read -r db; do
		[ -z "$db" ] && continue
		skip=0
		for excluded in $DB_EXCLUDE; do
			if [ "$db" = "$excluded" ]; then
				skip=1
				break
			fi
		done
		[ "$skip" -eq 0 ] && TARGET_DBS+=("$db")
	done <<<"$ALL_DBS"

	if [ ${#TARGET_DBS[@]} -eq 0 ]; then
		log "Aucune base à analyser (après exclusion de : ${DB_EXCLUDE:-rien})."
		exit 0
	fi

	TARGET_ARGS=(--databases "${TARGET_DBS[@]}")
	TARGET_LABEL="toutes les bases sauf : ${DB_EXCLUDE:-aucune exclusion} => ${TARGET_DBS[*]}"
fi

log "Début ANALYZE TABLE sur ${DB_HOST}:${DB_PORT} (${TARGET_LABEL}, client : ${CHECK_BIN})"

set +e
"$CHECK_BIN" --defaults-extra-file="$CNF_FILE" --analyze "${TARGET_ARGS[@]}" 2>&1 | tee -a "$LOG_FILE"
STATUS=${PIPESTATUS[0]}
set -e

if [ "$STATUS" -ne 0 ]; then
	log "ERREUR : $CHECK_BIN a échoué (code $STATUS)."
else
	log "Fin ANALYZE TABLE"
fi

find "$LOG_DIR" -name 'analyze_*.log' -mtime +"$LOG_RETENTION_DAYS" -delete

exit "$STATUS"
