#!/usr/bin/env bash
#
# Django -> cPanel deployment using rsync over SSH.
#
# Uploads the source with rsync, then runs the Django release steps on the
# server (pip install, migrate, collectstatic) and restarts Passenger.
#
# Required:
#   .env.deploy   (see .env.deploy.example)
#
# Usage:
#   ./deploy.sh


set -euo pipefail

cd "$(dirname "$0")"

START_TS=$(date +%s)

# =============================================================================
# Output helpers
# =============================================================================

STEP=0
step() { STEP=$((STEP + 1)); printf '\n\033[1;36m[%d] %s\033[0m\n' "$STEP" "$1"; }
info() { printf '    \033[0;90m%s\033[0m\n' "$1"; }
ok()   { printf '    \033[0;32m✔ %s\033[0m\n' "$1"; }
warn() { printf '    \033[0;33m⚠ %s\033[0m\n' "$1"; }
die()  { printf '\n\033[0;31m✖ %s\033[0m\n' "$1" >&2; exit 1; }

# =============================================================================
# 1. Load deployment configuration
# =============================================================================

step "Loading deployment configuration"

[[ -f .env.deploy ]] || die "Missing .env.deploy (copy .env.deploy.example)."

set -a
# shellcheck disable=SC1091
source .env.deploy
set +a

: "${SSH_HOST:?Set SSH_HOST in .env.deploy}"
: "${SSH_USER:?Set SSH_USER in .env.deploy}"
: "${REMOTE_DIR:?Set REMOTE_DIR in .env.deploy}"
: "${VENV_ACTIVATE:?Set VENV_ACTIVATE in .env.deploy (path to the cPanel venv activate script)}"

SSH_PORT="${SSH_PORT:-22}"
SSH_KEY="${SSH_KEY:-.ssh/shopapp_deploy}"
DEPLOY_URL="${DEPLOY_URL:-}"
PYTHON_BIN="${PYTHON_BIN:-python3}"

RUN_PIP="${RUN_PIP:-true}"
RUN_MIGRATE="${RUN_MIGRATE:-true}"
RUN_COLLECTSTATIC="${RUN_COLLECTSTATIC:-true}"
RUN_CREATE_ADMIN="${RUN_CREATE_ADMIN:-true}"

# Remove trailing slash.
REMOTE_DIR="${REMOTE_DIR%/}"

# Root of the venv (…/bin/activate -> …). Used to link ./venv in the app root so
# Passenger (Application Manager) runs the app with the venv interpreter.
VENV_ROOT="${VENV_ACTIVATE%/bin/activate}"

# Guard rsync --delete: refuse dangerous / home-root targets.
case "$REMOTE_DIR" in
  ""|"/"|"~"|"."|"$HOME"|"$HOME/")
    die "REMOTE_DIR is unsafe for rsync --delete: '$REMOTE_DIR'"
    ;;
  */*) ;;   # must be a nested path, not a bare top-level name
  *) die "REMOTE_DIR must be a nested path (e.g. myapp/ or public_html/api), got '$REMOTE_DIR'";;
esac

info "Host       : $SSH_HOST"
info "User       : $SSH_USER"
info "SSH port   : $SSH_PORT"
info "SSH key    : $SSH_KEY"
info "Remote dir : $REMOTE_DIR"
info "Venv       : $VENV_ACTIVATE"
[[ -n "$DEPLOY_URL" ]] && info "Deploy URL : $DEPLOY_URL"

# ---------------------------------------------------------------------------
# Consistency guard: REMOTE_DIR must be the SAME directory Passenger runs from.
# ---------------------------------------------------------------------------
# Normalise REMOTE_DIR to a home-relative path (drop a leading /home/<user>/).
REMOTE_REL="${REMOTE_DIR#/home/$SSH_USER/}"
REMOTE_REL="${REMOTE_REL#/}"

# Only cPanel-managed venvs encode the app root in their path

if [[ "$VENV_ACTIVATE" == */virtualenv/*/bin/activate ]]; then
  VENV_APP_ROOT="${VENV_ACTIVATE#*/virtualenv/}"
  VENV_APP_ROOT="${VENV_APP_ROOT%/*/bin/activate}"
  if [[ "$REMOTE_REL" != "$VENV_APP_ROOT" ]]; then
    warn "REMOTE_DIR ('$REMOTE_REL') does not match the venv's app root ('$VENV_APP_ROOT')."
    warn "Passenger runs the app from '$VENV_APP_ROOT'; deploying elsewhere means the"
    warn "live backend won't pick up your changes."
    if [[ "${ALLOW_DIR_MISMATCH:-false}" != "true" ]]; then
      die "Aborting. Set REMOTE_DIR=$VENV_APP_ROOT (recommended), or re-run with ALLOW_DIR_MISMATCH=true if you know what you're doing."
    fi
    warn "ALLOW_DIR_MISMATCH=true — continuing anyway."
  fi
else
  info "Custom venv path (not cPanel ~/virtualenv/…) — skipping app-root mirror check."
  info "Ensure the Passenger app root is '$REMOTE_REL' and its interpreter is this venv."
fi

ok "Configuration loaded"

# =============================================================================
# 2. Check required local tools
# =============================================================================

step "Checking local tools"

for tool in ssh ssh-add ssh-agent rsync; do
  if command -v "$tool" >/dev/null 2>&1; then
    info "$(printf '%-10s %s' "$tool" "$(command -v "$tool")")"
  else
    die "'$tool' is not installed."
  fi
done

ok "Local tools ready"

# =============================================================================
# 3. Prepare existing SSH private key
# =============================================================================

step "Preparing SSH key"

[[ -f "$SSH_KEY" ]] || die "SSH private key not found: $SSH_KEY"
chmod 600 "$SSH_KEY"
info "Using existing key: $SSH_KEY"

if [[ -z "${SSH_AUTH_SOCK:-}" ]] || ! ssh-add -l >/dev/null 2>&1; then
  info "Starting ssh-agent"
  eval "$(ssh-agent -s)" >/dev/null
fi

KEY_ALREADY_LOADED=false
if [[ -f "${SSH_KEY}.pub" ]] && ssh-add -T "${SSH_KEY}.pub" >/dev/null 2>&1; then
  KEY_ALREADY_LOADED=true
fi

if [[ "$KEY_ALREADY_LOADED" == "true" ]]; then
  info "SSH key already loaded in ssh-agent"
else
  info "Loading SSH key into ssh-agent"
  # A passphrase-protected key asks once here.
  ssh-add "$SSH_KEY" || die "Unable to load SSH key: $SSH_KEY"
fi

ok "SSH key ready"

# =============================================================================
# 4. Configure SSH connection reuse
# =============================================================================

step "Configuring SSH"

# Keep this SHORT: Unix domain sockets cap the path at ~104 chars, and macOS
# TMPDIR (/var/folders/…) plus %C easily overflows it. Use /tmp + a short prefix.
CONTROL_PATH="/tmp/sa-%C"

SSH_OPTS=(
  -p "$SSH_PORT"
  -o StrictHostKeyChecking=accept-new
  -o ConnectTimeout=15
  -o BatchMode=yes
  -o IdentitiesOnly=yes
  -i "$SSH_KEY"
  -o ControlMaster=auto
  -o ControlPersist=120
  -o "ControlPath=$CONTROL_PATH"
)

info "Target: ${SSH_USER}@${SSH_HOST}:${SSH_PORT}"
ok "SSH configured"

# =============================================================================
# 5. Verify SSH connection
# =============================================================================

step "Verifying SSH connection"

if ssh "${SSH_OPTS[@]}" "${SSH_USER}@${SSH_HOST}" \
    'printf "Connected as %s on %s\n" "$(whoami)" "$(hostname)"'; then
  ok "SSH connection works"
else
  printf '\nSSH reached the server but authentication failed.\n\n'
  if [[ -f "${SSH_KEY}.pub" ]]; then
    printf 'Ensure this public key is authorized in cPanel:\n\n'
    cat "${SSH_KEY}.pub"; printf '\n\n'
  fi
  printf 'cPanel: Security → SSH Access → Manage SSH Keys → Manage → Authorize\n\n'
  die "SSH authentication failed."
fi

# =============================================================================
# 6. Check rsync on cPanel
# =============================================================================

step "Checking remote rsync"

REMOTE_RSYNC="$(ssh "${SSH_OPTS[@]}" "${SSH_USER}@${SSH_HOST}" 'command -v rsync || true')"
[[ -n "$REMOTE_RSYNC" ]] || die "rsync is not installed on the cPanel server."
info "Remote rsync: $REMOTE_RSYNC"
ok "Remote rsync available"

# =============================================================================
# 7. Check remote deployment directory
# =============================================================================

step "Checking remote deployment directory"

if ssh "${SSH_OPTS[@]}" "${SSH_USER}@${SSH_HOST}" \
    "mkdir -p '$REMOTE_DIR' && test -d '$REMOTE_DIR' && test -w '$REMOTE_DIR'"; then
  ok "Remote directory is ready"
else
  die "Cannot create or write to $REMOTE_DIR"
fi

# The venv must exist on the server (it's created by Setup Python App, not by us).
if ssh "${SSH_OPTS[@]}" "${SSH_USER}@${SSH_HOST}" "test -f '$VENV_ACTIVATE'"; then
  ok "Virtualenv found on server"
else
  die "VENV_ACTIVATE not found on server: $VENV_ACTIVATE
    Create the app in cPanel → Setup Python App and copy the exact venv path."
fi

# =============================================================================
# 8. Local Django check (sanity gate before uploading)
# =============================================================================

if [[ "${SKIP_CHECK:-false}" == "true" ]]; then
  step "Local Django check skipped"
  info "SKIP_CHECK=true"
else
  step "Running local Django check"
  LOCAL_PY="$PYTHON_BIN"
  [[ -x .venv/bin/python ]] && LOCAL_PY=".venv/bin/python"
  info "Using: $LOCAL_PY"
  # Force sqlite for the check so it validates config/imports without needing a
  # DB driver locally (your local Django/interpreter may reject the prod MySQL driver).
  DATABASE_URL="sqlite:///:memory:" "$LOCAL_PY" manage.py check \
    || die "manage.py check failed — fix issues before deploying."
  ok "Django check passed"
fi

# =============================================================================
# 9. Determine rsync progress support
# =============================================================================

RSYNC_VERSION="$(rsync --version | awk 'NR == 1 { print $3 }')"
RSYNC_MAJOR="${RSYNC_VERSION%%.*}"
info "Local rsync: $RSYNC_VERSION"

RSYNC_DISPLAY_ARGS=(--stats)
if [[ "$RSYNC_MAJOR" =~ ^[0-9]+$ ]] && (( RSYNC_MAJOR >= 3 )); then
  RSYNC_DISPLAY_ARGS+=(--info=progress2)
else
  RSYNC_DISPLAY_ARGS+=(--progress)
fi

# =============================================================================
# 10. Prepare rsync SSH transport
# =============================================================================

step "Preparing rsync"

RSH_PARTS=(
  ssh -p "$SSH_PORT"
  -o StrictHostKeyChecking=accept-new
  -o ConnectTimeout=15
  -o BatchMode=yes
  -o IdentitiesOnly=yes
  -i "$SSH_KEY"
  -o ControlMaster=auto
  -o ControlPersist=120
  -o "ControlPath=$CONTROL_PATH"
)
printf -v RSH '%q ' "${RSH_PARTS[@]}"
RSH="${RSH% }"

info "Source : ./ (project root)"
info "Target : ${SSH_USER}@${SSH_HOST}:${REMOTE_DIR}/"
ok "rsync ready"

# =============================================================================
# 11. Upload source
#
# Excluded paths are ALSO protected from --delete, so the server's .env,
# staticfiles/, media/, and virtualenv are never touched by the sync.
# =============================================================================

step "Uploading source"

rsync \
  -az \
  --delay-updates \
  --delete-delay \
  --human-readable \
  "${RSYNC_DISPLAY_ARGS[@]}" \
  --no-owner \
  --no-group \
  --omit-dir-times \
  --exclude='.git*' \
  --exclude='.venv/' \
  --exclude='venv/' \
  --exclude='__pycache__/' \
  --exclude='*.pyc' \
  --exclude='.env.deploy' \
  --exclude='.ssh/' \
  --exclude='db.sqlite3' \
  --exclude='staticfiles/' \
  --exclude='media/' \
  --exclude='.idea/' \
  --exclude='.vscode/' \
  --exclude='*.log' \
  --exclude='.htaccess' \
  --exclude='tmp/' \
  --rsync-path="mkdir -p '$REMOTE_DIR' && '$REMOTE_RSYNC'" \
  -e "$RSH" \
  ./ \
  "${SSH_USER}@${SSH_HOST}:${REMOTE_DIR}/"

ok "Source uploaded"

# =============================================================================
# 12. Run Django release steps on the server
# =============================================================================

step "Running release steps on the server"

REMOTE_SCRIPT="set -e
source '$VENV_ACTIVATE'
cd '$REMOTE_DIR'
# Fail loudly if the venv didn't activate — otherwise pip silently installs into
# ~/.local under the system Python (\"Defaulting to user installation\").
if [ -z \"\${VIRTUAL_ENV:-}\" ]; then
  echo 'ERROR: virtualenv not active after sourcing:' '$VENV_ACTIVATE' >&2
  exit 1
fi
# Resolve the interpreter — some venvs expose only python3.
if command -v python >/dev/null 2>&1; then PY=python
elif command -v python3 >/dev/null 2>&1; then PY=python3
else
  echo 'ERROR: no python/python3 in this venv — it is not a working cPanel Python App venv.' >&2
  echo 'Create the app in cPanel > Setup Python App and use ITS venv path.' >&2
  exit 1
fi
echo \"venv   : \$VIRTUAL_ENV\"
echo \"python : \$(command -v \$PY) (\$(\$PY -V 2>&1))\"
# Link ./venv -> the real venv so passenger_wsgi.py re-execs into it. cPanel
# Application Manager does not set PassengerPython, so without this Passenger
# would run the app under the system Python (no Django) -> 503.
echo '-> link venv'; ln -sfn '$VENV_ROOT' venv
"
if [[ "$RUN_PIP" == "true" ]]; then
  REMOTE_SCRIPT+="echo '-> pip install'; \$PY -m pip install -r requirements.txt
"
fi
if [[ "$RUN_MIGRATE" == "true" ]]; then
  REMOTE_SCRIPT+="echo '-> migrate'; \$PY manage.py migrate --noinput
"
fi
if [[ "$RUN_CREATE_ADMIN" == "true" ]]; then
  REMOTE_SCRIPT+="echo '-> create default admin'; \$PY manage.py create_default_admin
"
fi
if [[ "$RUN_COLLECTSTATIC" == "true" ]]; then
  REMOTE_SCRIPT+="echo '-> collectstatic'; \$PY manage.py collectstatic --noinput
"
fi
REMOTE_SCRIPT+="echo '-> restart passenger'; mkdir -p tmp && touch tmp/restart.txt
"

ssh "${SSH_OPTS[@]}" "${SSH_USER}@${SSH_HOST}" "$REMOTE_SCRIPT" \
  || die "Remote release steps failed. Check the output above and the server logs."

ok "Release steps complete"

# =============================================================================
# 13. Verify deployment
# =============================================================================

step "Verifying deployment"

if ssh "${SSH_OPTS[@]}" "${SSH_USER}@${SSH_HOST}" \
    "test -f '$REMOTE_DIR/passenger_wsgi.py' && test -f '$REMOTE_DIR/manage.py'"; then
  ok "passenger_wsgi.py and manage.py present"
else
  die "Deployment finished but expected files are missing on the server."
fi

# =============================================================================
# 14. Optional website check
# =============================================================================

if [[ -n "$DEPLOY_URL" ]]; then
  step "Checking website"
  if command -v curl >/dev/null 2>&1; then
    HTTP_STATUS="$(curl -L -sS -o /dev/null -w '%{http_code}' \
      --connect-timeout 10 --max-time 30 "$DEPLOY_URL" || true)"
    case "$HTTP_STATUS" in
      2??|3??) ok "Website responded with HTTP $HTTP_STATUS";;
      *) warn "Deployed, but $DEPLOY_URL returned HTTP ${HTTP_STATUS:-unknown}";;
    esac
  else
    warn "curl not installed. Website check skipped."
  fi
fi

# =============================================================================
# 15. Close SSH master connection
# =============================================================================

ssh "${SSH_OPTS[@]}" -O exit "${SSH_USER}@${SSH_HOST}" >/dev/null 2>&1 || true

# =============================================================================
# Done
# =============================================================================

ELAPSED=$(( $(date +%s) - START_TS ))

printf '\n\033[1;32m============================================================\033[0m\n'
printf '\033[1;32m✔ DEPLOYMENT SUCCESSFUL\033[0m\n'
printf '\033[1;32m============================================================\033[0m\n\n'
printf 'Time   : %ds\n' "$ELAPSED"
printf 'Host   : %s\n' "$SSH_HOST"
printf 'Remote : %s\n' "$REMOTE_DIR"
[[ -n "$DEPLOY_URL" ]] && printf 'URL    : %s\n' "$DEPLOY_URL"
printf '\n'
