#!/usr/bin/env bash
#
# Séquence base de la bascule de production — issue #156, livrable B14.
# Runbook de référence : docs/roadmap/14_RUNBOOK_BASCULE_PROD.md §0.7 étape 2, §3 ter, §4 phase 5.
#
#   restauration du dump  →  conversion InnoDB  →  alignement des types portant une FK
#   →  migrations Laravel (purges + rattrapages)  →  création des clés étrangères
#   →  contrôles de sortie (diff de schéma outillé, comptages, moteurs)
#
# Le jour J on exécute, on n'improvise pas : chaque étape est idempotente, la séquence
# se rejoue autant de fois qu'on veut, et la seconde passe ne modifie rien.
#
# Ce qui n'est PAS ici, et pourquoi :
#   - la conversion InnoDB reste en SQL, hors migration (choix acté, runbook §3 ter) ;
#   - aucune clé étrangère n'est écrite en dur : elles sont dérivées d'une base de
#     référence construite par `migrate:fresh` au moment de l'exécution ;
#   - la table `migrations` n'est JAMAIS recalée (incident #137) ; seul le diff de
#     schéma fait preuve, « Nothing to migrate » ne prouve rien.
#
set -u -o pipefail

usage() {
    cat <<'FIN'
Usage : bascule_base.sh --base <nom> [--dump <fichier.sql>] [options]

  --base NOM              base cible (créée si absente). OBLIGATOIRE.
  --dump FICHIER          dump à restaurer : mysqldump réel de e-learning-prod, ou
                          fixture produite par scripts/bascule/fixture/. Restauré
                          seulement si la base cible est absente ou vide (voir
                          --forcer-restauration), ce qui rend la 2e passe no-op.
  --forcer-restauration   vide la base et rejoue le dump même si elle est peuplée.
                          C'est le mode « jour J » : on repart du dump frais.
  --mysql-bin CHEMIN      client mysql (défaut : mysql du PATH).
  --php CHEMIN            binaire PHP 8.2 (défaut : php du PATH).
  --host / --port / --user   connexion (défauts : 127.0.0.1 / 3306 / root).
                          Le mot de passe se passe par MYSQL_PWD, jamais en argument.
  --racine CHEMIN         racine Laravel (défaut : déduite de l'emplacement du script).
  --base-reference NOM    base de travail où est construit le schéma de référence
                          (défaut : <base>__ref156). Supprimée en fin de course.
  --garder-reference      ne pas supprimer la base de référence (mise au point).
  --journal DOSSIER       où déposer photos, TSV et diff (défaut : dossier temporaire).
  --sans-migrations       s'arrête avant `artisan migrate` (mise au point uniquement).
FIN
}

# ------------------------------------------------------------------ paramètres
BASE=""; DUMP=""; FORCER_RESTAURATION=0; MYSQL_BIN="mysql"; PHP_BIN="php"
HOTE="127.0.0.1"; PORT="3306"; UTILISATEUR="root"; RACINE=""; BASE_REF=""
GARDER_REF=0; JOURNAL=""; SANS_MIGRATIONS=0

while [ $# -gt 0 ]; do
    case "$1" in
        --base) BASE="$2"; shift 2 ;;
        --dump) DUMP="$2"; shift 2 ;;
        --forcer-restauration) FORCER_RESTAURATION=1; shift ;;
        --mysql-bin) MYSQL_BIN="$2"; shift 2 ;;
        --php) PHP_BIN="$2"; shift 2 ;;
        --host) HOTE="$2"; shift 2 ;;
        --port) PORT="$2"; shift 2 ;;
        --user) UTILISATEUR="$2"; shift 2 ;;
        --racine) RACINE="$2"; shift 2 ;;
        --base-reference) BASE_REF="$2"; shift 2 ;;
        --garder-reference) GARDER_REF=1; shift ;;
        --journal) JOURNAL="$2"; shift 2 ;;
        --sans-migrations) SANS_MIGRATIONS=1; shift ;;
        -h|--help) usage; exit 0 ;;
        *) echo "Option inconnue : $1" >&2; usage >&2; exit 2 ;;
    esac
done

[ -n "$BASE" ] || { echo "ERREUR : --base est obligatoire." >&2; usage >&2; exit 2; }

ICI="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
[ -n "$RACINE" ] || RACINE="$(cd "$ICI/../.." && pwd)"
[ -n "$BASE_REF" ] || BASE_REF="${BASE}__ref156"
[ -n "$JOURNAL" ] || JOURNAL="$(mktemp -d "${TMPDIR:-/tmp}/bascule156.XXXXXX")"
mkdir -p "$JOURNAL"

EXTRACTION="$RACINE/scripts/schema/extract_schema.sql"
DIFF_SCHEMA="$RACINE/scripts/schema/diff_schema.py"
GENERATEUR="$ICI/cles_etrangeres.py"

# ------------------------------------------------------------------ utilitaires
ETAPE=0
titre() { ETAPE=$((ETAPE + 1)); printf '\n== %d. %s\n' "$ETAPE" "$1"; }
info()  { printf '   %s\n' "$1"; }
echec() { printf '\n!! ÉCHEC — %s\n' "$1" >&2; exit 1; }

sql()      { "$MYSQL_BIN" -h "$HOTE" -P "$PORT" -u "$UTILISATEUR" --protocol=TCP "$@"; }
sql_base() { sql --database="$BASE" "$@"; }
val()      { sql -N --batch -e "$1" 2>/dev/null | head -1; }
val_base() { sql_base -N --batch -e "$1" 2>/dev/null | head -1; }

artisan() {
    ( cd "$RACINE" && DB_CONNECTION=mysql DB_HOST="$HOTE" DB_PORT="$PORT" \
        DB_DATABASE="$1" DB_USERNAME="$UTILISATEUR" DB_PASSWORD="${MYSQL_PWD:-}" \
        "$PHP_BIN" artisan "${@:2}" )
}

extraire() {  # $1 = base, $2 = fichier TSV de sortie
    sed "s/@@DB@@/$1/g" "$EXTRACTION" | sql -N --batch > "$2"
}

for f in "$EXTRACTION" "$DIFF_SCHEMA" "$GENERATEUR"; do
    [ -r "$f" ] || echec "outillage introuvable : $f"
done
command -v "$MYSQL_BIN" >/dev/null 2>&1 || echec "client mysql introuvable : $MYSQL_BIN"
command -v "$PHP_BIN"   >/dev/null 2>&1 || echec "binaire PHP introuvable : $PHP_BIN"
command -v python3      >/dev/null 2>&1 || echec "python3 introuvable (outillage de schéma)"

printf '=== Séquence base de bascule — #156 ===\n'
info "base cible        : $BASE"
info "serveur           : $HOTE:$PORT (MySQL $(val 'SELECT VERSION();'))"
info "racine Laravel    : $RACINE"
info "journal           : $JOURNAL"

# =================================================================== 1. dump
titre "Restauration du dump"
sql -e "CREATE DATABASE IF NOT EXISTS \`$BASE\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" \
    || echec "création de la base impossible"
NB_TABLES="$(val "SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA='$BASE';")"

if [ -z "$DUMP" ]; then
    info "aucun --dump : la base est prise telle quelle ($NB_TABLES tables)."
elif [ "$FORCER_RESTAURATION" -eq 1 ] || [ "${NB_TABLES:-0}" -eq 0 ]; then
    [ -r "$DUMP" ] || echec "dump illisible : $DUMP"
    if [ "$FORCER_RESTAURATION" -eq 1 ] && [ "${NB_TABLES:-0}" -gt 0 ]; then
        info "--forcer-restauration : la base est vidée avant rechargement."
        sql -e "DROP DATABASE \`$BASE\`; CREATE DATABASE \`$BASE\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" \
            || echec "réinitialisation de la base impossible"
    fi
    info "chargement de $(basename "$DUMP") …"
    sql_base < "$DUMP" || echec "restauration du dump en échec"
    info "restauré : $(val "SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA='$BASE';") tables"
else
    info "base déjà peuplée ($NB_TABLES tables) : restauration ignorée (no-op)."
    info "→ c'est ce qui rend la seconde passe sans effet, sans intervention manuelle."
fi

# =================================================================== 2. photo
titre "Photo de l'état initial"
PHOTO="$JOURNAL/photo-initiale-$(date +%Y%m%d-%H%M%S).txt"
{
    echo "### MOTEURS"
    sql -N --batch -e "SELECT ENGINE, COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA='$BASE' GROUP BY ENGINE ORDER BY ENGINE;"
    echo "### COMPTAGES"
    sql -N --batch -e "SELECT TABLE_NAME, TABLE_ROWS FROM information_schema.TABLES WHERE TABLE_SCHEMA='$BASE' ORDER BY TABLE_NAME;"
    echo "### MIGRATIONS"
    sql_base -N --batch -e "SELECT COUNT(*), IFNULL(MAX(batch),0) FROM migrations;" 2>/dev/null || echo "table migrations absente"
    echo "### FKS"
    sql -N --batch -e "SELECT COUNT(DISTINCT CONSTRAINT_NAME) FROM information_schema.KEY_COLUMN_USAGE WHERE TABLE_SCHEMA='$BASE' AND REFERENCED_TABLE_NAME IS NOT NULL;"
} > "$PHOTO"
info "moteurs   : $(sql -N --batch -e "SELECT GROUP_CONCAT(CONCAT(ENGINE,'=',n)) FROM (SELECT ENGINE, COUNT(*) n FROM information_schema.TABLES WHERE TABLE_SCHEMA='$BASE' GROUP BY ENGINE) t;")"
info "clés étr. : $(val "SELECT COUNT(DISTINCT CONSTRAINT_NAME) FROM information_schema.KEY_COLUMN_USAGE WHERE TABLE_SCHEMA='$BASE' AND REFERENCED_TABLE_NAME IS NOT NULL;")"
info "migrations: $(val_base "SELECT CONCAT(COUNT(*),' lignes, batch max ',IFNULL(MAX(batch),0)) FROM migrations;")"
info "photo     : $PHOTO"

# =================================================================== 3. InnoDB
titre "Conversion des tables en InnoDB (SQL, hors migration)"
A_CONVERTIR="$(sql -N --batch -e "SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA='$BASE' AND TABLE_TYPE='BASE TABLE' AND ENGINE <> 'InnoDB' ORDER BY TABLE_NAME;")"
if [ -z "$A_CONVERTIR" ]; then
    info "0 table à convertir — no-op."
else
    NB="$(printf '%s\n' "$A_CONVERTIR" | wc -l | tr -d ' ')"
    info "$NB table(s) hors InnoDB à convertir."
    printf '%s\n' "$A_CONVERTIR" | while IFS= read -r t; do
        [ -n "$t" ] && printf 'ALTER TABLE `%s` ENGINE=InnoDB;\n' "$t"
    done > "$JOURNAL/conversion-innodb.sql"
    sql_base < "$JOURNAL/conversion-innodb.sql" || echec "conversion InnoDB en échec"
    info "converti — DDL conservée dans $JOURNAL/conversion-innodb.sql"
fi
RESTE="$(val "SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA='$BASE' AND TABLE_TYPE='BASE TABLE' AND ENGINE <> 'InnoDB';")"
[ "${RESTE:-1}" -eq 0 ] || echec "$RESTE table(s) toujours hors InnoDB"
info "contrôle  : $(val "SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA='$BASE' AND TABLE_TYPE='BASE TABLE' AND ENGINE='InnoDB';")/$(val "SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA='$BASE' AND TABLE_TYPE='BASE TABLE';") en InnoDB"

# =================================================================== 4. référence
titre "Construction du schéma de référence (migrate:fresh sur base jetable)"
sql -e "DROP DATABASE IF EXISTS \`$BASE_REF\`; CREATE DATABASE \`$BASE_REF\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" \
    || echec "création de la base de référence impossible"
artisan "$BASE_REF" migrate:fresh --force > "$JOURNAL/reference-migrate-fresh.log" 2>&1 \
    || { tail -30 "$JOURNAL/reference-migrate-fresh.log" >&2; echec "migrate:fresh de référence en échec"; }
extraire "$BASE_REF" "$JOURNAL/ref.tsv"
info "référence : $(val "SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA='$BASE_REF';") tables, $(val "SELECT COUNT(DISTINCT CONSTRAINT_NAME) FROM information_schema.KEY_COLUMN_USAGE WHERE TABLE_SCHEMA='$BASE_REF' AND REFERENCED_TABLE_NAME IS NOT NULL;") clés étrangères"

# =================================================================== 5. types
titre "Alignement des types portant une clé étrangère (D2/D3 et suivantes)"
extraire "$BASE" "$JOURNAL/cible-avant-types.tsv"

python3 "$GENERATEUR" --ref "$JOURNAL/ref.tsv" --cible "$JOURNAL/cible-avant-types.tsv" \
    --mode garde-negatifs --sans-entete > "$JOURNAL/garde-negatifs.sql" 2>/dev/null
if [ -s "$JOURNAL/garde-negatifs.sql" ]; then
    NEG="$(sql_base -N --batch < "$JOURNAL/garde-negatifs.sql" | awk -F'\t' '$2 > 0 {print}')"
    [ -z "$NEG" ] || { printf '%s\n' "$NEG" >&2; echec "valeurs négatives sur une colonne à passer en UNSIGNED — conversion refusée (perte de donnée)"; }
    info "garde-fou : aucune valeur négative sur les colonnes à passer en UNSIGNED."
fi

python3 "$GENERATEUR" --ref "$JOURNAL/ref.tsv" --cible "$JOURNAL/cible-avant-types.tsv" \
    --mode types --sans-entete > "$JOURNAL/alignement-types.sql"
NB_TYPES="$(grep -c ';' "$JOURNAL/alignement-types.sql" || true)"
if [ "${NB_TYPES:-0}" -eq 0 ]; then
    info "0 colonne à réaligner — no-op."
else
    info "$NB_TYPES colonne(s) réalignée(s) sur la référence :"
    sed 's/^/     /' "$JOURNAL/alignement-types.sql"
    sql_base < "$JOURNAL/alignement-types.sql" || echec "alignement de type en échec"
fi

# =================================================================== 6. pré-contrôle
titre "Contrôle bloquant AVANT migrations — FK posées par une migration antérieure aux purges"
# Runbook §3 ter précision 3 : l'ordre purge ↔ création de FK est le point critique.
# `2026_08_05_000001_add_courses_subcategory_foreign_key` pose une FK sur une table
# PEUPLÉE avant que les purges ea136_* (2026_08_08_*) n'aient tourné : c'est la seule
# de ce cas, et elle échouerait en errno 1452 sans ce contrôle. Les tables L1/L2 créées
# entre-temps (login_events…) naissent vides : aucun orphelin possible.
python3 "$GENERATEUR" --ref "$JOURNAL/ref.tsv" --cible "$JOURNAL/cible-avant-types.tsv" \
    --mode orphelins --table courses --sans-entete > "$JOURNAL/orphelins-courses.sql"
if [ -s "$JOURNAL/orphelins-courses.sql" ]; then
    RES="$(sql_base -N --batch < "$JOURNAL/orphelins-courses.sql")"
    printf '%s\n' "$RES" | sed 's/^/     /'
    BLOQUANTS="$(printf '%s\n' "$RES" | awk -F'\t' '$4 > 0')"
    [ -z "$BLOQUANTS" ] || echec "orphelins sur courses : la FK posée par 2026_08_05_000001 échouerait en errno 1452"
else
    info "aucune FK manquante sur courses — no-op."
fi

# =================================================================== 7. migrations
if [ "$SANS_MIGRATIONS" -eq 1 ]; then
    titre "Migrations Laravel — IGNORÉES (--sans-migrations)"
else
titre "Migrations Laravel (purges d'orphelins + rattrapages)"
artisan "$BASE" migrate --force > "$JOURNAL/migrate.log" 2>&1 \
    || { tail -40 "$JOURNAL/migrate.log" >&2; echec "artisan migrate en échec — voir $JOURNAL/migrate.log"; }
NB_JOUEES="$(grep -c 'DONE' "$JOURNAL/migrate.log" || true)"
info "$NB_JOUEES migration(s) jouée(s) — journal : $JOURNAL/migrate.log"
[ "${NB_JOUEES:-0}" -eq 0 ] && info "→ aucune migration en attente : no-op."
info "migrations: $(val_base "SELECT CONCAT(COUNT(*),' lignes, batch max ',IFNULL(MAX(batch),0)) FROM migrations;")"
fi

# =================================================================== 8. orphelins
titre "Contrôle bloquant des orphelins — APRÈS les purges, AVANT les clés étrangères"
extraire "$BASE" "$JOURNAL/cible-avant-fk.tsv"
python3 "$GENERATEUR" --ref "$JOURNAL/ref.tsv" --cible "$JOURNAL/cible-avant-fk.tsv" \
    --mode orphelins --sans-entete > "$JOURNAL/orphelins.sql"
if [ -s "$JOURNAL/orphelins.sql" ]; then
    RES="$(sql_base -N --batch < "$JOURNAL/orphelins.sql")" || echec "contrôle des orphelins impossible"
    printf '%s\n' "$RES" > "$JOURNAL/orphelins.txt"
    RESTANTS="$(printf '%s\n' "$RES" | awk -F'\t' '$4 > 0')"
    info "$(printf '%s\n' "$RES" | wc -l | tr -d ' ') relation(s) contrôlée(s), rapport : $JOURNAL/orphelins.txt"
    # Le contrôle nominatif du runbook §3 ter : les leçons, purgées par ricochet.
    printf '%s\n' "$RES" | awk -F'\t' '$1=="lessons" {printf "     lessons.%s -> %s : %s orphelin(s)\n", $2, $3, $4}'
    if [ -n "$RESTANTS" ]; then
        printf '%s\n' "$RESTANTS" | sed 's/^/     /' >&2
        echec "orphelins restants : créer les clés étrangères échouerait en errno 1452"
    fi
    info "0 orphelin sur l'ensemble des relations à poser."
else
    info "aucune clé étrangère manquante — no-op."
fi

# =================================================================== 9. clés étrangères
titre "Création des clés étrangères manquantes"
python3 "$GENERATEUR" --ref "$JOURNAL/ref.tsv" --cible "$JOURNAL/cible-avant-fk.tsv" \
    --mode fk-ddl --sans-entete > "$JOURNAL/fk.sql"
NB_FK="$(grep -c ';' "$JOURNAL/fk.sql" || true)"
if [ "${NB_FK:-0}" -eq 0 ]; then
    info "0 clé étrangère à créer — no-op."
else
    info "$NB_FK clé(s) étrangère(s) à créer — DDL : $JOURNAL/fk.sql"
    sql_base < "$JOURNAL/fk.sql" || echec "création des clés étrangères en échec (voir $JOURNAL/fk.sql)"
    info "créées."
fi
info "total en base : $(val "SELECT COUNT(DISTINCT CONSTRAINT_NAME) FROM information_schema.KEY_COLUMN_USAGE WHERE TABLE_SCHEMA='$BASE' AND REFERENCED_TABLE_NAME IS NOT NULL;") clés étrangères"

# =================================================================== 10. sortie
titre "Contrôles de sortie"
if [ "$SANS_MIGRATIONS" -eq 0 ]; then
    PRETEND="$(artisan "$BASE" migrate --pretend --force 2>&1 | tr -d '\r')"
    printf '%s\n' "$PRETEND" > "$JOURNAL/pretend.log"
    if printf '%s' "$PRETEND" | grep -qi 'Nothing to migrate'; then
        info "migrate --pretend : « Nothing to migrate » (nécessaire, NON suffisant — cf. #137)"
    else
        printf '%s\n' "$PRETEND" | tail -20 >&2
        echec "migrate --pretend annonce encore des migrations en attente"
    fi
fi

extraire "$BASE" "$JOURNAL/cible.tsv"
python3 "$DIFF_SCHEMA" "$JOURNAL/ref.tsv" "$JOURNAL/cible.tsv" > "$JOURNAL/diff.json" \
    || echec "diff de schéma impossible"
info "diff outillé : $JOURNAL/diff.json"

python3 - "$JOURNAL/diff.json" "$ICI/ecarts_attendus.json" <<'PY'
import json, sys
d = json.load(open(sys.argv[1]))
attendus = json.load(open(sys.argv[2]))
cles = ["tables_missing_in_prod", "tables_extra_in_prod", "columns_missing_in_prod",
        "columns_extra_in_prod", "columns_divergent", "indexes_missing_in_prod",
        "indexes_extra_in_prod", "fks_missing_in_prod"]


def signature(k, e):
    """Appariement par objet, jamais par nom de contrainte ni d'index."""
    if isinstance(e, str):
        return e
    return (e.get("table"), e.get("column"),
            tuple(e.get("columns") or []), e.get("unique"))


total, tolere = 0, []
for k in cles:
    prevus = {signature(k, e) for e in attendus.get(k, [])}
    restants = [e for e in d[k] if signature(k, e) not in prevus]
    vus = len(d[k]) - len(restants)
    total += len(restants)
    tolere += [(k, e) for e in d[k] if signature(k, e) in prevus]
    print("     %-26s %d%s" % (k, len(restants),
                               "  (+%d attendu(s))" % vus if vus else ""))
print("     %-26s %s" % ("moteurs cible", ",".join(d["engines"]["prod"])))
print("     %-26s %s" % ("comptes", json.dumps(d["counts"])))
for k, e in tolere:
    ref = next((a for a in attendus[k] if signature(k, a) == signature(k, e)), {})
    print("     écart ACCEPTÉ (%s) : %s — %s" % (k, json.dumps(e, ensure_ascii=False),
                                                 ref.get("source", "sans source")))
if total:
    print("\n!! %d écart(s) de schéma NON prévus (détail dans le JSON)" % total,
          file=sys.stderr)
    sys.exit(1)
print("     écart de schéma hors liste d'attendus : AUCUN")
PY
[ $? -eq 0 ] || echec "le diff de schéma rapporte des écarts non prévus par ecarts_attendus.json"

info "comptages métier :"
for t in users courses lessons sections enrollments completed_courses quizzes options; do
    n="$(val_base "SELECT COUNT(*) FROM \`$t\`;")" || n="?"
    printf '     %-20s %s\n' "$t" "${n:-absente}"
done

EMPREINTE="$(sort "$JOURNAL/cible.tsv" | shasum -a 256 | cut -d' ' -f1)"
info "empreinte du schéma cible : $EMPREINTE"
echo "$EMPREINTE" > "$JOURNAL/empreinte-schema.txt"

# =================================================================== ménage
if [ "$GARDER_REF" -eq 0 ]; then
    sql -e "DROP DATABASE IF EXISTS \`$BASE_REF\`;" >/dev/null 2>&1
fi

printf '\n=== SÉQUENCE TERMINÉE — journal : %s ===\n' "$JOURNAL"
exit 0
