#!/bin/bash
# Installe Kanadr sur un VPS Debian 13 vierge (modèle Debian de l'hébergeur), en une commande
# (D1, D125) :
#
#   curl -fsSL https://apt.kanadr.com/installer.sh | sudo bash -s -- --nom pbx.operateur.exemple
#
# Vérifie la machine (Debian 13 amd64, ressources, rien d'installé qu'il casserait), donne le nom
# à la machine, déclare le dépôt apt signé, télécharge et vérifie (SHA-256 publiées avec elles)
# les deux archives de la dernière construction de l'image (ou --version), puis fait la même
# installation que l'image (installer-machine.sh) et son premier démarrage : la machine est alors
# dans l'état de l'image après son premier démarrage. Affiche enfin le lien de mise en route (sous
# le nom et sous l'adresse IP), les ports à ouvrir et la commande de vérification. Le réseau, SSH
# et les comptes de la machine restent ceux de l'hébergeur.
#
# Relancé après une coupure, il reprend (étapes rejouables) ; relancé sur une machine installée,
# il redonne le lien. Publié par ci/image/publier.sh ; --essai vérifie la machine et affiche les
# commandes sans rien changer (ci/image/tester-vps.sh, et ci/image/verifier-vps.sh la nuit).
set -euo pipefail

aide() {
    cat <<'AIDE'
Installe Kanadr sur un VPS Debian 13 vierge (en root, ou avec sudo).

Usage : installer.sh --nom <nom complet> [options]

  --nom NOM               nom complet de la machine (pbx.operateur.exemple) : adresse du portail
  --version V             version de Kanadr (défaut : la dernière publiée)
  --sans-enregistrements  aucun volume dédié aux enregistrements d'appels
  --essai                 vérifie la machine et affiche les commandes, sans rien changer
  --forcer                passe outre les ressources insuffisantes et une machine déjà occupée
                          (dangereux : un serveur existant peut être cassé)
  --aide                  cette aide
AIDE
}

DEPOT=${KANADR_DEPOT:-https://apt.kanadr.com}
IMAGES=${KANADR_IMAGES:-$DEPOT/images}
# Racine des fichiers lus et écrits par ce script : celle des essais (tester-vps.sh), sinon /.
# Les commandes (apt-get, systemctl…) y sont alors simulées.
RACINE=${KANADR_RACINE:-}
MARQUEUR=$RACINE/var/lib/kanadr-systeme/premier-demarrage-fait
ETAT=$RACINE/var/lib/kanadr-systeme/installation-vps
JOURNAL=$RACINE/var/log/kanadr-installation.log
MESSAGE=$RACINE/usr/sbin/kanadr-message-mise-en-route
SURCHARGE=$RACINE/run/systemd/system/kanadr-premier-demarrage.service.d
# Ressources minimales : vCPU, mémoire (Kio, 3,5 Go), place libre sur / (Kio, 20 Go).
MIN_CPU=2
MIN_MEMOIRE=3670016
MIN_LIBRE=19531250
# Taille maximale du fichier des enregistrements (Go), la moitié de la place restante au plus.
ENREGISTREMENTS_MAX=50

erreur() {
    printf 'ERREUR : %s\n' "$*" >&2
    exit 1
}

attention() {
    printf 'ATTENTION : %s\n' "$*" >&2
}

nom="" version="" sans_enregistrements=0 essai=0 forcer=0

lire_options() {
    local option valeur
    while [ $# -gt 0 ]; do
        option=$1
        valeur=""
        case "$option" in
            --nom | --version)
                [ $# -ge 2 ] || erreur "$option : valeur attendue"
                valeur=$2
                shift
                ;;
        esac
        shift
        case "$option" in
            --nom) nom=$(printf '%s' "$valeur" | tr '[:upper:]' '[:lower:]') ;;
            --version) version=$valeur ;;
            --sans-enregistrements) sans_enregistrements=1 ;;
            --essai) essai=1 ;;
            --forcer) forcer=1 ;;
            --aide | --help | -h)
                aide
                exit 0
                ;;
            *) erreur "option inconnue : $option (--aide pour la liste)" ;;
        esac
    done
}

valider() {
    [ -n "$nom" ] || erreur "--nom manquant (nom complet de la machine, pbx.operateur.exemple)"
    local etiquette='[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?'
    if ! [[ $nom =~ ^$etiquette(\.$etiquette)+$ ]] || [ "${#nom}" -gt 253 ]; then
        erreur "--nom $nom : nom complet attendu (pbx.operateur.exemple), il donne l'adresse du portail"
    fi
    [ -z "$version" ] || [[ $version =~ ^[0-9][0-9A-Za-z.~+-]*$ ]] \
        || erreur "--version $version : version invalide"
}

# Commande sur la machine, ou seulement affichée (--essai), sur la sortie d'origine (descripteur
# 3) : visible même quand la commande réelle serait rendue muette.
lancer() {
    if [ "$essai" = 1 ]; then
        local mot ligne="+"
        for mot in "$@"; do
            if [[ $mot =~ ^[A-Za-z0-9_./:=,@+-]+$ ]]; then
                ligne+=" $mot"
            else
                ligne+=" '${mot//\'/\'\\\'\'}'"
            fi
        done
        printf '%s\n' "$ligne" >&3
    else
        "$@"
    fi
}

# Valeur d'une clé de /etc/os-release (guillemets retirés), sans exécuter le fichier.
os_release() {
    awk -F= -v cle="$1" '$1 == cle { v = $2; gsub(/^["'\'']|["'\'']$/, "", v); print v; exit }' \
        "$RACINE/etc/os-release" 2>/dev/null || true
}

paquet_installe() {
    dpkg-query -W -f '${Status}' "$1" 2>/dev/null | grep -q 'install ok installed'
}

verifier_systeme() {
    [ "$(id -u)" = 0 ] || erreur "à lancer en root : curl … | sudo bash -s -- --nom …"
    local id_os version_os arch
    id_os=$(os_release ID)
    version_os=$(os_release VERSION_ID)
    if [ "$id_os" != debian ] || [ "$version_os" != 13 ]; then
        erreur "Debian 13 attendue, trouvé : ${id_os:-inconnu} ${version_os:-?} (choisir le modèle Debian 13 de l'hébergeur)"
    fi
    arch=$(dpkg --print-architecture 2>/dev/null || true)
    [ "$arch" = amd64 ] || erreur "architecture amd64 attendue, trouvé : ${arch:-inconnue}"
    local outil
    for outil in curl sha256sum systemctl tar; do
        command -v "$outil" >/dev/null || erreur "$outil introuvable (apt install $outil)"
    done
}

verifier_ressources() {
    local cpu memoire libre manque=()
    cpu=$(nproc 2>/dev/null || echo 0)
    memoire=$(awk '$1 == "MemTotal:" { print $2 }' "$RACINE/proc/meminfo" 2>/dev/null || true)
    libre=$(df -Pk "$RACINE/" 2>/dev/null | awk 'NR == 2 { print $4 }')
    [ "$cpu" -ge "$MIN_CPU" ] || manque+=("$cpu vCPU (2 au moins)")
    [ "${memoire:-0}" -ge "$MIN_MEMOIRE" ] \
        || manque+=("$(awk -v k="${memoire:-0}" 'BEGIN { printf "%.1f", k / 1048576 }' | tr . ,) Go de mémoire (3,5 Go au moins)")
    [ "${libre:-0}" -ge "$MIN_LIBRE" ] \
        || manque+=("$((${libre:-0} / 1000000)) Go libres sur / (20 Go au moins)")
    [ ${#manque[@]} -eq 0 ] && return 0
    local m
    for m in "${manque[@]}"; do
        attention "ressources insuffisantes : $m"
    done
    if [ "$forcer" = 1 ]; then
        attention "--forcer : installation malgré tout (voir Dimensionnement dans le guide)"
    else
        erreur "machine trop petite pour Kanadr : prendre un VPS plus grand, ou relancer avec --forcer"
    fi
}

# Ce que l'installation casserait : une liste, vide si la machine est vierge.
occupations() {
    local p base oid
    for p in kanadr centrex-v3 freeswitch fusionpbx; do
        if paquet_installe "$p"; then echo "paquet $p installé"; fi
    done
    [ ! -e "$RACINE/var/www/fusionpbx" ] || echo "FusionPBX présent (/var/www/fusionpbx)"
    [ ! -e "$RACINE/etc/fusionpbx" ] || echo "FusionPBX configuré (/etc/fusionpbx)"
    [ ! -e "$RACINE/usr/bin/freeswitch" ] || echo "FreeSWITCH présent (/usr/bin/freeswitch)"
    [ ! -e "$RACINE/etc/freeswitch" ] || echo "FreeSWITCH configuré (/etc/freeswitch)"
    # Bases PostgreSQL créées (identifiant 16384 ou plus : ni modèles, ni « postgres »).
    for base in "$RACINE"/var/lib/postgresql/*/main/base/*; do
        [ -d "$base" ] || continue
        oid=${base##*/}
        if [[ $oid =~ ^[0-9]+$ ]] && [ "$oid" -ge 16384 ]; then
            echo "PostgreSQL avec des bases ($(dirname "$(dirname "$base")"))"
            break
        fi
    done
    for p in "$RACINE"/etc/nginx/sites-enabled/* "$RACINE"/etc/nginx/conf.d/*.conf; do
        [ -e "$p" ] || continue
        case "${p##*/}" in default | kanadr) continue ;; esac
        echo "nginx configuré (${p#"$RACINE"})"
    done
    [ ! -d "$RACINE/etc/apache2/sites-enabled" ] || echo "Apache présent (/etc/apache2)"
    if command -v ss >/dev/null; then
        local ports
        ports=$(ss -Hltnu 2>/dev/null | awk '{ n = split($5, a, ":"); print a[n] }' \
            | grep -xE '80|443|5060|5061|5080' | sort -nu | tr '\n' ' ' || true)
        [ -z "$ports" ] || echo "ports déjà ouverts par un autre service : ${ports% }"
    fi
}

verifier_occupation() {
    local liste
    liste=$(occupations)
    [ -n "$liste" ] || return 0
    printf '%s\n' "$liste" | while read -r ligne; do attention "$ligne"; done
    if [ "$forcer" = 1 ]; then
        attention "--forcer : installation par-dessus (ce qui existe peut cesser de marcher)"
    else
        erreur "machine déjà occupée : Kanadr s'installe sur un VPS Debian 13 vierge (réinstaller le modèle Debian 13 de l'hébergeur, ou --forcer à vos risques)"
    fi
}

# Adresses IPv4 de la machine, une par ligne.
adresses_locales() {
    ip -4 -o addr show scope global 2>/dev/null | awk '{ split($4, a, "/"); print a[1] }'
}

joindre() { paste -sd ' ' -; }

# Le nom pointe-t-il vers la machine ? Simple avertissement : le lien par adresse IP marche sans.
verifier_dns() {
    local resolues locales
    # Par le DNS seulement : /etc/hosts donne déjà le nom à 127.0.1.1 lors d'une reprise.
    resolues=$(getent -s dns ahostsv4 "$nom" 2>/dev/null | awk '{ print $1 }' | sort -u || true)
    locales=$(adresses_locales | sort -u)
    if [ -z "$resolues" ]; then
        attention "$nom ne résout pas encore : créer l'enregistrement DNS (A) vers $(adresses_locales | head -n 1) avant l'étape Certificat ; le lien par adresse IP marche sans"
    elif [ -z "$(comm -12 <(printf '%s\n' "$resolues") <(printf '%s\n' "$locales"))" ]; then
        attention "$nom résout vers $(joindre <<<"$resolues"), et non vers cette machine ($(joindre <<<"$locales")) : corriger le DNS (ou, derrière une traduction d'adresses, vérifier qu'il s'agit bien de l'adresse publique)"
    fi
}

# Nom complet de la machine, gardé aux redémarrages : cloud-init de l'hébergeur remettrait sinon
# le sien (vps-….hébergeur), et le premier démarrage le prend pour adresse publique.
donner_le_nom() {
    local court=${nom%%.*}
    if command -v hostnamectl >/dev/null && hostnamectl set-hostname "$nom" 2>/dev/null; then
        :
    else
        printf '%s\n' "$nom" > "$RACINE/etc/hostname"
        hostname "$nom"
    fi
    if grep -q '^127\.0\.1\.1[[:space:]]' "$RACINE/etc/hosts"; then
        sed -i "s/^127\.0\.1\.1[[:space:]].*/127.0.1.1\t$nom $court/" "$RACINE/etc/hosts"
    else
        printf '127.0.1.1\t%s %s\n' "$nom" "$court" >> "$RACINE/etc/hosts"
    fi
    if [ -d "$RACINE/etc/cloud/cloud.cfg.d" ]; then
        cat > "$RACINE/etc/cloud/cloud.cfg.d/90-kanadr-nom.cfg" <<EOF
# Kanadr (installeur des VPS) : nom de la machine gardé, pas celui de l'hébergeur.
preserve_hostname: true
fqdn: $nom
hostname: $court
prefer_fqdn_over_hostname: true
EOF
    fi
}

declarer_depot() {
    lancer curl -fsSL -o "$RACINE/usr/share/keyrings/kanadr.gpg" "$DEPOT/kanadr.gpg" \
        || erreur "clé du dépôt introuvable : $DEPOT/kanadr.gpg"
    if [ "$essai" = 1 ]; then
        lancer tee /etc/apt/sources.list.d/kanadr.sources
    else
        cat > "$RACINE/etc/apt/sources.list.d/kanadr.sources" <<EOF
Types: deb
URIs: $DEPOT
Suites: stable
Components: main
Signed-By: /usr/share/keyrings/kanadr.gpg
EOF
    fi
    lancer apt-get update -qq || erreur "dépôt $DEPOT injoignable ou refusé (apt-get update)"
}

# Empreintes de la construction, puis ses deux archives, vérifiées dans $travail. Pose $version.
telecharger() {
    local empreintes=kanadr.sha256 f
    [ -z "$version" ] || empreintes=kanadr-$version.sha256
    if [ "$essai" = 1 ]; then
        lancer curl -fsSL -o "$travail/$empreintes" "$IMAGES/$empreintes"
        version=${version:-VERSION}
        for f in "kanadr-$version-pbx.tar.xz" "kanadr-$version-fichiers.tar.gz"; do
            lancer curl -fsSL -o "$travail/$f" "$IMAGES/$f"
        done
        lancer sha256sum -c --ignore-missing "$empreintes"
        return
    fi
    curl -fsSL -o "$travail/$empreintes" "$IMAGES/$empreintes" \
        || erreur "version introuvable : $IMAGES/$empreintes (seule la dernière est publiée)"
    local pbx
    pbx=$(awk '$2 ~ /^kanadr-[0-9][0-9A-Za-z.~+-]*-pbx\.tar\.xz$/ { print $2; exit }' \
        "$travail/$empreintes")
    [ -n "$pbx" ] || erreur "$empreintes : aucune archive pour les VPS"
    version=${pbx#kanadr-}
    version=${version%-pbx.tar.xz}
    for f in "kanadr-$version-pbx.tar.xz" "kanadr-$version-fichiers.tar.gz"; do
        echo "Téléchargement de $f…"
        curl -fsSL --retry 3 -o "$travail/$f" "$IMAGES/$f" \
            || erreur "téléchargement de $IMAGES/$f interrompu"
        (cd "$travail" && grep -F "  $f" "$empreintes" | sha256sum -c --quiet - >/dev/null 2>&1) \
            || erreur "empreinte SHA-256 fausse pour $f : fichier corrompu ; rien d'installé"
    done
    echo "Empreintes SHA-256 vérifiées (Kanadr $version)."
}

# Taille du fichier des enregistrements (D101) : la moitié de la place restante, 50 Go au plus ;
# « aucun » sous 2 Go, ou avec --sans-enregistrements. Un disque vide ajouté passe avant.
taille_enregistrements() {
    if [ "$sans_enregistrements" = 1 ]; then
        echo aucun
        return
    fi
    local libre_go
    libre_go=$(df -Pk "$RACINE/" | awk 'NR == 2 { print int($4 / 1048576) }')
    local go=$((libre_go / 2))
    [ "$go" -le "$ENREGISTREMENTS_MAX" ] || go=$ENREGISTREMENTS_MAX
    if [ "$go" -lt 2 ]; then
        attention "trop peu de place pour un volume des enregistrements ($libre_go Go libres) : aucun"
        echo aucun
    else
        echo "${go}G"
    fi
}

lien_pret() {
    texte=$("$MESSAGE" --texte 2>/dev/null) || return 1
    [[ $texte == *"/mise-en-route#"* ]]
}

afficher_fin() {
    local adresse
    adresse=$(adresses_locales | head -n 1)
    echo
    echo "Kanadr est installé : $nom (${adresse:-adresse inconnue})"
    echo
    if [ -n "$texte" ]; then
        printf '%s\n' "$texte"
    else
        echo "Instance déjà initialisée : se connecter au portail, https://$nom"
    fi
    cat <<EOF

Ports à ouvrir vers la machine (pare-feu de l'hébergeur) :
  443/tcp et 80/tcp              portail, API, certificat Let's Encrypt
  5060/udp+tcp, 5061/tcp         SIP des téléphones (5061 : TLS)
  5080/udp+tcp                   SIP du trunk (limitable aux adresses du fournisseur)
  16384-32768/udp                voix (RTP)
  3478/udp+tcp, 5349/tcp         TURN du téléphone sur l'ordinateur
  40000-41999/udp                relais du TURN
  22/tcp                         SSH, depuis vos adresses seulement

Vérifier la machine : sudo kanadr verifier
EOF
}

travail=""
fin() {
    local statut=$1
    [ -z "$travail" ] || rm -rf "$travail"
    if [ "$statut" != 0 ] && [ "$essai" = 0 ] && [ -e "$ETAT" ]; then
        echo "Installation interrompue : relancer la même commande pour la reprendre (journal : $JOURNAL)." >&2
    fi
}

principal() {
    lire_options "$@"
    valider
    trap 'fin $?' EXIT
    verifier_systeme

    # Déjà installée : le lien, rien d'autre.
    if [ -e "$MARQUEUR" ]; then
        echo "Kanadr est déjà installé sur cette machine ; rien à faire."
        texte=""
        lien_pret || texte=""
        afficher_fin
        return 0
    fi
    # Installation commencée par ce script et interrompue : elle reprend, sous le même nom.
    local reprise=0
    if [ -e "$ETAT" ]; then
        local avant
        avant=$(sed -n 's/^nom=//p' "$ETAT")
        [ "$avant" = "$nom" ] \
            || erreur "installation commencée sous le nom $avant : relancer avec --nom $avant"
        reprise=1
        echo "Reprise de l'installation commencée le $(sed -n 's/^debut=//p' "$ETAT")."
    fi

    verifier_ressources
    [ "$reprise" = 1 ] || verifier_occupation
    verifier_dns

    travail=$(mktemp -d "${TMPDIR:-/var/tmp}/kanadr.XXXXXX")
    if [ "$essai" = 1 ]; then
        echo "Essai : la machine convient ; commandes affichées, rien n'est changé."
    else
        install -d -m 0755 "$(dirname "$ETAT")"
        [ "$reprise" = 1 ] || printf 'nom=%s\ndebut=%s\n' "$nom" "$(date '+%d/%m/%Y %H:%M')" > "$ETAT"
        donner_le_nom
        echo "Nom de la machine : $nom"
    fi

    declarer_depot
    telecharger
    lancer mkdir -p "$travail/fichiers"
    lancer tar -xzf "$travail/kanadr-$version-fichiers.tar.gz" -C "$travail/fichiers"
    lancer mv "$travail/kanadr-$version-pbx.tar.xz" "$travail/fichiers/pbx.tar.xz"
    if [ "$essai" = 0 ]; then
        apt-cache madison kanadr 2>/dev/null | awk -F' *[|] *' '{ print $2 }' \
            | grep -qxF "$version" \
            || erreur "kanadr $version absent du dépôt $DEPOT (apt-cache madison kanadr)"
        echo "Installation des paquets, de FreeSWITCH et de FusionPBX (5 à 15 minutes ; journal : $JOURNAL)…"
        if ! KANADR_VERSION=$version sh "$travail/fichiers/installer-machine.sh" "$travail/fichiers" \
            >>"$JOURNAL" 2>&1; then
            tail -n 30 "$JOURNAL" >&2
            erreur "installation en échec (fin du journal ci-dessus)"
        fi
    else
        lancer env "KANADR_VERSION=$version" sh "$travail/fichiers/installer-machine.sh" \
            "$travail/fichiers"
    fi

    # Premier démarrage, par son service comme sur l'image : il survit à une coupure de la
    # connexion SSH. Taille des enregistrements par une surcharge provisoire (/run).
    local taille
    taille=$(taille_enregistrements)
    if [ "$essai" = 1 ]; then
        lancer tee "$SURCHARGE/installation.conf"
        lancer systemctl start kanadr-premier-demarrage.service
        echo "Essai terminé : rien n'a été changé (volume des enregistrements : $taille)."
        return 0
    fi
    mkdir -p "$SURCHARGE"
    printf '[Service]\nEnvironment=KANADR_ENREGISTREMENTS=%s\n' "$taille" > "$SURCHARGE/installation.conf"
    systemctl daemon-reload
    systemctl reset-failed kanadr-premier-demarrage.service 2>/dev/null || true
    echo "Premier démarrage : secrets, base, FreeSWITCH, services (une à trois minutes)…"
    if ! systemctl start kanadr-premier-demarrage.service; then
        journalctl -b -u kanadr-premier-demarrage.service --no-pager -n 30 >&2 || true
        erreur "premier démarrage en échec (journal ci-dessus ; complet : journalctl -u kanadr-premier-demarrage)"
    fi
    rm -rf "$SURCHARGE"
    systemctl daemon-reload
    [ -e "$MARQUEUR" ] || erreur "premier démarrage inachevé : journalctl -u kanadr-premier-demarrage"
    texte=""
    local i=0
    until lien_pret; do
        i=$((i + 1))
        [ "$i" -le 90 ] || erreur "lien de mise en route absent après 3 min : sudo kanadr mise-en-route"
        sleep 2
    done
    rm -f "$ETAT"
    afficher_fin
}

# Tout le script est lu avant de commencer (curl … | bash) ; rien ne lit l'entrée standard.
exec 3>&1
principal "$@" </dev/null
