#!/usr/bin/env bash
set -Eeuo pipefail
IFS=$'\n\t'
umask 027

PROGRAM=${0##*/}
SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
TEMPLATE_DIR="$SCRIPT_DIR/templates"
ENV_FILE=
DRY_RUN=no
CONFIG_ONLY=no
OUTPUT_DIR=
CHECK_ONLY=no
VERBOSE=no
CLI_VERBOSE=no
CLI_VAULT_ADDR=
CLI_VAULT_CREDENTIAL=
CLI_VAULT_TOKEN_FILE=
CLI_VAULT_SECRET_PATH=
CLI_VAULT_AUTH=
CLI_VAULT_USER=
CLI_VAULT_PASSWORD_FILE=
CLI_VAULT_ENVIRONMENT=
CLI_VAULT_BASE_PATH=
CLI_SCOG_ACCOUNT_ID=
WORK_DIR=
BACKUP_DIR=
VAULT_JSON=
VAULT_TOKEN=
ASTERISK_CLI_OUTPUT=

log()  { printf '[INFO] %s\n' "$*"; }
warn() { printf '[WARN] %s\n' "$*" >&2; }
die()  { printf '[ERROR] %s\n' "$*" >&2; exit 1; }
vault_trace() {
  [[ "$VERBOSE" == yes ]] || return 0
  printf '[VERBOSE] Vault HTTP: %s\n' "$*" >&2
}

vault_response_trace() {
  local response=$1
  [[ "$VERBOSE" == yes ]] || return 0
  printf '[VERBOSE] Vault HTTP response:\n%s\n' "$response" >&2
}

usage() {
  cat <<EOF
Usage: sudo $PROGRAM [--env-file FILE] [options]

  --env-file FILE  Paramètres publics et chemins vers les secrets.
                   Par défaut : install.env placé à côté de ce script.
  --config-only    Ne pas réinstaller les trois packages.
  --dry-run        Préparer et contrôler sans modifier /etc ni les services.
  --output-dir DIR Conserver une copie des configurations générées.
  -t, --check      Tester Vault et quitter sans installer ni configurer.
  -v, --verbose    Afficher les requêtes/réponses Vault, secrets inclus.
  --vault-url URL  Surcharger VAULT_ADDR.
  --vault-path P   Surcharger VAULT_SECRET_PATH (chemin API KV v2).
  --vault-credential TOKEN
                   Utiliser ce token Vault. Préférer --vault-token-file.
  --vault-token-file FILE
                   Lire le token Vault depuis FILE.
  --vault-auth MODE
                   Authentification token, ldap ou approle.
  --vault-user USER Identifiant LDAP Vault.
  --vault-password-file FILE
                   Lire le mot de passe LDAP depuis FILE.
  --vault-environment ENV
                   Environnement de secrets, par exemple hpr ou prd.
  --vault-base-path PATH
                   Racine KV v2 contenant apim, kafka et scog.
  --scog-account-id ID
                   Préfixe des clés SCOG <ID>_KEY et <ID>_SECRET.
EOF
}

while (($#)); do
  case "$1" in
    --env-file) (($# >= 2)) || die "--env-file nécessite un chemin"; ENV_FILE=$2; shift 2 ;;
    --config-only) CONFIG_ONLY=yes; shift ;;
    --dry-run) DRY_RUN=yes; shift ;;
    --output-dir) (($# >= 2)) || die "--output-dir nécessite un chemin"; OUTPUT_DIR=$2; shift 2 ;;
    -t|--check) CHECK_ONLY=yes; shift ;;
    -v|--verbose) CLI_VERBOSE=yes; shift ;;
    --vault-url) (($# >= 2)) || die "--vault-url nécessite une URL"; CLI_VAULT_ADDR=$2; shift 2 ;;
    --vault-path) (($# >= 2)) || die "--vault-path nécessite un chemin"; CLI_VAULT_SECRET_PATH=$2; shift 2 ;;
    --vault-credential) (($# >= 2)) || die "--vault-credential nécessite un token"; CLI_VAULT_CREDENTIAL=$2; shift 2 ;;
    --vault-token-file) (($# >= 2)) || die "--vault-token-file nécessite un chemin"; CLI_VAULT_TOKEN_FILE=$2; shift 2 ;;
    --vault-auth) (($# >= 2)) || die "--vault-auth nécessite un mode"; CLI_VAULT_AUTH=$2; shift 2 ;;
    --vault-user) (($# >= 2)) || die "--vault-user nécessite une valeur"; CLI_VAULT_USER=$2; shift 2 ;;
    --vault-password-file) (($# >= 2)) || die "--vault-password-file nécessite un chemin"; CLI_VAULT_PASSWORD_FILE=$2; shift 2 ;;
    --vault-environment) (($# >= 2)) || die "--vault-environment nécessite une valeur"; CLI_VAULT_ENVIRONMENT=$2; shift 2 ;;
    --vault-base-path) (($# >= 2)) || die "--vault-base-path nécessite une valeur"; CLI_VAULT_BASE_PATH=$2; shift 2 ;;
    --scog-account-id) (($# >= 2)) || die "--scog-account-id nécessite une valeur"; CLI_SCOG_ACCOUNT_ID=$2; shift 2 ;;
    -h|--help) usage; exit 0 ;;
    *) die "Option inconnue : $1" ;;
  esac
done

if [[ -z "$ENV_FILE" && -f "$SCRIPT_DIR/install.env" ]]; then
  ENV_FILE="$SCRIPT_DIR/install.env"
  log "Utilisation du fichier d'environnement par défaut : $ENV_FILE"
fi

[[ -n "$ENV_FILE" ]] || die \
  "Aucun $SCRIPT_DIR/install.env présent ; utiliser --env-file FILE"
[[ -r "$ENV_FILE" ]] || die "Fichier d'environnement illisible : $ENV_FILE"

# Entrée administrative de confiance : uniquement des affectations shell.
# shellcheck disable=SC1090
source "$ENV_FILE"

[[ "$CLI_VERBOSE" != yes ]] || VERBOSE=yes
if [[ "$VERBOSE" == yes ]]; then
  warn "Verbose mode exposes Vault credentials and secrets in clear text."
fi

# Les valeurs explicites de la ligne de commande ont priorité sur le fichier.
[[ -z "$CLI_VAULT_ADDR" ]] || VAULT_ADDR=$CLI_VAULT_ADDR
[[ -z "$CLI_VAULT_SECRET_PATH" ]] || VAULT_SECRET_PATH=$CLI_VAULT_SECRET_PATH
[[ -z "$CLI_VAULT_AUTH" ]] || VAULT_AUTH=$CLI_VAULT_AUTH
[[ -z "$CLI_VAULT_USER" ]] || VAULT_LDAP_USERNAME=$CLI_VAULT_USER
[[ -z "$CLI_VAULT_PASSWORD_FILE" ]] || VAULT_LDAP_PASSWORD_FILE=$CLI_VAULT_PASSWORD_FILE
[[ -z "$CLI_VAULT_ENVIRONMENT" ]] || VAULT_ENVIRONMENT=$CLI_VAULT_ENVIRONMENT
[[ -z "$CLI_VAULT_BASE_PATH" ]] || VAULT_BASE_PATH=$CLI_VAULT_BASE_PATH
[[ -z "$CLI_SCOG_ACCOUNT_ID" ]] || SCOG_ACCOUNT_ID=$CLI_SCOG_ACCOUNT_ID
if [[ -n "$CLI_VAULT_CREDENTIAL" && -n "$CLI_VAULT_TOKEN_FILE" ]]; then
  die "Choisir --vault-credential ou --vault-token-file, pas les deux"
fi
if [[ -n "$CLI_VAULT_CREDENTIAL" ]]; then
  warn "Le credential Vault fourni en ligne de commande peut être visible dans l'historique et dans ps"
  VAULT_CREDENTIAL=$CLI_VAULT_CREDENTIAL
fi
if [[ -n "$CLI_VAULT_TOKEN_FILE" ]]; then
  VAULT_AUTH=token
  VAULT_TOKEN_FILE=$CLI_VAULT_TOKEN_FILE
  VAULT_TOKEN=
fi
[[ "$CHECK_ONLY" != yes ]] || VAULT_ENABLE=yes

if [[ "$DRY_RUN" != yes && "$CHECK_ONLY" != yes && ${EUID:-$(id -u)} -ne 0 ]]; then
  die "Exécuter ce script avec root"
fi

run() {
  if [[ "$DRY_RUN" == yes ]]; then
    printf '[DRY-RUN]'; printf ' %q' "$@"; printf '\n'
  else
    "$@"
  fi
}

cleanup() {
  [[ -z "$WORK_DIR" || ! -d "$WORK_DIR" ]] || rm -rf -- "$WORK_DIR"
}
trap cleanup EXIT

require_command() {
  command -v "$1" >/dev/null 2>&1 || die "Commande requise absente : $1"
}

read_secret_file() {
  local file=$1
  [[ -r "$file" ]] || die "Secret illisible : $file"
  tr -d '\r\n' <"$file"
}

vault_request_args() {
  VAULT_CURL_ARGS=(--fail-with-body --silent --show-error --location)
  [[ -z ${VAULT_NAMESPACE:-} ]] || VAULT_CURL_ARGS+=(--header "X-Vault-Namespace: $VAULT_NAMESPACE")
  [[ -z ${VAULT_CACERT:-} ]] || VAULT_CURL_ARGS+=(--cacert "$VAULT_CACERT")
}

vault_login() {
  [[ ${VAULT_ENABLE:-no} == yes ]] || return 0
  require_command curl
  require_command jq
  : "${VAULT_ADDR:?VAULT_ADDR est obligatoire}"
  vault_request_args

  case "${VAULT_AUTH:-token}" in
    token)
      if [[ -z "$VAULT_TOKEN" ]]; then
        if [[ -n ${VAULT_CREDENTIAL:-} ]]; then
          VAULT_TOKEN=$VAULT_CREDENTIAL
        else
          : "${VAULT_TOKEN_FILE:?VAULT_TOKEN_FILE ou --vault-credential est obligatoire}"
          VAULT_TOKEN=$(read_secret_file "$VAULT_TOKEN_FILE")
        fi
      fi
      vault_trace "authentication=token; no login request; header X-Vault-Token: $VAULT_TOKEN"
      ;;
    ldap)
      : "${VAULT_LDAP_USERNAME:?VAULT_LDAP_USERNAME ou --vault-user est obligatoire}"
      local password encoded_user response
      if [[ -n ${VAULT_CREDENTIAL:-} ]]; then
        password=$VAULT_CREDENTIAL
      else
        : "${VAULT_LDAP_PASSWORD_FILE:?VAULT_LDAP_PASSWORD_FILE est obligatoire}"
        password=$(read_secret_file "$VAULT_LDAP_PASSWORD_FILE")
      fi
      encoded_user=$(jq -rn --arg value "$VAULT_LDAP_USERNAME" '$value | @uri')
      vault_trace "POST ${VAULT_ADDR%/}/v1/auth/ldap/login/$encoded_user"
      vault_trace "Content-Type: application/json; body=$(jq -nc --arg password "$password" '{password:$password}')"
      if ! response=$(jq -nc --arg password "$password" '{password:$password}' |
        curl "${VAULT_CURL_ARGS[@]}" --request POST \
          --header 'Content-Type: application/json' --data @- \
          "${VAULT_ADDR%/}/v1/auth/ldap/login/$encoded_user"); then
        vault_response_trace "$response"
        die "Échec de la requête Vault LDAP : ${VAULT_ADDR%/}/v1/auth/ldap/login/$encoded_user"
      fi
      vault_response_trace "$response"
      VAULT_TOKEN=$(jq -er '.auth.client_token' <<<"$response")
      unset password response
      ;;
    approle)
      : "${VAULT_ROLE_ID_FILE:?VAULT_ROLE_ID_FILE est obligatoire}"
      : "${VAULT_SECRET_ID_FILE:?VAULT_SECRET_ID_FILE est obligatoire}"
      local role_id secret_id response
      role_id=$(read_secret_file "$VAULT_ROLE_ID_FILE")
      secret_id=$(read_secret_file "$VAULT_SECRET_ID_FILE")
      vault_trace "POST ${VAULT_ADDR%/}/v1/auth/approle/login"
      vault_trace "Content-Type: application/json; body=$(jq -nc \
        --arg role "$role_id" --arg secret "$secret_id" \
        '{role_id:$role,secret_id:$secret}')"
      if ! response=$(jq -nc --arg role "$role_id" --arg secret "$secret_id" \
        '{role_id:$role,secret_id:$secret}' |
        curl "${VAULT_CURL_ARGS[@]}" --request POST \
          --header 'Content-Type: application/json' --data @- \
          "${VAULT_ADDR%/}/v1/auth/approle/login"); then
        vault_response_trace "$response"
        die "Échec de la requête Vault AppRole : ${VAULT_ADDR%/}/v1/auth/approle/login"
      fi
      vault_response_trace "$response"
      VAULT_TOKEN=$(jq -er '.auth.client_token' <<<"$response")
      ;;
    *) die "VAULT_AUTH doit valoir token, ldap ou approle" ;;
  esac
}

vault_read() {
  local path=$1
  local response
  vault_trace "GET ${VAULT_ADDR%/}/v1/${path#/}"
  vault_trace "X-Vault-Token: $VAULT_TOKEN"
  if ! response=$(curl "${VAULT_CURL_ARGS[@]}" \
    --header "X-Vault-Token: $VAULT_TOKEN" \
    "${VAULT_ADDR%/}/v1/${path#/}"); then
    vault_response_trace "$response"
    die "Échec de la lecture Vault : ${VAULT_ADDR%/}/v1/${path#/}"
  fi
  vault_response_trace "$response"
  jq -e '.data.data | type == "object"' >/dev/null <<<"$response" \
    || die "Le chemin Vault '$path' n'est pas un secret KV v2"
  printf '%s' "$response"
}

vault_fetch_configuration() {
  [[ ${VAULT_ENABLE:-no} == yes ]] || return 0

  if [[ -n ${VAULT_SECRET_PATH:-} ]]; then
    VAULT_JSON=$(vault_read "$VAULT_SECRET_PATH")
    return
  fi

  : "${VAULT_BASE_PATH:?VAULT_BASE_PATH est obligatoire}"
  : "${VAULT_ENVIRONMENT:?VAULT_ENVIRONMENT est obligatoire}"
  : "${SCOG_ACCOUNT_ID:?SCOG_ACCOUNT_ID est obligatoire}"

  local apim_json kafka_json scog_json key_name secret_name
  apim_json=$(vault_read "${VAULT_BASE_PATH%/}/$VAULT_ENVIRONMENT/apim")
  kafka_json=$(vault_read "${VAULT_BASE_PATH%/}/$VAULT_ENVIRONMENT/kafka")
  scog_json=$(vault_read "${VAULT_BASE_PATH%/}/$VAULT_ENVIRONMENT/scog")
  key_name="${SCOG_ACCOUNT_ID}_KEY"
  secret_name="${SCOG_ACCOUNT_ID}_SECRET"

  VAULT_JSON=$(jq -n \
    --argjson apim "$apim_json" \
    --argjson kafka "$kafka_json" \
    --argjson scog "$scog_json" \
    --arg key_name "$key_name" \
    --arg secret_name "$secret_name" '
      {
        data: { data: {
          stt_client_id: $apim.data.data.CLIENT_ID,
          stt_client_secret: $apim.data.data.CLIENT_SECRET,
          stt_token_url: $apim.data.data.URL,
          stt_url: $scog.data.data.URL,
          stt_credential: ("Basic " + (($scog.data.data[$key_name] + ":" + $scog.data.data[$secret_name]) | @base64)),
          kafka_consumer_brokers: $kafka.data.data.SERVERS,
          kafka_producer_brokers: $kafka.data.data.SERVERS,
          kafka_consumer_username: $kafka.data.data.USERNAME_CONSUMER,
          kafka_consumer_password: $kafka.data.data.PASSWORD_CONSUMER,
          kafka_producer_username: $kafka.data.data.USERNAME_PRODUCER,
          kafka_producer_password: $kafka.data.data.PASSWORD_PRODUCER,
          kafka_group_id: $kafka.data.data.GROUP_ID,
          kafka_transcription_topic: $kafka.data.data.TOPIC_TRANSCRIPTION,
          kafka_command_topic: $kafka.data.data.TOPIC_COMMANDE,
          kafka_session_topic: $kafka.data.data.TOPIC_SESSION
        } }
      }
    ')

  jq -e '
    .data.data |
    all(
      .stt_client_id, .stt_client_secret, .stt_token_url, .stt_url,
      .stt_credential, .kafka_consumer_brokers,
      .kafka_consumer_username, .kafka_consumer_password,
      .kafka_producer_username, .kafka_producer_password,
      .kafka_group_id, .kafka_transcription_topic,
      .kafka_command_topic, .kafka_session_topic;
      type == "string" and length > 0
    )
  ' >/dev/null <<<"$VAULT_JSON" || die "Un ou plusieurs paramètres Vault obligatoires sont absents"
}

check_vault() {
  vault_login
  vault_fetch_configuration
  log "Connexion Vault réussie"
  log "Serveur : ${VAULT_ADDR%/}"
  log "Authentification : ${VAULT_AUTH:-token}"
  if [[ -n ${VAULT_SECRET_PATH:-} ]]; then
    log "Secret KV v2 accessible : ${VAULT_SECRET_PATH#/}"
  else
    log "Secrets KV v2 accessibles : apim, kafka et scog (${VAULT_ENVIRONMENT})"
  fi
}

resolve() {
  local variable=$1 key=$2 default=${3-} value
  value=${!variable-}
  if [[ -z "$value" && -n "$VAULT_JSON" ]]; then
    value=$(jq -er --arg key "$key" '.data.data[$key] // empty' <<<"$VAULT_JSON" 2>/dev/null || true)
  fi
  [[ -n "$value" ]] || value=$default
  [[ "$value" != *$'\n'* && "$value" != *$'\r'* ]] || die "$variable doit tenir sur une ligne"
  printf -v "$variable" '%s' "$value"
}

resolve_all() {
  resolve SBC_CIDR sbc_cidr
  resolve SIP_BIND sip_bind 0.0.0.0
  resolve SIP_PORT sip_port 5060
  resolve RTP_START rtp_start 10000
  resolve RTP_END rtp_end 20000
  resolve RECORDING_DIR recording_dir /var/spool/asterisk/monitor
  resolve SIPREC_RECORDING_DIR siprec_recording_dir /tmp
  resolve SIPREC_MAX_DURATION siprec_max_duration 3600
  resolve LOOPBACK_TEST_DIR loopback_test_dir /var/spool/asterisk/monitor/pjsip-loop-test
  resolve LOOPBACK_DEFAULT_AUDIO loopback_default_audio hello-world
  resolve STT_PLUGIN stt_plugin /usr/lib/voxispeech/plugin_scog.so
  resolve STT_URL stt_url
  resolve STT_URL_SECONDARY stt_url_secondary "$STT_URL"
  resolve STT_LANGUAGE stt_language fr-FR
  resolve STT_CLIENT_ID stt_client_id
  resolve STT_CLIENT_SECRET stt_client_secret
  resolve STT_TOKEN_URL stt_token_url
  resolve STT_APIM_BASE stt_apim_base
  resolve STT_CREDENTIAL stt_credential
  resolve STT_RECONNECT stt_reconnect yes
  resolve STT_RECONNECT_DELAY stt_reconnect_delay 1000
  resolve STT_RECONNECT_MAX stt_reconnect_max 3
  resolve TRANSCRIPT_FORMAT transcript_format json:call
  resolve VOXIMONITOR_STEREO voximonitor_stereo yes
  resolve VOXIMONITOR_STEREO_RATE voximonitor_stereo_rate 8000
  resolve VOXIMONITOR_TIMESTAMP_MODE voximonitor_timestamp_mode event
  resolve VOXIMONITOR_EARLY_SPEECH_TIMEOUT voximonitor_early_speech_timeout 0
  resolve KAFKA_ENABLE kafka_enable yes
  resolve KAFKA_CONSUMER_CONNECTION kafka_consumer_connection DEV_CONSUMER
  resolve KAFKA_PRODUCER_CONNECTION kafka_producer_connection DEV_PRODUCER
  resolve KAFKA_CONSUMER_BROKERS kafka_consumer_brokers
  resolve KAFKA_PRODUCER_BROKERS kafka_producer_brokers "$KAFKA_CONSUMER_BROKERS"
  resolve KAFKA_CONSUMER_CLIENT_ID kafka_consumer_client_id recordConsumer
  resolve KAFKA_PRODUCER_CLIENT_ID kafka_producer_client_id recordProducer
  resolve KAFKA_SECURITY_PROTOCOL kafka_security_protocol SASL_SSL
  resolve KAFKA_SASL_MECHANISM kafka_sasl_mechanism SCRAM-SHA-512
  resolve KAFKA_CONSUMER_USERNAME kafka_consumer_username
  resolve KAFKA_CONSUMER_PASSWORD kafka_consumer_password
  resolve KAFKA_PRODUCER_USERNAME kafka_producer_username
  resolve KAFKA_PRODUCER_PASSWORD kafka_producer_password
  resolve KAFKA_GROUP_ID kafka_group_id asterisk-voximonitor
  resolve KAFKA_TRANSCRIPTION_TOPIC kafka_transcription_topic siprec-transcription
  resolve KAFKA_COMMAND_TOPIC kafka_command_topic siprec-command
  resolve KAFKA_INTERRUPTION_TOPIC kafka_interruption_topic siprec-interruption
  resolve KAFKA_SESSION_TOPIC kafka_session_topic siprec-session
  resolve KAFKA_MODE kafka_mode telia_dev
  resolve GATEWAY_ID gateway_id ASTERISK_GW_001
  resolve ENTITY_CODE entity_code GATEWAY_ID
  resolve PROMETHEUS_ENABLE prometheus_enable yes
  resolve HTTP_BIND http_bind 127.0.0.1
  resolve HTTP_PORT http_port 8088

  [[ -n "$SBC_CIDR" ]] || die "SBC_CIDR manque"
  [[ -n "$STT_URL" ]] || die "STT_URL manque"
  if [[ "$DRY_RUN" != yes ]]; then
    [[ -n "$STT_CLIENT_ID" && -n "$STT_CLIENT_SECRET" && -n "$STT_TOKEN_URL" ]] \
      || die "Identifiants STT incomplets"
  fi
  if [[ "$KAFKA_ENABLE" == yes ]]; then
    [[ -n "$KAFKA_CONSUMER_BROKERS" ]] || die "KAFKA_CONSUMER_BROKERS manque"
    [[ -n "$KAFKA_PRODUCER_BROKERS" ]] || die "KAFKA_PRODUCER_BROKERS manque"
    if [[ "$DRY_RUN" != yes && "$KAFKA_SECURITY_PROTOCOL" == SASL_* ]]; then
      [[ -n "$KAFKA_CONSUMER_USERNAME" && -n "$KAFKA_CONSUMER_PASSWORD" ]] \
        || die "Identifiants Kafka consommateur incomplets"
      [[ -n "$KAFKA_PRODUCER_USERNAME" && -n "$KAFKA_PRODUCER_PASSWORD" ]] \
        || die "Identifiants Kafka producteur incomplets"
    fi
  fi
}

fetch_package() {
  local source=$1 destination=$2 token=
  case "$source" in
    http://*|https://*)
      require_command curl
      [[ -z ${ARTIFACT_TOKEN_FILE:-} ]] || token=$(read_secret_file "$ARTIFACT_TOKEN_FILE")
      local args=(--fail --location --retry 3 --retry-delay 2)
      [[ -z "$token" ]] || args+=(--header "Authorization: Bearer $token")
      curl "${args[@]}" --output "$destination" "$source"
      ;;
    *) [[ -r "$source" ]] || die "Archive absente : $source"; cp -- "$source" "$destination" ;;
  esac
}

install_bundle() {
  local name=$1 source=$2 checksum=$3 shell_name=$4 args_text=$5
  local archive="$WORK_DIR/$name.archive" directory="$WORK_DIR/$name" installer
  local -a installer_args=()
  require_command tar
  require_command sha256sum
  [[ -n "$source" ]] || die "Package $name non défini"
  require_command "$shell_name"
  fetch_package "$source" "$archive"
  if [[ -n "$checksum" ]]; then
    printf '%s  %s\n' "$checksum" "$archive" | sha256sum --check --status \
      || die "SHA-256 incorrect pour $name"
  else
    warn "Aucun SHA-256 fourni pour $name"
  fi
  mkdir -p "$directory"
  tar -xf "$archive" -C "$directory" --no-same-owner
  installer=$(find "$directory" -type f -name install.sh -print -quit)
  [[ -n "$installer" ]] || die "$name ne contient pas install.sh"
  [[ -z "$args_text" ]] || IFS=' ' read -r -a installer_args <<<"$args_text"
  log "Installation de $name"
  (cd "$(dirname "$installer")" && "$shell_name" ./install.sh "${installer_args[@]}")
}

stop_asterisk_for_install() {
  local attempt

  if ! command -v systemctl >/dev/null 2>&1 \
    || [[ ! -d /run/systemd/system ]] \
    || ! systemctl cat asterisk.service >/dev/null 2>&1; then
    log "Service Asterisk absent ou systemd indisponible ; aucun arrêt préalable"
    return 0
  fi

  log "Arrêt d'Asterisk par systemd avant installation"
  systemctl stop asterisk.service
  for ((attempt = 1; attempt <= 30; attempt++)); do
    if ! systemctl is-active --quiet asterisk.service; then
      log "Asterisk arrêté ; installation des packages autorisée"
      return 0
    fi
    sleep 1
  done

  die "Asterisk reste actif après 30 secondes ; installation interrompue"
}

render_template() {
  local source=$1 destination=$2 variable value placeholder content
  content=$(<"$source")
  for variable in SBC_CIDR SIP_BIND SIP_PORT RTP_START RTP_END RECORDING_DIR \
    SIPREC_RECORDING_DIR SIPREC_MAX_DURATION LOOPBACK_TEST_DIR LOOPBACK_DEFAULT_AUDIO \
    STT_PLUGIN STT_URL STT_URL_SECONDARY STT_LANGUAGE STT_CLIENT_ID \
    STT_CLIENT_SECRET STT_TOKEN_URL STT_APIM_BASE STT_CREDENTIAL \
    STT_RECONNECT STT_RECONNECT_DELAY STT_RECONNECT_MAX TRANSCRIPT_FORMAT \
    VOXIMONITOR_STEREO VOXIMONITOR_STEREO_RATE VOXIMONITOR_TIMESTAMP_MODE \
    VOXIMONITOR_EARLY_SPEECH_TIMEOUT KAFKA_ENABLE KAFKA_CONSUMER_CONNECTION \
    KAFKA_PRODUCER_CONNECTION KAFKA_CONSUMER_BROKERS KAFKA_PRODUCER_BROKERS \
    KAFKA_CONSUMER_CLIENT_ID KAFKA_PRODUCER_CLIENT_ID KAFKA_SECURITY_PROTOCOL \
    KAFKA_SASL_MECHANISM KAFKA_CONSUMER_USERNAME KAFKA_CONSUMER_PASSWORD \
    KAFKA_PRODUCER_USERNAME KAFKA_PRODUCER_PASSWORD KAFKA_GROUP_ID \
    KAFKA_TRANSCRIPTION_TOPIC KAFKA_COMMAND_TOPIC KAFKA_INTERRUPTION_TOPIC \
    KAFKA_SESSION_TOPIC KAFKA_MODE GATEWAY_ID ENTITY_CODE \
    PROMETHEUS_ENABLE HTTP_BIND HTTP_PORT; do
    value=${!variable-}
    placeholder="@$variable@"
    content=${content//"$placeholder"/"$value"}
  done
  printf '%s\n' "$content" >"$destination"
  if grep -Eq '@[A-Z][A-Z0-9_]*@' "$destination"; then
    die "Variable non résolue dans $source"
  fi
}

backup_and_install() {
  local generated=$1 target=$2 mode=${3:-0640}
  if [[ "$DRY_RUN" == yes ]]; then
    log "Configuration préparée : $generated -> $target"
    return
  fi
  if [[ -e "$target" ]]; then
    mkdir -p "$BACKUP_DIR"
    cp -a -- "$target" "$BACKUP_DIR/"
  fi
  install -o root -g asterisk -m "$mode" "$generated" "$target.new"
  mv -f -- "$target.new" "$target"
}

ensure_include() {
  local file=$1 included=$2 line
  line="#include $included"
  if [[ ! -f "$file" ]]; then
    if [[ "$DRY_RUN" == yes ]]; then
      log "Création prévue : $file avec $line"
      return
    fi
    install -o root -g asterisk -m 0640 /dev/null "$file"
  fi
  grep -Fqx "$line" "$file" 2>/dev/null && return
  [[ "$DRY_RUN" == yes ]] && { log "Ajout prévu dans $file : $line"; return; }
  mkdir -p "$BACKUP_DIR"; cp -a -- "$file" "$BACKUP_DIR/"
  printf '\n%s\n' "$line" >>"$file"
}

generate_configuration() {
  local generated="$WORK_DIR/generated" name target mode
  mkdir -p "$generated"
  while IFS='|' read -r name target mode; do
    render_template "$TEMPLATE_DIR/$name.tpl" "$generated/$name"
    if [[ -n "$OUTPUT_DIR" ]]; then
      mkdir -p "$OUTPUT_DIR"
      install -m 0600 "$generated/$name" "$OUTPUT_DIR/$name"
    fi
    backup_and_install "$generated/$name" "$target" "$mode"
  done <<'EOF'
pjsip_siprec.conf|/etc/asterisk/pjsip_siprec.conf|0640
extensions.conf|/etc/asterisk/extensions.conf|0640
extensions_fromSBC.conf|/etc/asterisk/extensions_fromSBC.conf|0640
extensions_loopback_test.conf|/etc/asterisk/extensions_loopback_test.conf|0640
rtp.conf|/etc/asterisk/rtp.conf|0640
voximonitor.conf|/etc/asterisk/voximonitor.conf|0640
kafka.conf|/etc/asterisk/kafka.conf|0640
http.conf|/etc/asterisk/http.conf|0640
prometheus.conf|/etc/asterisk/prometheus.conf|0640
EOF
  ensure_include /etc/asterisk/pjsip.conf pjsip_siprec.conf
  run install -d -o asterisk -g asterisk -m 0750 "$RECORDING_DIR" /var/log/voxispeech
  run install -d -o asterisk -g asterisk -m 0750 "$LOOPBACK_TEST_DIR"
  if command -v restorecon >/dev/null 2>&1; then
    run restorecon -R /etc/asterisk "$RECORDING_DIR" /var/log/voxispeech
  fi
}

asterisk_cli_retry() {
  local command=$1 display=${2:-yes} attempt output=

  for ((attempt = 1; attempt <= 30; attempt++)); do
    if output=$(asterisk -rx "$command" 2>&1); then
      ASTERISK_CLI_OUTPUT=$output
      [[ "$display" != yes ]] || printf '%s\n' "$output"
      return 0
    fi
    sleep 1
  done

  warn "Dernière réponse du CLI Asterisk : $output"
  die "Commande Asterisk indisponible après 30 secondes : $command ; consulter journalctl -u asterisk"
}

verify_runtime() {
  local attempt consecutive=0 module status_line dialplan_summary
  local -a modules=(res_pjsip_siprec app_voximonitor res_speech_voxispeech)
  [[ "$DRY_RUN" == yes ]] && return
  command -v asterisk >/dev/null 2>&1 || die "Asterisk non installé"
  if command -v systemctl >/dev/null 2>&1 && [[ -d /run/systemd/system ]]; then
    log "Démarrage d'Asterisk après installation et configuration"
    systemctl start asterisk.service

    # Le socket CLI peut apparaître avant la fin du chargement des modules.
    # Exiger cinq réponses consécutives évite de valider un démarrage instable.
    for ((attempt = 1; attempt <= 60; attempt++)); do
      if systemctl is-active --quiet asterisk \
        && asterisk -rx 'core show version' >/dev/null 2>&1; then
        consecutive=$((consecutive + 1))
        if ((consecutive >= 5)); then
          break
        fi
      else
        consecutive=0
      fi
      sleep 1
    done
    ((consecutive >= 5)) || die \
      "Asterisk ou son CLI reste instable après 60 secondes ; consulter systemctl status asterisk et journalctl -u asterisk"
  else
    warn "systemd indisponible : démarrage Asterisk laissé à l'administrateur"
    return
  fi
  [[ "$KAFKA_ENABLE" != yes ]] || modules+=(res_kafka)
  printf '%-30s %-40s %-10s %-11s %s\n' \
    'Module' 'Description' 'Use Count' 'Status' 'Support Level'
  for module in "${modules[@]}"; do
    asterisk_cli_retry "module show like $module" no
    status_line=$(printf '%s\n' "$ASTERISK_CLI_OUTPUT" | \
      awk -v expected="$module.so" '$1 == expected { print; exit }')
    [[ -n "$status_line" ]] || die "Module Asterisk absent : $module.so"
    [[ "$status_line" == *" Running "* ]] || die \
      "Module Asterisk non opérationnel : $status_line"
    printf '%s\n' "$status_line"
  done
  asterisk_cli_retry 'dialplan show from-sbc' no
  dialplan_summary=$(printf '%s\n' "$ASTERISK_CLI_OUTPUT" | \
    awk '/extensions \([^)]*priorities\) in [0-9]+ context/ {
      sub(/^-=[[:space:]]*/, "")
      print
      exit
    }')
  [[ -n "$dialplan_summary" ]] || die \
    "Le contexte Asterisk from-sbc est absent ou illisible"
  printf '%s\n' "$dialplan_summary"
}

main() {
  [[ -d "$TEMPLATE_DIR" ]] || die "Templates absents : $TEMPLATE_DIR"
  WORK_DIR=$(mktemp -d /tmp/siprec-install.XXXXXX)
  BACKUP_DIR="/var/backups/siprec-install/$(date -u +%Y%m%dT%H%M%SZ)"

  if [[ "$CHECK_ONLY" == yes ]]; then
    check_vault
    return 0
  fi

  vault_login
  vault_fetch_configuration
  resolve_all

  if [[ "$CONFIG_ONLY" == no ]]; then
    if [[ "$DRY_RUN" == yes ]]; then
      log "Arrêt préalable prévu : asterisk.service"
      log "Installation prévue : Asterisk, VoxiSpeech, Voximal"
    else
      stop_asterisk_for_install
      install_bundle asterisk "${ASTERISK_PACKAGE:-}" "${ASTERISK_SHA256:-}" \
        "${ASTERISK_INSTALL_SHELL:-bash}" "${ASTERISK_INSTALL_ARGS:-}"
      install_bundle voxispeech "${VOXISPEECH_PACKAGE:-}" "${VOXISPEECH_SHA256:-}" \
        "${VOXISPEECH_INSTALL_SHELL:-bash}" "${VOXISPEECH_INSTALL_ARGS:-}"
      install_bundle voximal "${VOXIMAL_PACKAGE:-}" "${VOXIMAL_SHA256:-}" \
        "${VOXIMAL_INSTALL_SHELL:-bash}" "${VOXIMAL_INSTALL_ARGS:-}"
    fi
  fi

  [[ "$DRY_RUN" == yes ]] || id asterisk >/dev/null 2>&1 || die "Utilisateur asterisk absent"
  generate_configuration
  verify_runtime
  printf '\n%s\n' '================================================================'
  if [[ "$DRY_RUN" == yes ]]; then
    printf '%s\n' '[SUCCESS] VOXIBOT TELIA DRY-RUN COMPLETED SUCCESSFULLY'
    printf '%s\n' 'No system changes were applied.'
  else
    printf '%s\n' '[SUCCESS] VOXIBOT TELIA INSTALLATION COMPLETED SUCCESSFULLY'
    printf '%s\n' 'Asterisk is running and all required modules are operational.'
  fi
  printf '%s\n' '================================================================'
  log "Configuration backups: $BACKUP_DIR"
}

main "$@"
