#!/usr/bin/env bash
#
# Déploiement du site de Maud Gaquère sur l'hébergement mutualisé OVH (MG-121).
#
# Ce script se lance DEPUIS LE POSTE DE DÉVELOPPEMENT, pas depuis le serveur :
# le mutualisé OVH n'a pas Node, les fichiers de public/build/ doivent donc
# être construits ici puis téléversés. Le reste (git, composer, migrations,
# cache) est exécuté à distance par SSH.
#
#   ./deploy/deploy.sh                 # déploie la branche main
#   ./deploy/deploy.sh --branche=hotfix
#   ./deploy/deploy.sh --simulation    # ne modifie rien, montre ce qui serait fait
#   ./deploy/deploy.sh --aide
#
# set -e  : on s'arrête à la première erreur plutôt que de poursuivre sur une
#           base à moitié migrée.
# set -u  : une variable non définie est une erreur.
# pipefail: l'échec d'une commande dans un tube n'est pas masqué.
set -euo pipefail

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

# ══ Configuration ═════════════════════════════════════════════════════
#
# Deux façons de renseigner ces valeurs, au choix :
#
#   1. créer deploy/deploy.env (non versionné, lu automatiquement) :
#
#        SSH_HOST=ftp.cluster0XX.hosting.ovh.net
#        SSH_USER=monlogin
#        SSH_PORT=22
#        SSH_KEY=~/.ssh/id_rsa
#        SSH_PASS=…          # si le serveur refuse les clés (mutualisé OVH)
#        REMOTE_PATH=/home/monlogin/maud-gaquere
#        REMOTE_PHP=/usr/local/php8.3/bin/php
#        REMOTE_COMPOSER=/home/monlogin/maud-gaquere/composer.phar
#        BRANCHE=main
#
#      Ce fichier ne contient QUE ces réglages de connexion : les secrets de
#      l'application (base, mailer, hCaptcha) vivent dans .env.prod.local, sur
#      le serveur. deploy.sh ne lit rien d'autre ici.
#
#   2. passer les variables à l'appel :
#
#        SSH_HOST=… SSH_USER=… ./deploy/deploy.sh
#
# Ne pas écrire les vraies valeurs ci-dessous : ce fichier est versionné.

REGLAGES=(SSH_HOST SSH_USER SSH_PORT SSH_KEY SSH_PASS REMOTE_PATH REMOTE_PHP
          REMOTE_COMPOSER BRANCHE GIT_REMOTE RETENTION_SAUVEGARDES)

if [ -f "$PROJET/deploy/deploy.env" ]; then
    # Le « source » écraserait les variables passées à l'appel : on les met
    # de côté pour les rétablir ensuite, l'appel restant prioritaire.
    declare -A APPEL=()
    for variable in "${REGLAGES[@]}"; do
        [ -n "${!variable:-}" ] && APPEL[$variable]="${!variable}" || :
    done

    . "$PROJET/deploy/deploy.env"

    for variable in "${!APPEL[@]}"; do
        printf -v "$variable" '%s' "${APPEL[$variable]}"
    done
fi

# Hôte SSH OVH — espace client → Hébergements → onglet FTP-SSH.
# De la forme ftp.cluster0XX.hosting.ovh.net (ou sshXXX.cluster0XX…).
: "${SSH_HOST:=REMPLACER_SSH_HOST}"

# Identifiant SSH/FTP OVH, souvent le nom de l'hébergement suivi d'un suffixe.
: "${SSH_USER:=REMPLACER_SSH_USER}"

# 22 chez OVH ; certains clusters exposent aussi le port 2222.
: "${SSH_PORT:=22}"

# Clé privée utilisée pour la connexion, quand le serveur accepte les clés.
: "${SSH_KEY:=$HOME/.ssh/id_rsa}"

# Mot de passe SSH, pour les mutualisés OVH qui refusent l'authentification
# par clé. Défini, il remplace la clé et nécessite sshpass en local.
SSH_PASS="${SSH_PASS:-}"

# Dossier du projet sur le serveur. Le domaine doit pointer sur son
# sous-dossier public/ (voir DEPLOYMENT_GUIDE.md).
: "${REMOTE_PATH:=/home/$SSH_USER/maud-gaquere}"

# Binaire PHP côté OVH. Le « php » par défaut du shell est souvent une vieille
# version : viser explicitement 8.2 minimum (8.3 recommandé).
: "${REMOTE_PHP:=/usr/local/php8.3/bin/php}"

# Chemin du fichier Composer (composer.phar) sur le serveur, SANS le binaire
# PHP devant : le script l'appelle toujours sous la forme
# "$REMOTE_PHP" "$REMOTE_COMPOSER" …, pour qu'il tourne avec le bon PHP.
# Une valeur avec espace casserait d'ailleurs la lecture de deploy.env.
: "${REMOTE_COMPOSER:=/usr/local/php8.3/bin/composer}"

# Branche déployée et dépôt distant utilisé sur le serveur.
: "${BRANCHE:=main}"
: "${GIT_REMOTE:=origin}"

# Nombre de sauvegardes de base conservées sur le serveur.
: "${RETENTION_SAUVEGARDES:=10}"

# ══ Options ═══════════════════════════════════════════════════════════

SIMULATION=0
SANS_ASSETS=0
SANS_SAUVEGARDE=0

aide() {
    sed -n '3,13p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
    exit 0
}

for argument in "$@"; do
    case "$argument" in
        --branche=*)       BRANCHE="${argument#*=}" ;;
        --simulation)      SIMULATION=1 ;;
        --sans-assets)     SANS_ASSETS=1 ;;
        --sans-sauvegarde) SANS_SAUVEGARDE=1 ;;
        --aide|-h|--help)  aide ;;
        *) printf 'Option inconnue : %s\n' "$argument" >&2; exit 1 ;;
    esac
done

titre() { printf '\n\033[1;36m── %s\033[0m\n' "$1"; }
ok()    { printf '\033[0;32m  ✓ %s\033[0m\n' "$1"; }
avert() { printf '\033[0;33m  ! %s\033[0m\n' "$1"; }
erreur(){ printf '\033[0;31m  ✗ %s\033[0m\n' "$1" >&2; }

# Commande SSH commune à distant() et à rsync.
# Avec mot de passe : sshpass -e le lit dans $SSHPASS plutôt qu'en argument,
# ce qui le garde hors de la liste des processus et évite de le citer dans
# la chaîne passée à rsync -e (un ' ou un espace dans le mot de passe la
# casserait). BatchMode=yes interdirait l'authentification par mot de passe.
if [ -n "$SSH_PASS" ]; then
    export SSHPASS="$SSH_PASS"
    SSH_CMD=(sshpass -e ssh -p "$SSH_PORT" -o StrictHostKeyChecking=no)
else
    SSH_CMD=(ssh -p "$SSH_PORT" -o BatchMode=yes)
    [ -f "$SSH_KEY" ] && SSH_CMD+=(-i "$SSH_KEY")
fi
CIBLE="$SSH_USER@$SSH_HOST"

distant() { "${SSH_CMD[@]}" "$CIBLE" "$@"; }

# ══ 1. Vérifications locales ══════════════════════════════════════════

titre "Vérifications locales"

# Une variable laissée en REMPLACER_* produirait une erreur SSH obscure
# trente secondes plus tard : autant s'arrêter tout de suite.
MANQUANTES=()
for variable in SSH_HOST SSH_USER REMOTE_PATH REMOTE_PHP; do
    case "${!variable}" in REMPLACER_*) MANQUANTES+=("$variable") ;; esac
done
if [ ${#MANQUANTES[@]} -gt 0 ]; then
    erreur "Configuration incomplète : ${MANQUANTES[*]}"
    erreur "Les renseigner dans deploy/deploy.env (voir l'en-tête de ce script)."
    exit 1
fi

# Le modèle de deploy.env écrit REMOTE_PATH=/home/SSH_USER/… : le « SSH_USER »
# y est littéral et se remplace à la main, ce que la boucle ci-dessus ne voit pas.
case "$REMOTE_PATH" in
    */SSH_USER/*) erreur "REMOTE_PATH contient encore « SSH_USER » : $REMOTE_PATH"; exit 1 ;;
esac

OUTILS=(git ssh rsync)
[ -n "$SSH_PASS" ] && OUTILS+=(sshpass)
for outil in "${OUTILS[@]}"; do
    command -v "$outil" >/dev/null || { erreur "$outil est introuvable en local."; exit 1; }
done
[ "$SANS_ASSETS" -eq 1 ] || command -v npm >/dev/null || {
    erreur "npm est introuvable : impossible de construire public/build/."
    erreur "Relancer avec --sans-assets si les assets sont déjà à jour sur le serveur."
    exit 1
}
ok "Outils présents"

# Le serveur déploie depuis le dépôt distant : sans dépôt local, impossible
# de savoir ce qui serait mis en ligne.
git rev-parse --git-dir >/dev/null 2>&1 || {
    erreur "$PROJET n'est pas un dépôt git — voir infra/GIT_SSH_SETUP.md."
    exit 1
}

if [ -n "$(git status --porcelain)" ]; then
    avert "Le dépôt local contient des modifications non committées :"
    git status --short
    avert "Elles ne seront PAS déployées : le serveur tire la branche depuis $GIT_REMOTE."
fi

# Le serveur va faire un « pull --ff-only » : si la branche locale n'est pas
# poussée, il redéploierait l'ancien code sans que rien ne le signale.
if ! git rev-parse --verify --quiet "$GIT_REMOTE/$BRANCHE" >/dev/null; then
    erreur "$GIT_REMOTE/$BRANCHE est inconnue en local. Pousser la branche d'abord."
    exit 1
fi
if [ "$(git rev-parse "$BRANCHE")" != "$(git rev-parse "$GIT_REMOTE/$BRANCHE")" ]; then
    avert "$BRANCHE diffère de $GIT_REMOTE/$BRANCHE."
    avert "Le serveur déploiera $(git rev-parse --short "$GIT_REMOTE/$BRANCHE"), pas votre HEAD local."
fi
ok "Branche $BRANCHE → $(git rev-parse --short "$GIT_REMOTE/$BRANCHE")"

# ══ 2. Vérifications distantes ════════════════════════════════════════

titre "Connexion au serveur"

# Le shell restreint du mutualisé OVH peut n'afficher aucune sortie : seul le
# code de retour de SSH fait foi (0 = connexion et commande réussies).
if ! distant ls >/dev/null 2>&1; then
    erreur "Connexion SSH impossible vers $CIBLE (port $SSH_PORT)."
    if [ -n "$SSH_PASS" ]; then
        erreur "Vérifier l'activation de SSH côté OVH et SSH_PASS dans deploy/deploy.env."
    else
        erreur "Vérifier l'activation de SSH côté OVH et le dépôt de la clé :"
        erreur "  ssh-copy-id -i ${SSH_KEY}.pub -p $SSH_PORT $CIBLE"
        erreur "Si le serveur refuse les clés, définir SSH_PASS dans deploy/deploy.env."
    fi
    exit 1
fi
ok "Connexion établie ($CIBLE)"

# Chaque vérification sort avec son propre code : le diagnostic ne dépend pas
# de la sortie texte, que le shell restreint d'OVH peut avaler. Les messages
# sont tout de même envoyés sur stderr, pour le cas où ils passent.
set +e
distant bash -s -- "$REMOTE_PATH" "$REMOTE_PHP" "$REMOTE_COMPOSER" <<'DISTANT'
CHEMIN="$1"; PHP="$2"; COMPOSER="$3"

[ -d "$CHEMIN" ]            || exit 10
cd "$CHEMIN"                || exit 10
[ -f .env.prod.local ]      || exit 11
[ -x "$PHP" ] || command -v "$PHP" >/dev/null || exit 12
command -v rsync >/dev/null || exit 13

# Sans .env.local contenant APP_ENV=prod, Symfony ne lit jamais
# .env.prod.local : le site tournerait en dev avec la config de dev.
if ! grep -qs '^APP_ENV=prod' .env.local; then
    printf 'APP_ENV=prod\n' > .env.local && chmod 600 .env.local || exit 14
    echo "  ! .env.local créé (APP_ENV=prod) : il manquait." >&2
fi

# Un REMPLACER_ restant, c'est un secret oublié — et en général un site cassé.
if grep -q 'REMPLACER_' .env.prod.local; then
    grep -n 'REMPLACER_' .env.prod.local | cut -d= -f1 >&2
    exit 15
fi

"$PHP" -r 'exit(version_compare(PHP_VERSION, "8.2.0", ">=") ? 0 : 1);' || exit 16

"$PHP" -r '
    $manquantes = array_filter(["pdo_mysql","intl","gd","zip","exif","ctype","iconv"],
                               fn ($e) => !extension_loaded($e));
    fwrite(STDERR, $manquantes ? "  Manquantes : ".implode(" ", $manquantes)."\n" : "");
    exit($manquantes ? 1 : 0);
' || exit 17

[ -f "$COMPOSER" ] || exit 18

exit 0
DISTANT
CODE_PREFLIGHT=$?
set -e

case "$CODE_PREFLIGHT" in
    0)  ok "Serveur prêt ($REMOTE_PATH)" ;;
    10) erreur "$REMOTE_PATH est absent sur le serveur (premier déploiement : voir DEPLOYMENT_GUIDE.md §3)." ;;
    11) erreur ".env.prod.local est absent. Copier deploy/.env.prod.template et le renseigner." ;;
    12) erreur "PHP introuvable sur le serveur : $REMOTE_PHP" ;;
    13) erreur "rsync est absent du serveur." ;;
    14) erreur "Impossible de créer .env.local sur le serveur." ;;
    15) erreur ".env.prod.local contient encore des marqueurs REMPLACER_ à remplir." ;;
    16) erreur "PHP trop ancien sur le serveur ($REMOTE_PHP) : 8.2 minimum requis." ;;
    17) erreur "Extensions PHP manquantes (requises : pdo_mysql intl gd zip exif ctype iconv)." ;;
    18) erreur "Composer introuvable sur le serveur : $REMOTE_COMPOSER" ;;
    255) erreur "Connexion SSH perdue pendant les vérifications distantes." ;;
    *)  erreur "Vérifications distantes en échec (code $CODE_PREFLIGHT)." ;;
esac
[ "$CODE_PREFLIGHT" -eq 0 ] || exit 1

if [ "$SIMULATION" -eq 1 ]; then
    avert "Mode simulation : aucune modification ne sera appliquée."
fi

# ══ 3. Construction des assets ════════════════════════════════════════

titre "Assets (local)"

if [ "$SANS_ASSETS" -eq 1 ]; then
    avert "Construction ignorée (--sans-assets)"
else
    npm ci --silent
    npm run build
    ok "public/build/ construit"
fi

if [ ! -d public/build ] || [ -z "$(ls -A public/build 2>/dev/null)" ]; then
    erreur "public/build/ est vide : les CSS et JS ne seraient pas servis."
    exit 1
fi

# ══ 4. Sauvegarde de la base ══════════════════════════════════════════
#
# Une migration ratée sans sauvegarde préalable, c'est une base perdue.
# La sauvegarde a lieu avant le pull, sur l'état encore en ligne.

titre "Sauvegarde de la base (serveur)"

if [ "$SANS_SAUVEGARDE" -eq 1 ] || [ "$SIMULATION" -eq 1 ]; then
    avert "Sauvegarde ignorée"
else
    distant bash -s -- "$REMOTE_PATH" "$REMOTE_PHP" "$RETENTION_SAUVEGARDES" <<'DISTANT'
set -euo pipefail
CHEMIN="$1"; PHP="$2"; RETENTION="$3"
cd "$CHEMIN"

DOSSIER="var/backups"
mkdir -p "$DOSSIER"
FICHIER="$DOSSIER/avant-deploy-$(date +%Y%m%d-%H%M%S).sql"

# Les identifiants sont lus par Symfony lui-même : DB_* s'ils sont définis,
# sinon on décompose DATABASE_URL. Pas de mot de passe en dur ici.
eval "$("$PHP" -r '
    require "vendor/autoload.php";
    (new Symfony\Component\Dotenv\Dotenv())->bootEnv(".env");
    $v = ["host" => $_SERVER["DB_HOST"] ?? "", "port" => $_SERVER["DB_PORT"] ?? "3306",
          "user" => $_SERVER["DB_USER"] ?? "", "pass" => $_SERVER["DB_PASS"] ?? "",
          "name" => $_SERVER["DB_NAME"] ?? ""];
    if ($v["host"] === "" && isset($_SERVER["DATABASE_URL"])) {
        $u = parse_url($_SERVER["DATABASE_URL"]);
        $v = ["host" => $u["host"] ?? "", "port" => (string) ($u["port"] ?? 3306),
              "user" => urldecode($u["user"] ?? ""), "pass" => urldecode($u["pass"] ?? ""),
              "name" => ltrim($u["path"] ?? "", "/")];
    }
    foreach ($v as $cle => $valeur) {
        printf("DB_%s=%s\n", strtoupper($cle), escapeshellarg($valeur));
    }
')"

if ! command -v mysqldump >/dev/null; then
    echo "  ! mysqldump absent du serveur : sauvegarde impossible." >&2
    echo "  ! Sauvegarder depuis phpMyAdmin avant de poursuivre, ou --sans-sauvegarde." >&2
    exit 1
fi

# Le mot de passe passe par un fichier temporaire : en argument de
# mysqldump, il serait visible dans la liste des processus du serveur,
# partagé avec les autres clients de l'hébergement mutualisé.
CNF="$(umask 077 && mktemp)"
trap 'rm -f "$CNF"' EXIT
printf '[client]\nhost=%s\nport=%s\nuser=%s\npassword=%s\n' "$DB_HOST" "$DB_PORT" "$DB_USER" "$DB_PASS" > "$CNF"

# --no-tablespaces : le privilège PROCESS n'est pas accordé sur un mutualisé.
mysqldump --defaults-extra-file="$CNF" --single-transaction --no-tablespaces \
          --default-character-set=utf8mb4 "$DB_NAME" > "$FICHIER"
gzip -f "$FICHIER"
echo "  ✓ Sauvegarde : $CHEMIN/${FICHIER}.gz ($(du -h "${FICHIER}.gz" | cut -f1))"

ls -1t "$DOSSIER"/*.sql.gz 2>/dev/null | tail -n "+$((RETENTION + 1))" | xargs -r rm --
DISTANT
fi

# ══ 5. Code et dépendances ════════════════════════════════════════════

titre "Code et dépendances (serveur)"

if [ "$SIMULATION" -eq 1 ]; then
    avert "git pull + composer install : ignorés en simulation"
else
    distant bash -s -- "$REMOTE_PATH" "$REMOTE_PHP" "$REMOTE_COMPOSER" "$BRANCHE" "$GIT_REMOTE" <<'DISTANT'
set -euo pipefail
CHEMIN="$1"; PHP="$2"; COMPOSER="$3"; BRANCHE="$4"; DEPOT="$5"
cd "$CHEMIN"

if [ -n "$(git status --porcelain)" ]; then
    echo "✗ Le dépôt du serveur contient des modifications locales :" >&2
    git status --short >&2
    echo "✗ Déploiement interrompu : elles seraient écrasées." >&2
    exit 1
fi

git fetch --prune "$DEPOT"
git checkout "$BRANCHE"
git pull --ff-only "$DEPOT" "$BRANCHE"
echo "  ✓ $BRANCHE à jour ($(git rev-parse --short HEAD))"

# --no-scripts : les auto-scripts de Symfony Flex vident le cache avant que
# les assets ne soient téléversés. Cache et assets sont traités à l'étape 7.
# COMPOSER_MEMORY_LIMIT : la limite PHP d'un mutualisé fait échouer le
# résolveur de dépendances sans message clair.
export COMPOSER_MEMORY_LIMIT=-1
export APP_ENV=prod APP_DEBUG=0
"$PHP" "$COMPOSER" install --no-dev --optimize-autoloader --no-interaction --prefer-dist --no-progress --no-scripts
echo "  ✓ composer install terminé"
DISTANT
fi

# ══ 6. Téléversement des fichiers non versionnés ══════════════════════
#
# public/build/ est produit par Webpack et ignoré par git : c'est le seul
# chemin par lequel il arrive sur le serveur.
# Le --delete est volontairement limité à ce dossier : public/uploads/ et
# public/media/ contiennent ce que le back-office a déposé et ne doivent
# jamais être touchés.

titre "Téléversement des assets"

RSYNC_OPTS=(-az --human-readable --delete -e "${SSH_CMD[*]}")
[ "$SIMULATION" -eq 1 ] && RSYNC_OPTS+=(--dry-run)

rsync "${RSYNC_OPTS[@]}" public/build/ "$CIBLE:$REMOTE_PATH/public/build/"
ok "public/build/ synchronisé"

if [ -d public/images ] && [ -n "$(ls -A public/images 2>/dev/null)" ]; then
    rsync "${RSYNC_OPTS[@]}" public/images/ "$CIBLE:$REMOTE_PATH/public/images/"
    ok "public/images/ synchronisé"
fi

# ══ 7. Migrations, cache et droits ════════════════════════════════════

titre "Migrations, cache et droits (serveur)"

if [ "$SIMULATION" -eq 1 ]; then
    avert "Migrations et cache : ignorés en simulation"
else
    distant bash -s -- "$REMOTE_PATH" "$REMOTE_PHP" <<'DISTANT'
set -euo pipefail
CHEMIN="$1"; PHP="$2"
cd "$CHEMIN"

export APP_ENV=prod APP_DEBUG=0
CONSOLE="$PHP -d memory_limit=512M bin/console"

# up-to-date sort en erreur dès qu'une migration reste à jouer : c'est le
# code de retour qui décide, pas le texte, qui dépend de la langue.
if $CONSOLE doctrine:migrations:up-to-date --env=prod >/dev/null 2>&1; then
    echo "  ✓ Aucune migration en attente"
else
    $CONSOLE doctrine:migrations:migrate --no-interaction --allow-no-migration --env=prod
    echo "  ✓ Migrations appliquées"
fi

$CONSOLE cache:clear --env=prod --no-debug
$CONSOLE cache:warmup --env=prod --no-debug
$CONSOLE assets:install public --env=prod --no-debug >/dev/null
echo "  ✓ Cache reconstruit"

# Apache et le PHP-FPM d'OVH tournent sous le même compte : 775 suffit,
# et le back-office doit pouvoir écrire dans uploads/.
mkdir -p var/log var/cache public/uploads
chmod -R 775 var public/uploads
chmod 600 .env.prod.local .env.local
echo "  ✓ Droits ajustés"

# opcache garde en mémoire les fichiers de l'ancienne version : sur un
# mutualisé on ne peut pas recharger PHP, mais toucher les entrées suffit
# à forcer la relecture au prochain appel.
[ -f public/index.php ] && touch public/index.php
DISTANT
fi

# ══ 8. Contrôle final ═════════════════════════════════════════════════

titre "Contrôle"

if [ "$SIMULATION" -eq 1 ]; then
    printf '\n\033[1;33m✓ Simulation terminée — rien n'"'"'a été modifié\033[0m\n\n'
    exit 0
fi

# Le shell restreint peut ne rien renvoyer : on retombe alors sur la
# révision attendue, et le contrôle HTTP ci-dessous est simplement sauté.
REVISION="$(distant "cd $REMOTE_PATH && git rev-parse --short HEAD" 2>/dev/null || true)"
[ -n "$REVISION" ] || REVISION="$(git rev-parse --short "$GIT_REMOTE/$BRANCHE") (attendue, non confirmée par le serveur)"

# DEFAULT_URI est la seule URL publique connue du script : on l'interroge
# pour vérifier que le site répond vraiment après le déploiement.
URL="$(distant "cd $REMOTE_PATH && grep -m1 '^DEFAULT_URI=' .env.prod.local | cut -d= -f2-" || true)"
if [ -n "$URL" ] && command -v curl >/dev/null; then
    CODE="$(curl -s -o /dev/null -w '%{http_code}' -L --max-time 20 "$URL" || echo 000)"
    case "$CODE" in
        200) ok "$URL répond 200" ;;
        000) avert "$URL injoignable depuis ce poste (DNS pas encore propagé ?)" ;;
        *)   erreur "$URL répond $CODE — consulter var/log/prod.log sur le serveur." ;;
    esac
fi

printf '\n\033[1;32m✓ Déploiement terminé — %s\033[0m\n' "$REVISION"
printf '  Dérouler maintenant deploy/VERIFICATION_PROD.md\n\n'
