#!/bin/bash
# install_amd_client.sh — point a VICIdial / Asterisk dialer at the self-hosted AMD service.
#
# It installs the EAGI client (amd_agi_client.py) + a dialplan hook that streams the
# callee's audio to the AMD service and sets the standard AMDSTATUS channel variable
# (HUMAN / MACHINE) that your campaign routing already reads.
#
# AUTH is by source-IP allowlist (no API key): this dialer's PUBLIC IP must be added to
# the service allowlist first — do it in the dashboard's "Access — IP allowlist" panel.
#
# Usage (run as root on the dialer):
#   AMD_WS_URL=ws://144.76.101.53:2700 bash install_amd_client.sh
# Options via env: AMD_WS_URL (service endpoint), AMD_EXT (dialplan extension, default 8370),
#                  AMD_CTX (context VICIdial originates into, default "default"),
#                  AGI_DIR (default /var/lib/asterisk/agi-bin).
set -e
AMD_WS_URL="${AMD_WS_URL:-ws://144.76.101.53:2700}"
AGI_DIR="${AGI_DIR:-/var/lib/asterisk/agi-bin}"
EXT="${AMD_EXT:-8370}"
CTX="${AMD_CTX:-default}"
SELF_DIR="$(cd "$(dirname "$0")" && pwd)"
EXTCONF="/etc/asterisk/extensions.conf"

log(){ echo -e "\033[0;32m[amd-install] $1\033[0m"; }
warn(){ echo -e "\033[0;33m[amd-install] WARN: $1\033[0m"; }
err(){ echo -e "\033[0;31m[amd-install] ERROR: $1\033[0m"; exit 1; }

[ "$(id -u)" = "0" ] || err "run as root"
command -v asterisk >/dev/null || err "asterisk not found on PATH"
[ -f "$SELF_DIR/amd_agi_client.py" ] || err "amd_agi_client.py not next to this script"
[ -f "$EXTCONF" ] || err "$EXTCONF not found"

# --- 0. resolve the interpreter the EAGI script will ACTUALLY run under ---
# The shebang decides this at call time, not whatever pip3 happens to point at.
PYBIN="$(head -1 "$SELF_DIR/amd_agi_client.py" | sed 's|^#!||; s|[[:space:]].*||')"
[ -x "$PYBIN" ] || PYBIN="$(command -v python3 || true)"
[ -n "$PYBIN" ] && [ -x "$PYBIN" ] || err "no usable python3 interpreter (shebang points at a missing binary)"
log "EAGI interpreter: $PYBIN  ($("$PYBIN" -V 2>&1))"

# The user Asterisk runs as — deps must be importable for THAT user, not just root.
AST_USER="$(ps -o user= -C asterisk 2>/dev/null | head -1 | tr -d ' ')"
[ -n "$AST_USER" ] || AST_USER="asterisk"
log "asterisk runs as: $AST_USER"

# --- 1. python deps for the EAGI client ---
# Only websocket-client and pyst2 are imported by the client; numpy/soundfile were never used.
log "installing python deps (websocket-client, pyst2) for $PYBIN …"
if   command -v zypper >/dev/null; then zypper -n in -y python3-pip >/dev/null 2>&1 || true
elif command -v yum    >/dev/null; then yum -y install python3-pip   >/dev/null 2>&1 || true
elif command -v apt    >/dev/null; then apt-get update -qq && apt-get install -y python3-pip >/dev/null 2>&1 || true
fi
"$PYBIN" -m pip install --quiet --upgrade pyst2 websocket-client 2>/dev/null \
  || "$PYBIN" -m pip install --quiet pyst2 websocket-client \
  || err "pip install failed for $PYBIN — install pyst2 + websocket-client manually"

# --- 2. drop the EAGI client, baking the endpoint into it ---
install -m 0755 "$SELF_DIR/amd_agi_client.py" "$AGI_DIR/amd_agi_client.py"

# Asterisk does NOT export channel variables into AGI/EAGI child environments, so a
# dialplan Set(AMD_WS_URL=...) can never reach os.environ. Bake the endpoint in instead.
sed -i "s#^WS_ENDPOINT = .*#WS_ENDPOINT = os.environ.get(\"AMD_WS_URL\", \"$AMD_WS_URL\")#" \
  "$AGI_DIR/amd_agi_client.py"
grep -q "\"$AMD_WS_URL\"" "$AGI_DIR/amd_agi_client.py" \
  || err "failed to bake endpoint into $AGI_DIR/amd_agi_client.py — check the WS_ENDPOINT line"
log "installed $AGI_DIR/amd_agi_client.py  (endpoint baked in: $AMD_WS_URL)"

# --- 2b. verify the deps import as the asterisk user, under the real interpreter ---
if su -s /bin/sh -c "\"$PYBIN\" -c 'import websocket, asterisk.agi'" "$AST_USER" >/dev/null 2>&1; then
  log "dependency check OK (websocket + asterisk.agi importable as $AST_USER)"
else
  err "$AST_USER cannot import websocket/asterisk.agi under $PYBIN — EAGI would fail silently.
       Reproduce with:  su -s /bin/sh -c '$PYBIN -c \"import websocket, asterisk.agi\"' $AST_USER"
fi

# --- 3. remove any previous install, then rewrite from scratch ---
# Always back up before touching the file; every run is a clean replace, never an append,
# so a half-written or hand-edited block from an earlier run can't survive.
BACKUP="$EXTCONF.amd.bak.$(date +%s)"
cp "$EXTCONF" "$BACKUP"
log "backed up $EXTCONF -> $BACKUP"

TMP="$EXTCONF.amdtmp.$$"
trap 'rm -f "$TMP"' EXIT

# Strips three things:
#   a) marker-delimited blocks written by this version (BEGIN..END)
#   b) legacy blocks from older versions, which had no END marker — run from the old
#      comment to the extension's Hangup(), or to the next context header, whichever first
#   c) any 'include => amd-selfhosted' anywhere, so step 4b can re-add it deterministically
#      even if AMD_CTX changed between runs
# Blank lines are buffered and only emitted once a non-blank follows, so the separator
# blank line left behind by a removed block is dropped rather than accumulating one
# extra blank per install run.
awk -v ext="$EXT" '
  skip == 2 {
      if ($0 ~ "^[[:space:]]*exten[[:space:]]*=>[[:space:]]*" ext "[[:space:]]*,.*Hangup") { skip=0; next }
      if ($0 ~ /^\[/ && $0 !~ /^\[amd-selfhosted\]/) { skip=0 } else { next }
  }
  skip == 1 {
      if ($0 ~ /^; --- END self-hosted AMD/) { skip=0 }
      next
  }
  /^; --- BEGIN self-hosted AMD/ { skip=1; next }
  /^; --- self-hosted AMD/       { skip=2; next }
  /^[[:space:]]*include[[:space:]]*=>[[:space:]]*amd-selfhosted/ { next }
  /^[[:space:]]*$/ { blanks++; next }
  { for (i = 0; i < blanks; i++) print ""; blanks = 0; print }
' "$EXTCONF" > "$TMP" || err "cleanup pass failed — $EXTCONF untouched (backup: $BACKUP)"

# cat, not mv: preserves the existing inode, owner and mode on an asterisk-owned config
cat "$TMP" > "$EXTCONF"

REMOVED=$(( $(wc -l < "$BACKUP") - $(wc -l < "$EXTCONF") ))
if [ "$REMOVED" -gt 0 ]; then
  log "removed previous install ($REMOVED lines)"
else
  log "no previous install found"
fi

# A stray EAGI reference outside our block means someone wired the client in by hand
# (e.g. patched VICIdial's own AMD extension). Don't clobber it — but don't create a
# second, competing path silently either.
STRAY="$(grep -n "amd_agi_client.py" "$EXTCONF" || true)"
if [ -n "$STRAY" ]; then
  warn "amd_agi_client.py is still referenced outside the managed block:"
  echo "$STRAY" | sed 's/^/         /'
  warn "leaving those lines alone — if that is a hand-patched VICIdial extension,"
  warn "you probably do NOT also want the $EXT path below. Remove one of them."
fi

# --- 4. write the dialplan hook: EAGI streams audio -> sets AMDSTATUS ---
log "adding dialplan extension $EXT to $EXTCONF"
cat >> "$EXTCONF" <<DP

; --- BEGIN self-hosted AMD (usad2/gru) — managed by install_amd_client.sh, do not edit ---
[amd-selfhosted]
exten => $EXT,1,AGI(agi://127.0.0.1:4577/call_log)
exten => $EXT,n,Answer()
exten => $EXT,n,EAGI($AGI_DIR/amd_agi_client.py)
exten => $EXT,n,AGI(VD_amd.agi,\${EXTEN})
exten => $EXT,n,AGI(agi-VDAD_ALL_outbound.agi,NORMAL-----LB-----\${CONNECTEDLINE(name)})
exten => $EXT,n,Hangup()
; --- END self-hosted AMD ---
DP

# --- 4b. make that context REACHABLE from the one VICIdial originates into ---
# Without this the extension parses fine and is simply never dialled: calls die with
# "no such extension" and nothing in the AMD path ever logs a thing.
grep -qE "^[[:space:]]*\[$CTX\]" "$EXTCONF" \
  || err "context [$CTX] not found in $EXTCONF — set AMD_CTX to the context VICIdial originates into"
log "adding 'include => amd-selfhosted' to [$CTX]"
awk -v c="$CTX" '
    {print}
    !done && $0 ~ "^[[:space:]]*\\["c"\\][[:space:]]*$" {
        print "include => amd-selfhosted   ; self-hosted AMD (install_amd_client.sh)"; done=1
    }
' "$EXTCONF" > "$TMP" || err "include pass failed (backup: $BACKUP)"
cat "$TMP" > "$EXTCONF"
grep -q "include => amd-selfhosted" "$EXTCONF" \
  || err "could not insert include into [$CTX] — is the header line exactly '[$CTX]'? (backup: $BACKUP)"

# --- 5. reload and PROVE the extension is reachable ---
asterisk -rx "dialplan reload" >/dev/null 2>&1 || asterisk -rx "reload" >/dev/null 2>&1 || err "asterisk reload failed"
if asterisk -rx "dialplan show $EXT@$CTX" 2>/dev/null | grep -q "amd_agi_client.py"; then
  log "verified: $EXT@$CTX is reachable and runs the EAGI client"
else
  err "$EXT is NOT reachable from [$CTX] after reload.
       Restore with:  cp $BACKUP $EXTCONF && asterisk -rx 'dialplan reload'
       Check:  asterisk -rx 'dialplan show $EXT@$CTX'
               asterisk -rx 'dialplan show amd-selfhosted'"
fi

log "done."
echo
echo "NEXT:"
echo "  1. Add this dialer's PUBLIC IP ($(curl -s --max-time 5 ifconfig.me 2>/dev/null || echo '?')) to the service allowlist."
echo "  2. Make VICIdial actually dial $EXT. Either set the campaign's VDAD extension to $EXT,"
echo "     or replace the AMD() line in VICIdial's own AMD extension with:"
echo "         exten => <that ext>,n,EAGI($AGI_DIR/amd_agi_client.py)"
echo "  3. Test call, then: asterisk -rx 'core set verbose 9' and watch for 'AMD:' lines."
