#!/usr/bin/env bash
# =============================================================================
# V-Connect PBX — Update Script
# =============================================================================
# Pulls the latest code from Bitbucket and applies it safely to this machine.
#
# Usage:
#   sudo bash /var/www/html/scripts/pbx-update.sh
#   sudo bash /var/www/html/scripts/pbx-update.sh --check   # dry-run, no changes
#
# What it does:
#   1. Pulls latest code from the configured git remote (Bitbucket)
#   2. Runs composer install (no dev deps, optimised autoloader)
#   3. Runs any new database migrations
#   4. Clears and rebuilds Laravel caches
#   5. Fixes file ownership (www-data)
#   6. Reloads Asterisk dialplan
#   7. Prints a summary
#
# What it does NOT touch:
#   - .env (your local config — never in git)
#   - /etc/asterisk/* (system configs — managed by pbx:deploy)
#   - Database content (migrations only add/alter schema, never wipe data)
#   - Firewall rules
# =============================================================================

set -euo pipefail

APP_DIR="/var/www/html"
WEB_USER="www-data"
DRY_RUN=0

# Ensure all users (including www-data) can run git in this directory.
# --system writes to /etc/gitconfig so it applies to every user on the machine.
git config --system --add safe.directory "$APP_DIR" 2>/dev/null || true

# Ignore file-permission (mode) changes. The deploy chmod's storage/ which
# flips the mode bits on tracked files like storage/**/.gitignore; git then
# sees them as "modified" and blocks `git pull`. Telling git to ignore mode
# changes stops these phantom modifications from ever blocking an update.
git config core.fileMode false 2>/dev/null || true

# ── Argument parsing ──────────────────────────────────────────────────────────
for arg in "$@"; do
    case "$arg" in
        --check|--dry-run) DRY_RUN=1 ;;
        --help)
            echo "Usage: sudo bash $0 [--check]"
            echo "  --check   Show what would change without applying anything"
            exit 0
            ;;
    esac
done

# ── Permission check ──────────────────────────────────────────────────────────
if [[ $EUID -ne 0 ]]; then
    echo "ERROR: This script must be run as root (sudo)."
    exit 1
fi

# ── Helpers ───────────────────────────────────────────────────────────────────
info()    { echo -e "\033[0;34m▸ $*\033[0m"; }
success() { echo -e "\033[0;32m  ✓ $*\033[0m"; }
warn()    { echo -e "\033[0;33m  ⚠ $*\033[0m"; }
dry()     { echo -e "\033[0;36m  [dry-run] $*\033[0m"; }

run() {
    # run <description> <command...>
    local desc="$1"; shift
    if [[ $DRY_RUN -eq 1 ]]; then
        dry "$desc"
    else
        if "$@" 2>&1; then
            success "$desc"
        else
            warn "$desc — command returned non-zero (may be harmless)"
        fi
    fi
}

# ── Banner ────────────────────────────────────────────────────────────────────
echo ""
echo "═══════════════════════════════════════════════════════"
echo "  V-Connect PBX — Update$([ $DRY_RUN -eq 1 ] && echo ' (DRY RUN)' || echo '')"
echo "═══════════════════════════════════════════════════════"
echo ""

cd "$APP_DIR"

# ── 1. Git pull ───────────────────────────────────────────────────────────────
info "Pulling latest code from Bitbucket"

if [[ $DRY_RUN -eq 1 ]]; then
    dry "git fetch origin main"
    dry "git diff HEAD origin/main --stat"
    git fetch origin main 2>&1 || warn "Could not fetch (check SSH key / network)"
    git diff HEAD origin/main --stat 2>&1 || true
else
    # Stash any local uncommitted changes (shouldn't be any on client machines)
    if ! git diff --quiet 2>/dev/null; then
        warn "Uncommitted local changes detected — stashing before pull"
        git stash push -m "pbx-update auto-stash $(date +%Y%m%d-%H%M%S)"
    fi

    # Untracked files that the incoming update ALSO ships (now tracked) block a
    # merge with "untracked working tree files would be overwritten". This
    # happens on boxes imaged/installed with files later added to the repo
    # (e.g. add-on module files). Move ONLY those colliding files into a
    # timestamped backup so the pull brings the canonical tracked versions —
    # any other local-only files are left untouched.
    git fetch origin main 2>&1 || warn "Could not fetch origin main"
    COLLISIONS=$(comm -12 \
        <(git ls-files --others --exclude-standard 2>/dev/null | sort) \
        <(git diff --name-only HEAD origin/main 2>/dev/null | sort))
    if [[ -n "$COLLISIONS" ]]; then
        BK="/root/pbx-update-untracked-$(date +%Y%m%d-%H%M%S)"
        warn "Untracked files collide with the update — backing up to $BK"
        while IFS= read -r f; do
            [[ -z "$f" ]] && continue
            mkdir -p "$BK/$(dirname "$f")"
            mv -f "$APP_DIR/$f" "$BK/$f" 2>/dev/null || true
        done <<< "$COLLISIONS"
        success "Moved $(echo "$COLLISIONS" | grep -c .) colliding file(s) aside; repo versions will be restored by the pull"
    fi

    BEFORE=$(git rev-parse HEAD 2>/dev/null || echo "none")
    git pull origin main --ff-only 2>&1
    AFTER=$(git rev-parse HEAD 2>/dev/null || echo "none")

    if [[ "$BEFORE" == "$AFTER" ]]; then
        success "Already up to date ($(git rev-parse --short HEAD))"
    else
        success "Updated $(git rev-parse --short "$BEFORE")..$(git rev-parse --short "$AFTER")"
        echo ""
        git log --oneline "$BEFORE..$AFTER" 2>/dev/null | sed 's/^/    /'
        echo ""
    fi
fi

# ── 2. Composer install ───────────────────────────────────────────────────────
info "Installing PHP dependencies"

# Run as root to avoid permission issues on fresh clones, then fix ownership
if [[ $DRY_RUN -eq 1 ]]; then
    dry "composer install --no-dev --optimize-autoloader"
else
    composer install \
        --no-dev \
        --optimize-autoloader \
        --no-interaction \
        --quiet \
        --working-dir="$APP_DIR" 2>&1 \
    && success "composer install" \
    || { echo "  composer not found, trying to install..."; \
         php -r "copy('https://getcomposer.org/installer', '/tmp/composer-setup.php');"; \
         php /tmp/composer-setup.php --install-dir=/usr/local/bin --filename=composer --quiet; \
         rm -f /tmp/composer-setup.php; \
         composer install --no-dev --optimize-autoloader --no-interaction --quiet --working-dir="$APP_DIR" 2>&1; \
         success "composer install (after auto-install)"; }
    # Fix ownership so www-data can write to vendor if needed
    chown -R "$WEB_USER:$WEB_USER" "$APP_DIR/vendor" 2>/dev/null || true
fi

# ── 2b. System dependencies (ffmpeg — recording-email compression) ────────────
# Recordings are uncompressed WAV (~1 MB/min). When a recording is emailed it is
# transcoded to a small MP3 first to stay under mail-server attachment limits.
# That needs ffmpeg; without it, emails still send but attach the larger WAV.
info "Ensuring system dependencies (ffmpeg)"
if [[ $DRY_RUN -eq 1 ]]; then
    dry "apt-get install -y ffmpeg (if missing)"
elif command -v ffmpeg >/dev/null 2>&1; then
    success "ffmpeg present ($(ffmpeg -version 2>/dev/null | head -1 | cut -d' ' -f1-3))"
else
    warn "ffmpeg not found — installing (compresses call recordings for email)"
    if apt-get update -qq && apt-get install -y ffmpeg 2>&1; then
        success "ffmpeg installed"
    else
        warn "Could not install ffmpeg automatically — recording emails will attach the larger WAV until it is installed (apt-get install -y ffmpeg)"
    fi
fi

# Voice-prompt normalisation needs an audio converter. ffmpeg (above) is the
# primary; sox is the fallback used by the Voice Prompts upload. Ensure sox
# with MP3 support is present so uploaded greetings/announcements are always
# transcoded to Asterisk-playable 8kHz mono PCM (an unconverted MP3 plays as
# silence and the queue skips straight past the welcome message).
info "Ensuring system dependencies (sox)"
if [[ $DRY_RUN -eq 1 ]]; then
    dry "apt-get install -y sox libsox-fmt-mp3 (if missing)"
elif command -v sox >/dev/null 2>&1; then
    success "sox present"
else
    warn "sox not found — installing (voice-prompt audio conversion)"
    if apt-get install -y sox libsox-fmt-mp3 2>&1; then
        success "sox installed"
    else
        warn "Could not install sox automatically — uploaded MP3/OGG prompts may not convert (apt-get install -y sox libsox-fmt-mp3)"
    fi
fi

# ── 3. Database backup (before any migration) ────────────────────────────────
info "Backing up database before migrations"
if [[ $DRY_RUN -eq 1 ]]; then
    dry "mysqldump (pre-migration safety backup)"
else
    BACKUP_DIR="/var/backups/pbx"
    mkdir -p "$BACKUP_DIR"
    BACKUP_FILE="$BACKUP_DIR/pre-update-$(date +%Y%m%d-%H%M%S).sql.gz"
    # Read DB creds from .env without printing them.
    DB_DATABASE=$(grep -E '^DB_DATABASE=' "$APP_DIR/.env" | head -1 | cut -d= -f2- | tr -d '"' | tr -d "'")
    DB_USERNAME=$(grep -E '^DB_USERNAME=' "$APP_DIR/.env" | head -1 | cut -d= -f2- | tr -d '"' | tr -d "'")
    DB_PASSWORD=$(grep -E '^DB_PASSWORD=' "$APP_DIR/.env" | head -1 | cut -d= -f2- | tr -d '"' | tr -d "'")
    if [[ -n "${DB_DATABASE:-}" ]] && command -v mysqldump >/dev/null 2>&1; then
        if MYSQL_PWD="$DB_PASSWORD" mysqldump --single-transaction --quick \
            -u "$DB_USERNAME" "$DB_DATABASE" 2>/dev/null | gzip > "$BACKUP_FILE"; then
            success "Database backed up to $BACKUP_FILE"
        else
            echo "ERROR: Database backup failed — aborting update before any migration."
            rm -f "$BACKUP_FILE"
            exit 1
        fi
    else
        warn "mysqldump unavailable or DB not configured — skipping backup"
    fi
fi

# ── 3b. Stop dialler worker before migrations (if installed) ──────────────────
info "Stopping dialler worker before migrations (if present)"
if [[ $DRY_RUN -eq 1 ]]; then
    dry "systemctl stop shiftbridge-dialer-worker"
else
    if systemctl list-unit-files 2>/dev/null | grep -q '^shiftbridge-dialer-worker'; then
        systemctl stop shiftbridge-dialer-worker 2>/dev/null || true
        success "Dialler worker stopped for update"
    else
        success "Dialler worker not installed yet — nothing to stop"
    fi
fi

# ── 3c. Database migrations (hard-abort on failure) ───────────────────────────
info "Running database migrations"
if [[ $DRY_RUN -eq 1 ]]; then
    dry "artisan migrate --force"
else
    if sudo -u "$WEB_USER" php "$APP_DIR/artisan" migrate --force --no-interaction 2>&1; then
        success "artisan migrate"
    else
        echo "ERROR: Migration failed — aborting update. Worker left stopped."
        echo "       Restore from backup if needed: ${BACKUP_FILE:-<no backup>}"
        exit 1
    fi
fi

# ── 3d. Idempotent seeders (settings, outcomes, permissions) ──────────────────
info "Seeding dialler defaults (idempotent)"
run "DialerSettingsSeeder" \
    sudo -u "$WEB_USER" php "$APP_DIR/artisan" db:seed --class=DialerSettingsSeeder --force
run "DialerOutcomesSeeder" \
    sudo -u "$WEB_USER" php "$APP_DIR/artisan" db:seed --class=DialerOutcomesSeeder --force
run "DialerPermissionsSeeder" \
    sudo -u "$WEB_USER" php "$APP_DIR/artisan" db:seed --class=DialerPermissionsSeeder --force

# ── 4. Cache rebuild ──────────────────────────────────────────────────────────
info "Rebuilding caches"
# Ensure required .env keys exist (safe defaults, never overwrites operator
# values) BEFORE caching config so any newly-added keys are baked in.
run "ensure .env defaults" \
    sudo -u "$WEB_USER" php "$APP_DIR/artisan" pbx:ensure-env
run "config:cache"  sudo -u "$WEB_USER" php "$APP_DIR/artisan" config:cache
run "route:cache"   sudo -u "$WEB_USER" php "$APP_DIR/artisan" route:cache
run "view:clear"    sudo -u "$WEB_USER" php "$APP_DIR/artisan" view:clear
run "event:cache"   sudo -u "$WEB_USER" php "$APP_DIR/artisan" event:cache 2>/dev/null || true

# ── 4b. Restart long-running queue workers so they load the NEW code ──────────
# A long-running worker (started days ago) keeps the OLD PHP code in memory for
# the life of the process — a code fix deployed here does NOT take effect in
# that worker until it restarts. `queue:restart` signals every worker to exit
# gracefully after its current job so systemd respawns it on fresh code.
# Also hard-restart the known unit if present, for immediate effect. Best-effort:
# never abort the update if the worker isn't installed.
info "Restarting queue workers to load new code"
if [[ $DRY_RUN -eq 1 ]]; then
    dry "artisan queue:restart + systemctl restart laravel-queue-worker"
else
    sudo -u "$WEB_USER" php "$APP_DIR/artisan" queue:restart 2>/dev/null || true
    if systemctl list-unit-files 2>/dev/null | grep -q '^laravel-queue-worker'; then
        systemctl restart laravel-queue-worker 2>/dev/null || true
        success "Queue worker restarted"
    else
        success "queue:restart signalled (no dedicated queue-worker unit)"
    fi
fi

# ── 5. File ownership ─────────────────────────────────────────────────────────
info "Fixing file ownership"
run "chown storage/ bootstrap/cache/ vendor/" \
    chown -R "$WEB_USER:$WEB_USER" \
        "$APP_DIR/storage" \
        "$APP_DIR/bootstrap/cache" \
        "$APP_DIR/vendor"

# ── 6. Asterisk dialplan reload ───────────────────────────────────────────────
info "Reloading Asterisk dialplan"
run "dialplan reload" \
    sudo -n asterisk -rx "dialplan reload"

# ── 7. AstDB reconciliation ───────────────────────────────────────────────────
info "Reconciling AstDB"
run "pbx:reconcile-astdb" \
    sudo -u "$WEB_USER" php "$APP_DIR/artisan" pbx:reconcile-astdb

# ── 8. Mobile TLS transport (idempotent — ensures port 5161 + cert) ──────────
info "Ensuring mobile app transport"
run "pbx:setup-mobile-transport" \
    php "$APP_DIR/artisan" pbx:setup-mobile-transport --reload

# ── 8b. Full system deploy (idempotent, self-healing) ─────────────────────────
# Runs the same provisioning pbx:deploy does on a fresh install: extconfig +
# res_config realtime mapping, feature-code dialplan, extension dialplan,
# DID normalizer, BLF hints, recording audit, etc. This is what makes an
# UPDATE self-heal the same drift a fresh deploy would (missing feature codes,
# wiped dialplan, duplicate realtime mapping). Safe to run repeatedly.
info "Running full system deploy (pbx:deploy)"
if [[ $DRY_RUN -eq 1 ]]; then
    dry "artisan pbx:deploy"
else
    php "$APP_DIR/artisan" pbx:deploy || warn "pbx:deploy reported issues — review output above"
fi

# ── 9. Dialler worker restart (only if feature flag enabled) ──────────────────
info "Dialler worker"
if [[ $DRY_RUN -eq 1 ]]; then
    dry "Check feature flag and start/stop worker accordingly"
else
    # Check if the dialler module is enabled for this unit.
    # Read the dialler feature flag via a dedicated, psysh-free command.
    # NOTE: do NOT use `artisan tinker` here — psysh needs a writable HOME and
    # fails when run as www-data during an update, which (under set -euo
    # pipefail) aborted the whole script before "Update complete." The trailing
    # "|| echo 0" guarantees this line can never abort the update.
    DIALER_ENABLED=$(sudo -u "$WEB_USER" php "$APP_DIR/artisan" pbx:dialer-enabled 2>/dev/null | tail -1 || echo 0)
    DIALER_ENABLED="${DIALER_ENABLED:-0}"

    if [[ "$DIALER_ENABLED" == "1" ]]; then
        # Install/refresh the systemd unit and start.
        if [[ -f "$APP_DIR/scripts/shiftbridge-dialer-worker.service" ]]; then
            cp "$APP_DIR/scripts/shiftbridge-dialer-worker.service" /etc/systemd/system/shiftbridge-dialer-worker.service
            chmod 644 /etc/systemd/system/shiftbridge-dialer-worker.service
            systemctl daemon-reload 2>/dev/null
            systemctl enable shiftbridge-dialer-worker 2>/dev/null
            systemctl restart shiftbridge-dialer-worker 2>/dev/null
            success "Dialler worker started (module enabled)"
        fi
    else
        # Module disabled — ensure worker is stopped.
        systemctl stop shiftbridge-dialer-worker 2>/dev/null || true
        systemctl disable shiftbridge-dialer-worker 2>/dev/null || true
        success "Dialler worker stopped (module disabled)"
    fi
fi

# ── Summary ───────────────────────────────────────────────────────────────────
echo ""
echo "═══════════════════════════════════════════════════════"
if [[ $DRY_RUN -eq 1 ]]; then
    echo "  Dry run complete — no changes made."
    echo "  Re-run without --check to apply."
else
    echo "  Update complete."
    echo "  Running on: $(git rev-parse --short HEAD 2>/dev/null || echo 'unknown')"
    echo "  $(date '+%Y-%m-%d %H:%M:%S')"
fi
echo "═══════════════════════════════════════════════════════"
echo ""
