#!/usr/bin/env bash
# pull.sh - Sync the application directory on cPanel with GitHub.
#
# Run this from cPanel's "Cron Jobs" or "Terminal":
#   bash /home/CPANELUSER/public_html/pull.sh
#
# Or trigger it from a GitHub webhook by hitting deploy.php which execs this.
#
# It will:
#   1. cd into the app directory
#   2. fetch + reset to origin/$BRANCH (matching what's on GitHub exactly)
#   3. preserve local-only files (.env, .htaccess) untouched across deploys
#   4. fix permissions
#
# Retire mode (server migration):
#
#   bash pull.sh --retire <store-uuid> [--base https://stores.maalcards.com]
#
# Replaces this store with a 301 to its home on the shared, UUID-routed
# storefront: syncs first (to get the current redirect template from git),
# writes the redirect .htaccess to the WEBROOT, deletes the app files, and
# drops a `.retired` marker. See migration/README.md.
#
# The marker is load-bearing: a later plain `pull.sh` (or a bulk update
# across every store) would otherwise `git reset --hard` the storefront
# straight back into service and silently un-retire the shop. Once the
# marker exists, a normal run refuses to touch anything. `--unretire`
# is the way back.

set -euo pipefail

# ---------- Configuration ----------
APP_DIR="${APP_DIR:-$(cd "$(dirname "$0")" && pwd)}"
BRANCH="${BRANCH:-main}"
REMOTE="${REMOTE:-origin}"
LOG_FILE="${LOG_FILE:-$APP_DIR/.pull.log}"

# --- retire-mode arguments -------------------------------------------
RETIRE_UUID=""
RETIRE_BASE="${MIGRATION_BASE_URL:-https://stores.maalcards.com}"
# The document root the vhost actually serves. The deploy layout puts the
# app in <docroot>/JawwalPayStore/, so the parent is the default.
WEBROOT="${WEBROOT:-}"
UNRETIRE=0
RETIRED_MARKER="$APP_DIR/.retired"
# Must stay in step with app/Core/Router.php::UUID_PATTERN on the new front end.
UUID_RE='^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$'

while [ $# -gt 0 ]; do
  case "$1" in
    --branch)    BRANCH="${2:-}";      shift 2 ;;
    --retire)    RETIRE_UUID="${2:-}"; shift 2 ;;
    --base)      RETIRE_BASE="${2:-}"; shift 2 ;;
    --webroot)   WEBROOT="${2:-}";     shift 2 ;;
    --unretire)  UNRETIRE=1;           shift ;;
    *)           shift ;;   # unknown flags ignored: callers append their own
  esac
done

[ -z "$WEBROOT" ] && WEBROOT="$(cd "$APP_DIR/.." && pwd)"
RETIRE_BASE="${RETIRE_BASE%/}"

# Local-only files that must survive a deploy untouched. `.env` is
# git-ignored; `.htaccess` is tracked, so a hard reset would otherwise
# overwrite it. We snapshot both to a temp dir (outside the repo, so git
# stash/clean/reset can't touch the backups) and restore them after the reset.
PROTECT=(".env" ".htaccess")
# -----------------------------------

ts() { date '+%Y-%m-%d %H:%M:%S'; }
log() { echo "[$(ts)] $*" | tee -a "$LOG_FILE"; }

# Backups live outside the working tree so they're immune to clean/reset.
BAKDIR="$(mktemp -d "${TMPDIR:-/tmp}/jp-deploy.XXXXXX")"
trap 'rm -rf "$BAKDIR" 2>/dev/null || true' EXIT

cd "$APP_DIR"

# --- retire-mode preflight -------------------------------------------
if [ -n "$RETIRE_UUID" ]; then
  if ! [[ "$RETIRE_UUID" =~ $UUID_RE ]]; then
    log "ERROR: --retire '$RETIRE_UUID' is not a UUID."
    log "  The new site routes on the store's public_id only; a numeric id or"
    log "  a slug would 301 every customer onto a 404."
    exit 2
  fi
  RETIRE_UUID="$(printf '%s' "$RETIRE_UUID" | tr '[:upper:]' '[:lower:]')"
  case "$RETIRE_BASE" in
    http://*|https://*) ;;
    *) log "ERROR: --base '$RETIRE_BASE' must be an absolute http(s) URL"; exit 2 ;;
  esac
fi

if [ "$UNRETIRE" = 1 ]; then
  if [ -f "$RETIRED_MARKER" ]; then
    rm -f "$RETIRED_MARKER"
    log "Un-retired: marker removed; syncing the storefront back into service."
    log "NOTE: the sync restores the app code, but NOT the two files retiring"
    log "      removed or replaced: this store's .env (API_URL / STORE_ID /"
    log "      API_SECRET) and the webroot .htaccess that used to rewrite into"
    log "      JawwalPayStore/. Without them the shop will not serve. Restore"
    log "      both from the pre-retire backup tarball, or redeploy the store."
  else
    log "Not retired; --unretire is a no-op."
  fi
fi

# A retired store must stay retired. Without this, one plain pull.sh — or a
# bulk update across every store — would reset --hard the storefront back
# into service, and customers already 301'd away would start seeing a live
# shop again on the old domain.
if [ -f "$RETIRED_MARKER" ] && [ -z "$RETIRE_UUID" ]; then
  log "==> Store is RETIRED (see $RETIRED_MARKER) — refusing to sync."
  log "    Its traffic is redirected by $WEBROOT/.htaccess."
  log "    To put this storefront back in service: bash pull.sh --unretire"
  exit 0
fi

log "==> Sync start in $APP_DIR (branch=$BRANCH)"

if [ ! -d .git ]; then
  log "ERROR: $APP_DIR is not a git repository. Clone it first:"
  log "  git clone -b $BRANCH <repo-url> $APP_DIR"
  exit 1
fi

# Snapshot protected files before we touch anything. For tracked ones
# (.htaccess), also tell git to ignore local edits so they aren't stashed
# and `git status` stays clean between deploys.
for f in "${PROTECT[@]}"; do
  if [ -f "$APP_DIR/$f" ]; then
    cp -p "$APP_DIR/$f" "$BAKDIR/$f" 2>/dev/null || true
    log "Protected: snapshotted $f"
  fi
  if git ls-files --error-unmatch "$f" >/dev/null 2>&1; then
    git update-index --skip-worktree "$f" >>"$LOG_FILE" 2>&1 || true
  fi
done

# Stash any accidental local edits so reset is clean (.env is git-ignored, so it's safe).
if ! git diff --quiet || ! git diff --cached --quiet; then
  log "Local changes detected; stashing before pull."
  git stash push -u -m "pull.sh-autostash-$(ts)" >>"$LOG_FILE" 2>&1 || true
fi

log "Fetching $REMOTE..."
git fetch --prune "$REMOTE" >>"$LOG_FILE" 2>&1

log "Resetting to $REMOTE/$BRANCH..."
git reset --hard "$REMOTE/$BRANCH" >>"$LOG_FILE" 2>&1

# Restore protected files clobbered by the hard reset, BEFORE cleaning.
# `git reset --hard` can clear the skip-worktree bit, so re-apply it for
# tracked files to keep `git status` clean for the next deploy.
for f in "${PROTECT[@]}"; do
  if [ -f "$BAKDIR/$f" ]; then
    cp -p "$BAKDIR/$f" "$APP_DIR/$f" 2>/dev/null || true
    log "Protected: restored $f"
  fi
  if git ls-files --error-unmatch "$f" >/dev/null 2>&1; then
    git update-index --skip-worktree "$f" >>"$LOG_FILE" 2>&1 || true
  fi
done

# Remove untracked files but KEEP git-ignored ones (.env, assets/uploads/...).
# Plain `git clean -fd` already skips ignored files — do NOT add -x or -X here:
# -X would delete every ignored file, including .env.
git clean -fd >>"$LOG_FILE" 2>&1 || true

# Permissions: typical cPanel - dirs 755, files 644, scripts 755.
log "Fixing permissions..."
find "$APP_DIR" -type d -not -path '*/.git/*' -exec chmod 755 {} \; 2>/dev/null || true
find "$APP_DIR" -type f -not -path '*/.git/*' -exec chmod 644 {} \; 2>/dev/null || true
chmod 755 "$APP_DIR/pull.sh" 2>/dev/null || true
[ -f "$APP_DIR/.env" ] && chmod 600 "$APP_DIR/.env" || true

# Reset PHP OPcache so the freshly-pulled code takes effect immediately.
# The web SAPI (PHP-FPM / Apache) caches bytecode separately from the CLI,
# so a CLI reset alone usually isn't enough — we also ping a tiny,
# self-deleting endpoint over HTTP to clear the web process's cache.
log "Resetting OPcache..."
php -r 'function_exists("opcache_reset") && @opcache_reset();' >/dev/null 2>&1 || true

# Resolve the site URL: env var wins, else read DEPLOY_BASE_URL / APP_URL from .env.
RESET_BASE="${DEPLOY_BASE_URL:-}"
if [ -z "$RESET_BASE" ] && [ -f "$APP_DIR/.env" ]; then
  RESET_BASE="$(sed -nE 's/^(DEPLOY_BASE_URL|APP_URL)=//p' "$APP_DIR/.env" | head -n1 | tr -d '\r')"
  # Strip surrounding quotes and whitespace.
  RESET_BASE="$(printf '%s' "$RESET_BASE" | sed -E 's/^[[:space:]]*["'\'']?//; s/["'\'']?[[:space:]]*$//')"
fi
if [ -n "$RESET_BASE" ]; then
  RESET_NAME=".opcache-reset-$(date +%s)-$$.php"
  printf '%s\n' "<?php if (function_exists('opcache_reset')) { @opcache_reset(); } @unlink(__FILE__); echo 'ok';" > "$APP_DIR/$RESET_NAME"
  chmod 644 "$APP_DIR/$RESET_NAME" 2>/dev/null || true
  curl -fsS --max-time 10 "${RESET_BASE%/}/$RESET_NAME" >>"$LOG_FILE" 2>&1 || true
  rm -f "$APP_DIR/$RESET_NAME" 2>/dev/null || true
  log "OPcache reset pinged at ${RESET_BASE%/}"
else
  log "OPcache web reset skipped (set DEPLOY_BASE_URL or APP_URL in .env to enable)."
fi

REV=$(git rev-parse --short HEAD)
log "==> Sync OK at $REV"

# ---------------------------------------------------------------------
# Retire: replace this storefront with a 301 to the shared storefront.
# ---------------------------------------------------------------------
# Runs AFTER the sync on purpose — the sync is what puts the current
# migration/redirect.htaccess template on disk, so the redirect installed
# here is always the one on main rather than whatever this store last
# happened to have.
if [ -n "$RETIRE_UUID" ]; then
  TARGET="$RETIRE_BASE/$RETIRE_UUID"
  TEMPLATE="$APP_DIR/migration/redirect.htaccess"
  log "==> Retiring store -> $TARGET"
  log "    webroot: $WEBROOT"

  if [ ! -d "$WEBROOT" ]; then
    log "ERROR: webroot $WEBROOT does not exist — pass --webroot explicitly."
    exit 3
  fi
  # Refuse to render from a stale checkout rather than silently installing a
  # different, worse redirect: a wrong file here is a 301 that sticks in every
  # customer's browser.
  if [ ! -f "$TEMPLATE" ]; then
    log "ERROR: $TEMPLATE missing — this checkout predates the migration tooling."
    log "  Update the store first (plain pull.sh), then retire it."
    exit 3
  fi

  # Back up before touching anything: the app files, the store's .env (which
  # holds its API_SECRET) and the webroot's existing .htaccess. Outside the
  # webroot so it is never web-reachable. A failure here stops the retire —
  # this tarball is the only way back if the UUID turns out to be wrong.
  STAMP="$(date +%Y%m%d%H%M%S)"
  BACKUP="$(dirname "$WEBROOT")/$(basename "$WEBROOT")-pre-retire-$STAMP.tgz"
  if ! tar -czf "$BACKUP" -C "$WEBROOT" . 2>/dev/null; then
    log "ERROR: backup to $BACKUP failed — not retiring."
    exit 3
  fi
  chmod 600 "$BACKUP" 2>/dev/null || true
  log "Backup: $BACKUP (holds .env / API_SECRET — move it off this server)"

  # Render the redirect to a temp file first: the template lives inside the
  # app dir we are about to delete.
  RENDERED="$(mktemp "${TMPDIR:-/tmp}/jp-redirect.XXXXXX")"
  sed -e "s|__NEW_BASE__|$RETIRE_BASE|g" \
      -e "s|__STORE_UUID__|$RETIRE_UUID|g" \
      -e "s|__STORE_NAME__|retired via pull.sh|g" \
      -e "s|__OLD_DOMAIN__|$(basename "$WEBROOT")|g" \
      "$TEMPLATE" > "$RENDERED"
  if grep -q '__[A-Z_]\+__' "$RENDERED"; then
    log "ERROR: template still has placeholders after rendering — not retiring."
    rm -f "$RENDERED"
    exit 3
  fi

  # Install the redirect at the webroot, replacing the wrapper .htaccess that
  # used to rewrite every request into JawwalPayStore/.
  cp "$RENDERED" "$WEBROOT/.htaccess"
  chmod 644 "$WEBROOT/.htaccess"
  rm -f "$RENDERED"
  log "Installed $WEBROOT/.htaccess -> $TARGET"

  # Delete the storefront. .git and pull.sh stay: bash reads this script
  # incrementally, so removing it mid-run is unsafe, and keeping .git means
  # `--unretire` can restore the shop with a plain sync.
  cd "$WEBROOT"
  find "$APP_DIR" -mindepth 1 -maxdepth 1 \
    ! -name '.git' ! -name 'pull.sh' ! -name '.retired' ! -name '.pull.log' \
    -exec rm -rf -- {} + 2>/dev/null || true
  log "Removed the storefront app files from $APP_DIR"

  printf 'retired %s -> %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$TARGET" > "$RETIRED_MARKER"
  log "==> Retired. $(basename "$WEBROOT") now 301s to $TARGET"
  echo "$REV"
  exit 0
fi

echo "$REV"
