#!/bin/bash
#
# WordPress Full Backup Script
# Captures: core files, plugins, themes, uploads, mu-plugins, and a complete SQL dump.
# Designed for clean restore on any compatible environment.
#
# Usage: ./wordpress-backup.sh
#

set -euo pipefail

# ---------------------------------------------------------------------------
# Colors & helpers
# ---------------------------------------------------------------------------
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
BOLD='\033[1m'
NC='\033[0m'

info()    { echo -e "${BLUE}[INFO]${NC}  $*"; }
success() { echo -e "${GREEN}[OK]${NC}    $*"; }
warn()    { echo -e "${YELLOW}[WARN]${NC}  $*"; }
error()   { echo -e "${RED}[ERROR]${NC} $*" >&2; }
die()     { error "$*"; exit 1; }

# ---------------------------------------------------------------------------
# Banner
# ---------------------------------------------------------------------------
echo
echo -e "${BOLD}WordPress Full Backup Script${NC}"
echo "================================="
echo "Captures core + plugins + themes + uploads + database SQL"
echo

# ---------------------------------------------------------------------------
# 1. Ask for WordPress root path
# ---------------------------------------------------------------------------
read -r -p "Enter the absolute path to the WordPress root directory: " WP_ROOT

# Expand ~ and resolve to absolute path
WP_ROOT="${WP_ROOT/#\~/$HOME}"
if command -v realpath >/dev/null 2>&1; then
  WP_ROOT=$(realpath -m "$WP_ROOT" 2>/dev/null || echo "$WP_ROOT")
fi

[[ -n "$WP_ROOT" ]] || die "Path cannot be empty."
[[ -d "$WP_ROOT" ]] || die "Directory does not exist: $WP_ROOT"
[[ -r "$WP_ROOT" ]] || die "Directory is not readable: $WP_ROOT"

info "Using WordPress root: $WP_ROOT"

# ---------------------------------------------------------------------------
# 2. Validate structure
# ---------------------------------------------------------------------------
info "Validating WordPress structure..."

REQUIRED_FILES=(wp-config.php wp-load.php index.php wp-blog-header.php)
REQUIRED_DIRS=(wp-admin wp-includes wp-content)

for f in "${REQUIRED_FILES[@]}"; do
  [[ -f "$WP_ROOT/$f" ]] || die "Missing required file: $f — this does not look like a WordPress installation."
done

for d in "${REQUIRED_DIRS[@]}"; do
  [[ -d "$WP_ROOT/$d" ]] || die "Missing required directory: $d — this does not look like a WordPress installation."
done

# Quick sanity check that wp-config.php actually contains DB defines
if ! grep -qE "define\s*\(\s*['\"]DB_NAME['\"]" "$WP_ROOT/wp-config.php"; then
  die "wp-config.php does not appear to contain DB_NAME definition."
fi

success "WordPress structure validated."

# ---------------------------------------------------------------------------
# 3. Check for symbolic links
# ---------------------------------------------------------------------------
info "Checking for symbolic links in critical paths..."

SYMLINK_WARNINGS=0
CRITICAL_PATHS=(
  "$WP_ROOT"
  "$WP_ROOT/wp-admin"
  "$WP_ROOT/wp-includes"
  "$WP_ROOT/wp-content"
  "$WP_ROOT/wp-content/plugins"
  "$WP_ROOT/wp-content/themes"
  "$WP_ROOT/wp-content/uploads"
  "$WP_ROOT/wp-content/mu-plugins"
)

declare -a FOUND_SYMLINKS=()

for path in "${CRITICAL_PATHS[@]}"; do
  if [[ -e "$path" && -L "$path" ]]; then
    target=$(readlink -f "$path" 2>/dev/null || readlink "$path")
    warn "Symbolic link detected: $path → $target"
    FOUND_SYMLINKS+=("$path")
    ((SYMLINK_WARNINGS++)) || true
  fi
done

# Also surface any other top-level symlinks under the root (lightweight scan)
while IFS= read -r -d '' link; do
  # Skip the ones we already reported
  already=0
  for known in "${FOUND_SYMLINKS[@]+"${FOUND_SYMLINKS[@]}"}"; do
    [[ "$link" == "$known" ]] && already=1 && break
  done
  if [[ $already -eq 0 ]]; then
    target=$(readlink -f "$link" 2>/dev/null || readlink "$link")
    warn "Additional symlink: $link → $target"
    ((SYMLINK_WARNINGS++)) || true
  fi
done < <(find "$WP_ROOT" -maxdepth 2 -type l -print0 2>/dev/null || true)

if [[ $SYMLINK_WARNINGS -gt 0 ]]; then
  echo
  warn "Symbolic links were found. The backup will follow them (--dereference)"
  warn "so the actual file content is captured rather than the link itself."
  echo
  read -r -p "Continue with backup? [Y/n]: " CONTINUE
  CONTINUE=${CONTINUE:-Y}
  if [[ ! "$CONTINUE" =~ ^[Yy]$ ]]; then
    die "Backup aborted by user."
  fi
  TAR_DEREF="--dereference"
else
  success "No critical symbolic links found."
  TAR_DEREF=""
fi

# ---------------------------------------------------------------------------
# 4. Extract configuration from wp-config.php (robust PHP parser)
# ---------------------------------------------------------------------------
info "Reading database credentials from wp-config.php..."

WP_CONFIG="$WP_ROOT/wp-config.php"

# Prefer PHP for accurate parsing of the PHP source
extract_wp_constant() {
  local key="$1"
  php -r "
    \$src = file_get_contents('${WP_CONFIG}');
    // Match define( 'KEY', 'value' ) or define(\"KEY\", \"value\")
    if (preg_match(\"/define\\s*\\(\\s*['\\\"]${key}['\\\"]\\s*,\\s*['\\\"]([^'\\\"]*)['\\\"]\\s*\\)/s\", \$src, \$m)) {
      echo \$m[1];
    }
  " 2>/dev/null || true
}

extract_table_prefix() {
  php -r "
    \$src = file_get_contents('${WP_CONFIG}');
    if (preg_match('/\\\$table_prefix\s*=\s*[\'\"]([^\'\"]*)[\'\"]\s*;/', \$src, \$m)) {
      echo \$m[1];
    }
  " 2>/dev/null || true
}

DB_NAME=$(extract_wp_constant "DB_NAME")
DB_USER=$(extract_wp_constant "DB_USER")
DB_PASSWORD=$(extract_wp_constant "DB_PASSWORD")
DB_HOST=$(extract_wp_constant "DB_HOST")
TABLE_PREFIX=$(extract_table_prefix)

# Fallbacks with grep if PHP is unavailable or parsing failed
if [[ -z "$DB_NAME" ]]; then
  DB_NAME=$(grep -oP "define\s*\(\s*['\"]DB_NAME['\"]\s*,\s*['\"]\K[^'\"]+" "$WP_CONFIG" 2>/dev/null || true)
fi
if [[ -z "$DB_USER" ]]; then
  DB_USER=$(grep -oP "define\s*\(\s*['\"]DB_USER['\"]\s*,\s*['\"]\K[^'\"]+" "$WP_CONFIG" 2>/dev/null || true)
fi
if [[ -z "$DB_PASSWORD" ]]; then
  DB_PASSWORD=$(grep -oP "define\s*\(\s*['\"]DB_PASSWORD['\"]\s*,\s*['\"]\K[^'\"]+" "$WP_CONFIG" 2>/dev/null || true)
fi
if [[ -z "$DB_HOST" ]]; then
  DB_HOST=$(grep -oP "define\s*\(\s*['\"]DB_HOST['\"]\s*,\s*['\"]\K[^'\"]+" "$WP_CONFIG" 2>/dev/null || true)
fi
if [[ -z "$TABLE_PREFIX" ]]; then
  TABLE_PREFIX=$(grep -oP "\\\$table_prefix\s*=\s*['\"]\K[^'\"]+" "$WP_CONFIG" 2>/dev/null || echo "wp_")
fi
DB_USER='root'
DB_PASSWORD='bis.bald'

[[ -n "$DB_NAME" ]]     || die "Could not determine DB_NAME from wp-config.php"
[[ -n "$DB_USER" ]]     || die "Could not determine DB_USER from wp-config.php"
[[ -n "$DB_HOST" ]]     || die "Could not determine DB_HOST from wp-config.php"
# Password may legitimately be empty
TABLE_PREFIX=${TABLE_PREFIX:-wp_}

# Show what we extracted (password is masked)
if [[ -n "$DB_PASSWORD" ]]; then
  PW_MASK="${DB_PASSWORD:0:2}****${DB_PASSWORD: -2}"
else
  PW_MASK="(empty)"
fi
info "Extracted DB credentials:"
info "  DB_NAME   = $DB_NAME"
info "  DB_USER   = $DB_USER"
info "  DB_HOST   = $DB_HOST"
info "  DB_PASS   = $PW_MASK"
info "  table_prefix = $TABLE_PREFIX"

# ---------------------------------------------------------------------------
# 5. Determine site name for the backup filename
# ---------------------------------------------------------------------------
info "Determining site name for backup filename..."

SITE_NAME=""

# Try to pull blogname from the live database
if command -v mysql >/dev/null 2>&1; then
  # Use MYSQL_PWD to avoid exposing password on command line (still not perfect, but better)
  export MYSQL_PWD="$DB_PASSWORD"
  SITE_NAME=$(mysql -h"$DB_HOST" -u"$DB_USER" "$DB_NAME" -N -s \
    -e "SELECT option_value FROM \`${TABLE_PREFIX}options\` WHERE option_name = 'blogname' LIMIT 1;" \
    2>/dev/null || true)
  unset MYSQL_PWD
fi

# Fallback: try siteurl and extract hostname
if [[ -z "$SITE_NAME" ]] && command -v mysql >/dev/null 2>&1; then
  export MYSQL_PWD="$DB_PASSWORD"
  SITEURL=$(mysql -h"$DB_HOST" -u"$DB_USER" "$DB_NAME" -N -s \
    -e "SELECT option_value FROM \`${TABLE_PREFIX}options\` WHERE option_name = 'siteurl' LIMIT 1;" \
    2>/dev/null || true)
  unset MYSQL_PWD
  if [[ -n "$SITEURL" ]]; then
    # Extract domain-ish part
    SITE_NAME=$(echo "$SITEURL" | sed -E 's|https?://||; s|/.*||; s|:.*||')
  fi
fi

# Fallback: directory basename
if [[ -z "$SITE_NAME" ]]; then
  SITE_NAME=$(basename "$WP_ROOT")
  warn "Could not read site name from database — using directory name: $SITE_NAME"
fi

# Sanitize for filesystem safety
SITE_NAME=$(echo "$SITE_NAME" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+|-+$//g')
[[ -n "$SITE_NAME" ]] || SITE_NAME="wordpress"

# Final confirmation / override
echo
info "Suggested backup name base: ${SITE_NAME}"
read -r -p "Press Enter to accept, or type a different name: " CUSTOM_NAME
if [[ -n "$CUSTOM_NAME" ]]; then
  SITE_NAME=$(echo "$CUSTOM_NAME" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+|-+$//g')
fi

TIMESTAMP=$(date +%Y%m%d-%H%M%S)
BACKUP_BASENAME="${SITE_NAME}-backup-${TIMESTAMP}"
BACKUP_FILE="${BACKUP_BASENAME}.tar.gz"

echo
info "Backup will be written as: ${BACKUP_FILE}"

# ---------------------------------------------------------------------------
# 6. Create temporary working directory
# ---------------------------------------------------------------------------
TMPDIR=$(mktemp -d -t wp-backup-XXXXXX)
trap 'rm -rf "$TMPDIR"' EXIT

SQL_FILE="${TMPDIR}/database.sql"
info "Temporary workspace: $TMPDIR"

# ---------------------------------------------------------------------------
# 7. Dump the database
# ---------------------------------------------------------------------------
info "Creating database dump..."

if ! command -v mysqldump >/dev/null 2>&1; then
  die "mysqldump is not installed or not in PATH. Cannot create SQL dump."
fi

# Use a temporary defaults file so passwords with special characters work reliably
# and so we do not expose the password on the process list as much as -p does.
MYSQL_CNF="${TMPDIR}/mysql.cnf"
cat > "$MYSQL_CNF" << EOF
[client]
host=${DB_HOST}
user=${DB_USER}
password=${DB_PASSWORD}
EOF
chmod 600 "$MYSQL_CNF"

# Capture stderr so the user can see real errors instead of a silent failure
DUMP_ERR="${TMPDIR}/mysqldump.err"

# Comprehensive dump options for a clean restore
set +e
mysqldump \
  --defaults-extra-file="$MYSQL_CNF" \
  --single-transaction \
  --quick \
  --lock-tables=false \
  --routines \
  --triggers \
  --events \
  --add-drop-table \
  --default-character-set=utf8mb4 \
  "$DB_NAME" > "$SQL_FILE" 2>"$DUMP_ERR"
DUMP_RC=$?
set -e

if [[ $DUMP_RC -ne 0 ]]; then
  echo
  error "mysqldump failed (exit code $DUMP_RC)."
  if [[ -s "$DUMP_ERR" ]]; then
    echo -e "${RED}----- mysqldump error output -----${NC}"
    cat "$DUMP_ERR"
    echo -e "${RED}----------------------------------${NC}"
  fi
  echo
  error "Common causes:"
  error "  • Wrong DB_USER / DB_PASSWORD / DB_HOST extracted from wp-config.php"
  error "  • MySQL user lacks privileges for this database"
  error "  • Host is 'localhost' but the server expects 127.0.0.1 (or vice-versa)"
  error "  • Socket authentication vs password authentication mismatch"
  die "Database dump aborted."
fi

if [[ ! -s "$SQL_FILE" ]]; then
  die "Database dump produced an empty file. Check credentials and connectivity."
fi

SQL_SIZE=$(du -h "$SQL_FILE" | cut -f1)
success "Database dump created (${SQL_SIZE})"

# ---------------------------------------------------------------------------
# 8. Create the archive
# ---------------------------------------------------------------------------
info "Archiving WordPress files + database..."

# We place the SQL dump at the root of the archive so restore is obvious.
# Files are archived relative to WP_ROOT so the structure is preserved.

# Exclude common transient / cache directories to keep the archive smaller
# while still capturing everything required for a full restore.
EXCLUDE_ARGS=(
  --exclude='./wp-content/cache'
  --exclude='./wp-content/upgrade'
  --exclude='./wp-content/uploads/cache'
  --exclude='./wp-content/wflogs'
  --exclude='./wp-content/ai1wm-backups'
  --exclude='./wp-content/backups'
  --exclude='./wp-content/updraft'
  --exclude='./*.log'
  --exclude='./.git'
  --exclude='./.svn'
  --exclude='./node_modules'
)

# Create the tarball
# Note: we cd into WP_ROOT so paths inside the archive are clean
(
  cd "$WP_ROOT"
  tar -czf "${TMPDIR}/${BACKUP_FILE}" \
    ${TAR_DEREF} \
    "${EXCLUDE_ARGS[@]}" \
    --exclude="./${BACKUP_FILE}" \
    .
)

# Add the SQL dump into the already-created archive
# (tar --append works on uncompressed, so we use a small trick with a second stage)
# Simpler & more reliable: recreate with both contents
(
  cd "$TMPDIR"
  # Unpack the files archive into a staging area, add SQL, re-pack
  mkdir -p staging
  tar -xzf "$BACKUP_FILE" -C staging
  cp "$SQL_FILE" staging/database.sql
  # Also drop a small README for the restore process
  cat > staging/BACKUP-README.txt << EOF
WordPress Backup created on $(date -u +"%Y-%m-%d %H:%M:%S UTC")
Site name      : ${SITE_NAME}
Source path    : ${WP_ROOT}
Database       : ${DB_NAME}
Table prefix   : ${TABLE_PREFIX}

RESTORE INSTRUCTIONS
====================
1. Extract this archive into the target web root:
     tar -xzf ${BACKUP_FILE} -C /path/to/new/wordpress

2. Import the database:
     mysql -u DB_USER -p DB_NAME < database.sql

3. Update wp-config.php with the new database credentials if they differ.

4. If the domain or path has changed, run a search-replace on the database
   (WP-CLI recommended):
     wp search-replace 'old-url' 'new-url' --all-tables

5. Ensure correct file ownership and permissions:
     chown -R www-data:www-data .
     find . -type d -exec chmod 755 {} \;
     find . -type f -exec chmod 644 {} \;

6. Delete this BACKUP-README.txt and database.sql after a successful restore
   (or keep them for reference).
EOF

  tar -czf "$BACKUP_FILE" -C staging .
  rm -rf staging
)

# Move final archive to the current working directory
FINAL_PATH="$(pwd)/${BACKUP_FILE}"
mv "${TMPDIR}/${BACKUP_FILE}" "$FINAL_PATH"

ARCHIVE_SIZE=$(du -h "$FINAL_PATH" | cut -f1)

# ---------------------------------------------------------------------------
# 9. Summary
# ---------------------------------------------------------------------------
echo
echo -e "${GREEN}${BOLD}Backup completed successfully${NC}"
echo "----------------------------------------"
echo "  File     : $FINAL_PATH"
echo "  Size     : $ARCHIVE_SIZE"
echo "  Site     : $SITE_NAME"
echo "  Database : $DB_NAME (included as database.sql)"
echo "----------------------------------------"
echo
echo "Contents of the archive:"
echo "  • Complete WordPress file tree (core, plugins, themes, uploads, etc.)"
echo "  • database.sql  – full MySQL dump ready for import"
echo "  • BACKUP-README.txt – restore instructions"
echo
success "Done."
echo
