#!/usr/bin/env bash
#
# merge_train.sh — merge un train de PR en refusant les pièges connus.
#
# Deux incidents vécus, tous deux sur des PR empilées (07/08 puis 09/08) :
# une PR dont la base est une AUTRE branche de fonctionnalité fusionne dans
# cette branche, pas dans `development`. GitHub ne re-cible la base que si la
# branche de base est supprimée — ce qui n'est pas le cas avec
# `--delete-branch=false`. Résultat : le merge « réussit », les issues passent
# en recette, et le code n'est nulle part.
#
# Ce script rend l'erreur impossible :
#   1. il REFUSE toute PR dont la base n'est pas la cible ;
#   2. après merge, il VÉRIFIE que chaque commit de chaque PR est bien contenu
#      dans la cible — c'est le contrôle qui manquait, et qu'aucune relecture
#      humaine ne fait de manière fiable à 1 h du matin.
#
# Usage :
#   scripts/merge_train.sh --base development 170 171 172
#   scripts/merge_train.sh --base development --top 174        # pile : sommet seul
#   scripts/merge_train.sh --base main --dry-run 175 176       # répétition
#
set -euo pipefail

BASE="development"
DRY_RUN=0
TOP_ONLY=0
PRS=()

usage() {
    sed -n '2,30p' "$0" | sed 's/^# \{0,1\}//'
    exit 1
}

while [[ $# -gt 0 ]]; do
    case "$1" in
        --base) BASE="$2"; shift 2 ;;
        --dry-run) DRY_RUN=1; shift ;;
        --top) TOP_ONLY=1; shift ;;
        -h|--help) usage ;;
        *) PRS+=("$1"); shift ;;
    esac
done

[[ ${#PRS[@]} -eq 0 ]] && usage

rouge() { printf '\033[31m%s\033[0m\n' "$*"; }
vert()  { printf '\033[32m%s\033[0m\n' "$*"; }
gris()  { printf '\033[90m%s\033[0m\n' "$*"; }

echo "── Train de merge vers '$BASE' ──"
[[ $DRY_RUN -eq 1 ]] && gris "(répétition : aucun merge ne sera exécuté)"

# ── 1. Contrôle préalable : bases, état, conflits ────────────────────────────
REFUS=0
for pr in "${PRS[@]}"; do
    lecture=$(gh pr view "$pr" --json baseRefName,headRefName,state,mergeable,title)
    base=$(jq -r .baseRefName <<<"$lecture")
    head=$(jq -r .headRefName <<<"$lecture")
    etat=$(jq -r .state <<<"$lecture")
    fusionnable=$(jq -r .mergeable <<<"$lecture")
    titre=$(jq -r .title <<<"$lecture")

    printf '#%-5s %-52s ' "$pr" "${titre:0:52}"

    if [[ "$etat" != "OPEN" ]]; then
        rouge "REFUS — état $etat"
        REFUS=1
        continue
    fi

    if [[ "$base" != "$BASE" ]]; then
        rouge "REFUS — base '$base' au lieu de '$BASE'"
        gris "        → gh pr edit $pr --base $BASE"
        gris "        → ou ne merger que le sommet de la pile : --top"
        REFUS=1
        continue
    fi

    if [[ "$fusionnable" == "CONFLICTING" ]]; then
        rouge "REFUS — conflits à résoudre"
        REFUS=1
        continue
    fi

    vert "OK ($head)"
done

if [[ $REFUS -eq 1 ]]; then
    echo
    rouge "Train interrompu : aucune PR n'a été mergée."
    rouge "Corrige les bases ci-dessus, puis relance. C'est exactement l'incident"
    rouge "des 07/08 et 09/08 — la base d'une PR empilée n'est pas re-ciblée toute seule."
    exit 2
fi

if [[ $TOP_ONLY -eq 1 && ${#PRS[@]} -gt 1 ]]; then
    echo
    rouge "--top attend une seule PR (le sommet de la pile, qui contient tout)."
    exit 2
fi

[[ $DRY_RUN -eq 1 ]] && { echo; vert "Répétition terminée : le train passerait."; exit 0; }

# ── 2. Merge ────────────────────────────────────────────────────────────────
echo
for pr in "${PRS[@]}"; do
    printf 'merge #%s … ' "$pr"
    gh pr merge "$pr" --merge >/dev/null
    vert "fait"
    sleep 2
done

# ── 3. Vérification du contenu réel — le contrôle qui manquait ───────────────
echo
echo "── Vérification : chaque commit est-il dans '$BASE' ? ──"
git fetch origin --quiet

MANQUANT=0
for pr in "${PRS[@]}"; do
    commits=$(gh pr view "$pr" --json commits --jq '.commits[].oid')
    absents=0
    total=0
    for sha in $commits; do
        total=$((total + 1))
        if ! git merge-base --is-ancestor "$sha" "origin/$BASE" 2>/dev/null; then
            absents=$((absents + 1))
        fi
    done

    printf '#%-5s %2d commit(s) — ' "$pr" "$total"
    if [[ $absents -eq 0 ]]; then
        vert "tous présents dans $BASE"
    else
        rouge "$absents commit(s) ABSENTS de $BASE"
        MANQUANT=1
    fi
done

echo
if [[ $MANQUANT -eq 1 ]]; then
    rouge "⚠ Le merge a « réussi » mais du code n'est pas arrivé dans '$BASE'."
    rouge "  Ne passe AUCUNE issue en recette. Ouvre une PR depuis le sommet de la pile"
    rouge "  (il contient tout) vers '$BASE', puis relance cette vérification."
    exit 3
fi

vert "Train complet : le contenu de toutes les PR est dans '$BASE'."

# ── 4. Passage des issues en recette ────────────────────────────────────────
# Le label `statut: en recette` est la SEULE réponse fiable à « qu'est-ce qui est
# développé ? ». Les dates de jalon sont un plan, le compteur d'issues fermées
# reflète la cadence de contre-recette — ni l'un ni l'autre ne dit ce qui tourne.
#
# Il était tenu à la main, donc il a dérivé : le 13/08, sept issues mergées
# (#148, #313, #320, #337, #359, #363, #368) portaient encore `statut: à faire`.
# Un label faux est pire qu'un label absent — il se lit comme une information.
#
# D'où l'automatisation ICI : le seul endroit qui SAIT qu'un merge a réussi ET
# que le code est bien arrivé (vérification ci-dessus). Le label devient une
# conséquence du merge, plus une discipline.
#
# Les issues visées sont celles que la PR désigne explicitement :
#   - le préfixe numérique de la branche (convention « 1 issue = 1 branche ») ;
#   - une ligne « Issue : #N » du corps de la PR.
# Volontairement PAS tous les `#N` du corps : une PR cite couramment les issues
# voisines, et étiqueter une issue voisine « en recette » serait exactement le
# mensonge qu'on cherche à supprimer.
#
# Non bloquant : un échec d'étiquetage ne remet pas en cause un merge réussi.
echo
echo "── Passage en recette ──"
for pr in "${PRS[@]}"; do
    lecture=$(gh pr view "$pr" --json headRefName,body 2>/dev/null) || continue
    branche=$(jq -r .headRefName <<<"$lecture")
    corps=$(jq -r .body <<<"$lecture")

    # « Partiellement livrée » n'existe pas : une issue est livrée ou elle ne
    # l'est pas (décision Enguerran, 13/08). Une PR qui ne fait qu'une TRANCHE
    # de son issue le déclare par « Issue (partielle) : #N » — l'issue reste
    # alors dans son état, et le train dit qu'elle doit être DÉCOUPÉE.
    #
    # Sans ce garde-fou, l'automatisation d'hier produirait exactement le
    # mensonge qu'elle est censée supprimer : #180 (25 écrans à migrer) est
    # passée « en recette » sur le merge d'un premier écran.
    partielles=$(grep -oiE '^ *issues? *\(partielles?\) *: *#[0-9]+' <<<"$corps" | grep -oE '[0-9]+' | sort -u || true)

    issues=$(
        {
            [[ "$branche" =~ ^([0-9]+)- ]] && echo "${BASH_REMATCH[1]}"
            grep -oiE '^ *issues? *: *#[0-9]+' <<<"$corps" | grep -oE '[0-9]+' || true
        } | sort -u
    )

    for partielle in $partielles; do
        issues=$(grep -vx "$partielle" <<<"$issues" || true)
        rouge "  #$pr → issue #$partielle : livraison PARTIELLE, pas d'étiquetage"
        gris "        une issue est livrée ou ne l'est pas — découpe-la :"
        gris "        la tranche livrée devient sa propre issue (en recette),"
        gris "        #$partielle garde le reste du CA et reste « à faire »."
    done

    if [[ -z "$issues" ]]; then
        gris "  #$pr — aucune issue désignée (branche '$branche', pas de ligne « Issue : #N »)"
        continue
    fi

    for issue in $issues; do
        etat_issue=$(gh issue view "$issue" --json state --jq .state 2>/dev/null) || {
            gris "  #$pr → issue #$issue introuvable"
            continue
        }

        # Une issue déjà fermée a été traitée à la main, avec un motif : on ne la
        # rouvre pas et on ne la ré-étiquette pas.
        if [[ "$etat_issue" != "OPEN" ]]; then
            gris "  #$pr → issue #$issue déjà fermée, laissée telle quelle"
            continue
        fi

        if gh issue edit "$issue" \
            --add-label "statut: en recette" \
            --remove-label "statut: à faire" \
            --remove-label "statut: en dev" \
            --remove-label "statut: PR ouverte" >/dev/null 2>&1; then
            vert "  #$pr → issue #$issue : en recette"
        else
            gris "  #$pr → issue #$issue : étiquetage impossible (label absent du dépôt ?)"
        fi
    done
done

# ── Audit du suivi de sprint ────────────────────────────────────────────────
# Accroché ICI et pas ailleurs : c'est la commande qu'on lance à chaque merge, donc la
# seule qui s'exécute sans qu'on ait à y penser. Un script qu'il faut penser à lancer
# n'est qu'une convention de plus — et une convention se respecte jusqu'au jour où on
# est pressé. C'est très exactement ce qui a laissé dériver dix issues front hors de
# tout jalon pendant tout le lot L4.
#
# Non bloquant : un écart de suivi ne doit jamais empêcher un merge. Il doit se voir.
if [[ -f "$(dirname "$0")/audit_sprints.py" ]]; then
    echo
    echo "── Suivi de sprint (les deux dépôts) ──"
    python3 "$(dirname "$0")/audit_sprints.py" 2>/dev/null | grep -E "⚠️|✅|📋|écart" | head -8 \
        || gris "  (audit indisponible — gh authentifié ?)"
fi

if [[ "$BASE" == "main" ]]; then
    echo
    echo "── Passage en production : à faire maintenant ──"
    gris "1. Migrations en attente :"
    git diff --name-only "origin/main@{1}" origin/main -- database/migrations 2>/dev/null | sed 's/^/     /' || true
    gris "2. Déployer selon docs/roadmap/12_PROCEDURE_DEPLOIEMENT.md"
    gris "3. Vérifier un LIEN PROFOND, pas seulement la racine :"
    gris "     curl -s -o /dev/null -w '%{http_code}\\n' https://<site>/courses/<slug-existant>"
    gris "   (200 attendu — le .htaccess de routage SPA est la victime classique du rsync --delete)"
fi
